<?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" version="2.0"><channel><title><![CDATA[Rajeev.dev | Exploring Code, AI Tools, and Fullstack Projects]]></title><description><![CDATA[Rajeev R Sharma's blog for curious developers—covering JavaScript, Nuxt, AI experiments, and much more.]]></description><link>https://rajeev.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 22 Apr 2026 11:50:59 GMT</lastBuildDate><atom:link href="https://rajeev.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building Brew Haven: A/B Testing My Coffee Shop Dreams with DevCycle]]></title><description><![CDATA[Do you love coffee? As developers, many of us jokingly claim to be "powered by coffee", and the thought of opening a quaint coffee shop someday often lingers in the back of our minds — perhaps as a post-dev career dream.
When brainstorming ideas for ...]]></description><link>https://rajeev.dev/building-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle</link><guid isPermaLink="true">https://rajeev.dev/building-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle</guid><category><![CDATA[devcycle]]></category><category><![CDATA[webdev]]></category><category><![CDATA[React]]></category><category><![CDATA[  feature flags]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sun, 29 Dec 2024 11:09:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735466036644/1b223b14-15c4-401a-b08c-bb3816a5a85a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Do you love coffee? As developers, many of us jokingly claim to be "powered by coffee", and the thought of opening a quaint coffee shop someday often lingers in the back of our minds — perhaps as a post-dev career dream.</p>
<p>When brainstorming ideas for a fun project to showcase feature flags, this coffee shop fantasy kept nudging me: "Pick me! Build me!". So, I decided to indulge that thought and create Brew Haven, a dummy coffee shop app that explores the power of feature flags.</p>
<blockquote>
<p>This project was originally created as part of a dev challenge to showcase the power of feature flags in an engaging way.</p>
</blockquote>
<h2 id="heading-project-overview">Project Overview</h2>
<p>As you might have guessed, I built a playground to explore the power of feature flags, packaged as a coffee shop app. The app features customizable menus, seasonal items, and dynamic A/B testing for promotions using the DevCycle SDK. It also leverages the DevCycle Management APIs to power an admin panel, giving shop managers full control over these features in real time.</p>
<h3 id="heading-demo">Demo</h3>
<p>The following video gives a short walkthrough of the app.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/Xp6i7GXl4hM">https://youtu.be/Xp6i7GXl4hM</a></div>
<p> </p>
<p>You can try out the live app here: <a target="_blank" href="https://brewwhaven.netlify.app">Brew Haven</a></p>
<blockquote>
<p>Note: The admin page uses a dummy password auth. Use <code>admin@123</code> passowrd to view the admin panel and play around with the available feature flags.</p>
</blockquote>
<h3 id="heading-technologies-used">Technologies Used</h3>
<p>To bring Brew Haven to life, I used the following:</p>
<ul>
<li><p>React with Vite for a fast and modular frontend.</p>
</li>
<li><p>Shadcn UI for styling and components.</p>
</li>
<li><p>Netlify Functions for secure serverless DevCycle Management API calls.</p>
</li>
<li><p>DevCycle SDK for feature flag integration on the client side.</p>
</li>
</ul>
<h2 id="heading-what-is-devcycle">What is DevCycle?</h2>
<p>DevCycle is a feature management platform that helps developers experiment with new features, run A/B tests, and gradually roll out updates without downtime. It uses <strong>feature flags</strong>—switches in your code that let you turn specific features on or off in real time.</p>
<p>With DevCycle, you can:</p>
<ul>
<li><p>Deploy features safely using controlled rollouts.</p>
</li>
<li><p>Customize user experiences through A/B testing.</p>
</li>
<li><p>Quickly respond to user feedback without redeploying your code.</p>
</li>
</ul>
<p>DevCycle integrates seamlessly into your development process, making feature management simple and efficient. In Brew Haven, I used it to power dynamic menu customization, promotions, and more.</p>
<h3 id="heading-how-does-devcycle-work">How Does DevCycle Work?</h3>
<p>DevCycle is a powerful tool designed to simplify the management of feature flags and provide advanced functionality for tailoring your software’s behavior. Here’s an overview of how it works:</p>
<ol>
<li><p><strong>Feature Flags and Feature Types</strong><br /> At the heart of DevCycle are <strong>features</strong>, which can have one or more toggles (or flags). Features are categorized into four types, tailored for specific use cases:</p>
<ul>
<li><p><strong>Release</strong>: Designed for separating code deployment from feature release. This enables merging incomplete or in-progress code into production without affecting end users until it’s toggled on.</p>
</li>
<li><p><strong>Ops</strong>: Helps ensure system safety during feature rollouts, with built-in mechanisms for gradual rollouts or emergency kill switches.</p>
</li>
<li><p><strong>Experiment</strong>: Ideal for A/B or multivariate testing. It lets you distribute users across variations and track outcomes to make data-driven decisions.</p>
</li>
<li><p><strong>Permission</strong>: Gating features based on user attributes, such as subscription plans or roles, allowing granular control over feature access.</p>
</li>
</ul>
</li>
<li><p><strong>Variables and Variations</strong><br /> Each feature flag can have associated <strong>variables</strong>, representing configurable values. For example, a flag for a promotion might include variables like <code>discountPercentage</code> or <code>minimumOrderValue</code>.<br /> <strong>Variations</strong> are pre-defined combinations of variable values, which dictate how the feature behaves. For instance:</p>
<ul>
<li><p>Variation A might offer a 10% discount.</p>
</li>
<li><p>Variation B might provide a 15% discount with a higher order value threshold.</p>
</li>
</ul>
</li>
<li><p><strong>Targeting Rules and Rollouts</strong><br /> DevCycle allows you to define rules that control which users see specific variations. These rules can be based on user properties (like location or plan type) or random distribution for A/B testing. Features can also be rolled out gradually, allowing you to monitor performance and ensure stability before scaling.</p>
</li>
</ol>
<p>This structured approach ensures safe experimentation, seamless feature rollouts, and precise customization—all with minimal risk to your users’ experience.</p>
<h3 id="heading-feature-flags-in-brew-haven">Feature Flags in Brew Haven</h3>
<p>Brew Haven uses the following feature flags to make the app dynamic and customizable:</p>
<ol>
<li><p><strong>Coffee Menu</strong>: Feature flags control menu customization, seasonal items, and order personalization.</p>
</li>
<li><p><strong>Payment &amp; Ordering</strong>: Enable or disable online payments, loyalty points, and live order tracking with simple toggles.</p>
</li>
<li><p><strong>A/B Testing Promotions</strong>: Run experiments on discounts and promotional offers to optimize customer engagement.</p>
</li>
</ol>
<p>The first two are release type feature flags, while the last one is an experiment type feature. These flags not only made development faster but also showcased the versatility of DevCycle.</p>
<h2 id="heading-integrating-devcycle">Integrating DevCycle</h2>
<p>The source code of the app is open source, and is available on GitHub. We’ll briefly look at how to integrate and use DevCycle in a React App.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/brew-haven">https://github.com/ra-jeev/brew-haven</a></div>
<p> </p>
<h3 id="heading-client-side-usage-of-devcycle-sdk">Client Side Usage of DevCycle SDK</h3>
<p>After adding the <code>DevCycle React SDK</code> dependency, we first initialize the <code>DevCycleProvider</code> in the <code>App.tsx</code> file as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/App.tsx</span>

<span class="hljs-keyword">import</span> { withDevCycleProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"@devcycle/react-client-sdk"</span>;
<span class="hljs-comment">// ...</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;ThemeProvider defaultTheme=<span class="hljs-string">"system"</span> storageKey=<span class="hljs-string">"coffee-shop-ui-theme"</span>&gt;
      &lt;Router&gt;
        &lt;Layout&gt;
          &lt;Routes&gt;
            &lt;Route path=<span class="hljs-string">"/"</span> element={&lt;Home /&gt;} /&gt;
            &lt;Route path=<span class="hljs-string">"/menu"</span> element={&lt;Menu /&gt;} /&gt;
            &lt;Route path=<span class="hljs-string">"/checkout"</span> element={&lt;Checkout /&gt;} /&gt;
            &lt;Route path=<span class="hljs-string">"/orders"</span> element={&lt;Orders /&gt;} /&gt;
            &lt;Route
              path=<span class="hljs-string">"/admin"</span>
              element={
                &lt;ProtectedRoute&gt;
                  &lt;Admin /&gt;
                &lt;/ProtectedRoute&gt;
              }
            /&gt;
          &lt;/Routes&gt;
        &lt;/Layout&gt;
      &lt;/Router&gt;
      &lt;Toaster /&gt;
    &lt;/ThemeProvider&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> withDevCycleProvider({
  sdkKey: <span class="hljs-keyword">import</span>.meta.env.VITE_DEVCYCLE_CLIENT_SDK_KEY,
  options: {
    logLevel: <span class="hljs-string">"debug"</span>,
  },
})(App);
</code></pre>
<p>And then we create a <code>useFeatureFlags</code> hook to get the various feature flags variables associated with the app as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/hooks/use-feature-flags.ts</span>

<span class="hljs-keyword">import</span> { useVariableValue } <span class="hljs-keyword">from</span> <span class="hljs-string">"@devcycle/react-client-sdk"</span>;
<span class="hljs-keyword">import</span> { featureKeys } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/consts"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlags</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> showNutritionInfo = useVariableValue(
    featureKeys.SHOW_NUTRITION_INFO,
    <span class="hljs-literal">false</span>,
  );
  <span class="hljs-keyword">const</span> enableOnlinePayment = useVariableValue(
    featureKeys.ENABLE_ONLINE_PAYMENT,
    <span class="hljs-literal">false</span>,
  );
  <span class="hljs-keyword">const</span> showPromotionalBanner = useVariableValue(
    featureKeys.SHOW_PROMOTIONAL_BANNER,
    <span class="hljs-string">""</span>,
  );

  <span class="hljs-comment">// etc.</span>

  <span class="hljs-keyword">return</span> {
    showNutritionInfo,
    enableOnlinePayment,
    showPromotionalBanner,
    <span class="hljs-comment">// ...</span>
  };
}
</code></pre>
<p>This hook is used in the app to toggle various code sections on/off to change the UI.</p>
<h3 id="heading-interacting-with-devcycle-management-apis">Interacting with DevCycle Management APIs</h3>
<p>For securely calling the Management APIs, we use Netlify serverless functions so that the DevCycle API's <code>client_id</code> and <code>client_secret</code> are not exposed to the client.</p>
<p>Before we can interact with the DevCycle APIs, we need to generate an auth token using the DevCycle APIs client id and secret. The below code shows how to generate the auth token:</p>
<pre><code class="lang-ts"><span class="hljs-comment">// netlify/functions/feature-flags.mts</span>

<span class="hljs-keyword">interface</span> AuthToken {
  access_token: <span class="hljs-built_in">string</span>;
  expires_in: <span class="hljs-built_in">number</span>;
  token_type: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">let</span> tokenCache: { token: <span class="hljs-built_in">string</span>; expiresAt: <span class="hljs-built_in">number</span> } | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAuthToken</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (tokenCache &amp;&amp; tokenCache.expiresAt &gt; <span class="hljs-built_in">Date</span>.now()) {
    <span class="hljs-keyword">return</span> tokenCache.token;
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://auth.devcycle.com/oauth/token"</span>, {
      method: <span class="hljs-string">"POST"</span>,
      headers: {
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/x-www-form-urlencoded"</span>,
      },
      body: <span class="hljs-keyword">new</span> URLSearchParams({
        grant_type: <span class="hljs-string">"client_credentials"</span>,
        audience: <span class="hljs-string">"https://api.devcycle.com/"</span>,
        client_id: process.env.DEVCYCLE_API_CLIENT_ID!,
        client_secret: process.env.DEVCYCLE_API_CLIENT_SECRET!,
      }),
    });

    <span class="hljs-keyword">if</span> (!response.ok) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Auth failed: <span class="hljs-subst">${response.status}</span>`</span>);
    }

    <span class="hljs-keyword">const</span> data: AuthToken = <span class="hljs-keyword">await</span> response.json();

    tokenCache = {
      token: data.access_token,
      expiresAt: <span class="hljs-built_in">Date</span>.now() + data.expires_in * <span class="hljs-number">1000</span> - <span class="hljs-number">5</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>,
    };

    <span class="hljs-keyword">return</span> data.access_token;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to get auth token:"</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
}
</code></pre>
<p>Now we can fetch the available feature flags, and their config (to get the currently active variation) using the below code:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// netlify/functions/feature-flags.mts</span>

<span class="hljs-keyword">interface</span> Distribution {
  _variation: <span class="hljs-built_in">string</span>;
  percentage: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">interface</span> FeatureConfig {
  _feature: <span class="hljs-built_in">string</span>;
  _environment: <span class="hljs-built_in">string</span>;
  status: <span class="hljs-built_in">string</span>;
  targets: {
    _id: <span class="hljs-built_in">string</span>;
    name: <span class="hljs-built_in">string</span>;
    distribution: Distribution[];
    audience: {
      name: <span class="hljs-built_in">string</span>;
      filters: {
        operator: <span class="hljs-string">"and"</span> | <span class="hljs-string">"or"</span>;
        filters: { <span class="hljs-keyword">type</span>: <span class="hljs-built_in">string</span> }[];
      };
    };
  }[];
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFeatures</span>(<span class="hljs-params">
  featuresUrl: <span class="hljs-built_in">string</span>,
  headers: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;,
</span>) </span>{
  <span class="hljs-keyword">const</span> featuresResponse = <span class="hljs-keyword">await</span> fetch(featuresUrl, { headers });

  <span class="hljs-keyword">if</span> (!featuresResponse.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to fetch flags: <span class="hljs-subst">${featuresResponse.status}</span>`</span>);
  }

  <span class="hljs-keyword">const</span> featuresData = <span class="hljs-keyword">await</span> featuresResponse.json();

  <span class="hljs-comment">// Fetch the config for each of the feature flags</span>
  <span class="hljs-comment">// We're only fetching the configs for the development env</span>
  <span class="hljs-keyword">const</span> configPromises = featuresData.map(<span class="hljs-keyword">async</span> (feature: <span class="hljs-built_in">any</span>) =&gt; {
    <span class="hljs-keyword">const</span> configResponse = <span class="hljs-keyword">await</span> fetch(
      <span class="hljs-string">`<span class="hljs-subst">${featuresUrl}</span>/<span class="hljs-subst">${feature._id}</span>/configurations?environment=development`</span>,
      { headers },
    );

    <span class="hljs-keyword">if</span> (!configResponse.ok) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Failed to fetch config for feature <span class="hljs-subst">${feature._id}</span>`</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }

    <span class="hljs-keyword">const</span> configs: FeatureConfig[] = <span class="hljs-keyword">await</span> configResponse.json();

    <span class="hljs-keyword">return</span> {
      ...feature,
      targets: configs[<span class="hljs-number">0</span>].targets,
      status: configs[<span class="hljs-number">0</span>].status,
    };
  });

  <span class="hljs-keyword">const</span> featuresWithConfigs = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(configPromises);
  <span class="hljs-keyword">return</span> featuresWithConfigs.filter(<span class="hljs-function">(<span class="hljs-params">f</span>) =&gt;</span> f !== <span class="hljs-literal">null</span>);
}
</code></pre>
<p>To update the feature flags config (changing the variation, or toggle the flag altogether), we can use the below function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateFeature</span>(<span class="hljs-params">
  featuresUrl: <span class="hljs-built_in">string</span>,
  featureId: <span class="hljs-built_in">string</span>,
  headers: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">string</span>&gt;,
  update: <span class="hljs-built_in">any</span>,
</span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
    <span class="hljs-string">`<span class="hljs-subst">${featuresUrl}</span>/<span class="hljs-subst">${featureId}</span>/configurations?environment=development`</span>,
    {
      method: <span class="hljs-string">"PATCH"</span>,
      headers,
      body: <span class="hljs-built_in">JSON</span>.stringify(update),
    },
  );

  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to update feature: <span class="hljs-subst">${response.status}</span>`</span>);
  }

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> response.json();
}
</code></pre>
<p>Finally, here is the Netlify function that uses the above functions to serve the client requests:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> (req: Request) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { method } = req;
    <span class="hljs-keyword">const</span> token = <span class="hljs-keyword">await</span> getAuthToken();
    <span class="hljs-keyword">const</span> featuresBaseUrl = <span class="hljs-string">`https://api.devcycle.com/v1/projects/<span class="hljs-subst">${process.env.DEVCYCLE_PROJECT_ID}</span>/features`</span>;
    <span class="hljs-keyword">const</span> headers = {
      Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${token}</span>`</span>,
    };

    <span class="hljs-keyword">if</span> (method === <span class="hljs-string">"GET"</span>) {
      <span class="hljs-keyword">const</span> features = <span class="hljs-keyword">await</span> getFeatures(featuresBaseUrl, headers);

      <span class="hljs-keyword">return</span> Response.json({ features });
    }

    <span class="hljs-keyword">if</span> (method === <span class="hljs-string">"PATCH"</span>) {
      <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> req.json();
      <span class="hljs-keyword">const</span> { featureId, update } = body;

      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> updateFeature(
        featuresBaseUrl,
        featureId,
        {
          ...headers,
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        },
        update,
      );

      <span class="hljs-keyword">return</span> Response.json({ data });
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: <span class="hljs-string">"Method not allowed"</span> }), {
      status: <span class="hljs-number">405</span>,
      headers: {
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      },
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(
      <span class="hljs-built_in">JSON</span>.stringify({
        error: error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? error.message : <span class="hljs-string">"Internal server error"</span>,
      }),
      {
        status: <span class="hljs-number">500</span>,
        headers: {
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        },
      },
    );
  }
};
</code></pre>
<p>This Netlify function is called by the client in the following way:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/lib/api.ts</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">getFeatureFlags</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"/.netlify/functions/feature-flags"</span>);
    <span class="hljs-keyword">if</span> (!response.ok) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Failed to fetch flags"</span>);
    }

    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();
    <span class="hljs-keyword">return</span> { success: <span class="hljs-literal">true</span>, data: data.features <span class="hljs-keyword">as</span> Feature[] };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> {
      success: <span class="hljs-literal">false</span>,
      error: error <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? error.message : <span class="hljs-string">"Unknown error"</span>,
    };
  }
}

<span class="hljs-comment">// and, so on...</span>
</code></pre>
<p>The above code snippets capture how the <code>DevCycle SDK</code> and its <code>Management APIs</code> are used within the app. You can go through the shared source code to view the complete implementation in more detail.</p>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>Brew Haven isn’t just a coffee shop app — it’s a showcase of how feature flags can make your projects more dynamic, responsive, and fun.</p>
<p>Feature flags allow you to:</p>
<ul>
<li><p>Do gradual rollouts.</p>
</li>
<li><p>Reduce deployment risks.</p>
</li>
<li><p>Enable rapid experimentation.</p>
</li>
<li><p>Personalize user experiences.</p>
</li>
</ul>
<p>Next time you’re sipping coffee and dreaming big, think about how feature flags can brew innovation into your projects. ☕</p>
<hr />
<p>Thank you for sticking with me until the end! I hope you’ve picked up some new concepts along the way. I’d love to hear what you learned or any thoughts you have in the comments section. Your feedback is not only valuable to me, but to the entire developer community exploring this exciting field.</p>
<p>Until next time.</p>
<blockquote>
<p><em>Keep adding the bits, and soon you'll have a lot of bytes to share with the world.</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Building Vhisper: Voice Notes App with AI Transcription and Post-Processing]]></title><description><![CDATA[After wrapping up my last project—a chat interface to search GitHub—I found myself searching for the next idea to tackle. As a developer, inspiration often comes unexpectedly, and this time, it struck while scrolling through my GitHub feed. A repo, s...]]></description><link>https://rajeev.dev/building-voice-notes-app-with-ai-transcription-and-post-processing</link><guid isPermaLink="true">https://rajeev.dev/building-voice-notes-app-with-ai-transcription-and-post-processing</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[nuxthub]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[AI]]></category><category><![CDATA[nuxtui]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Thu, 28 Nov 2024 19:28:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1733124620082/9f989586-15d7-4cba-862d-edc11882aee4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After wrapping up my last project—a <a target="_blank" href="https://rajeev.dev/building-a-chat-interface-to-search-github">chat interface to search GitHub</a>—I found myself searching for the next idea to tackle. As a developer, inspiration often comes unexpectedly, and this time, it struck while scrolling through my GitHub feed. A repo, starred by Daniel Roe (Nuxt Core Team Lead), caught my eye. It was an <a target="_blank" href="https://github.com/egoist/whispo">Electron-based voice notes app</a> designed for macOS.</p>
<p>Something about the simplicity of voice notes combined with the technical challenge intrigued me. Could I take this concept further? Could I build a modern, AI-powered voice notes app using web technologies? That urge to build led me here, to this blog post, where I’ll walk you through building <strong>Vhisper</strong>, a voice notes app with AI transcription and post-processing, built with the Nuxt ecosystem and powered by Cloudflare.</p>
<p>And before you say it, I must make a confession: <em>“Hi! My name is Rajeev, and I am addicted to talking/chatting.”.</em></p>
<p><img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNDJzcGdtYWJkeHhobDRzYzJ3MzhmZG8zNHczcThobmcxZnVrd3EyZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/C4CI97S8sKstW/giphy.gif" alt="A bear confessing to its addiction" class="image--center mx-auto" /></p>
<h2 id="heading-project-overview">Project Overview</h2>
<p>Now that the formalities are done, let’s focus on what we’ll be building in this project. The goal is to create <strong>Vhisper</strong>, a web-based voice notes application with the following core features:</p>
<ul>
<li><p><strong>Recording Voice Notes</strong>: Users can record voice notes directly in the browser.</p>
</li>
<li><p><strong>AI-Powered Transcription</strong>: Each recording is processed via Cloudflare Workers AI, converting speech to text.</p>
</li>
<li><p><strong>Post-Processing with Custom Prompts</strong>: Users can customize how transcriptions are refined using an AI-driven post-processing step.</p>
</li>
<li><p><strong>Seamless Data Management (CRUD)</strong>: Notes and audio files are efficiently stored using Cloudflare’s D1 database and R2 storage.</p>
</li>
</ul>
<p>To give you a better sense of what we’re aiming for, here’s a quick demo showcasing Vhisper’s main features:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/7IcwTIrHIPg">https://youtu.be/7IcwTIrHIPg</a></div>
<p> </p>
<p>You can experience it live here: <a target="_blank" href="https://vhisper.nuxt.dev">https://vhisper.nuxt.dev</a></p>
<p>By the end of this guide, you’ll know exactly how to build and deploy this voice notes app using <strong>Nuxt</strong>, <strong>NuxtHub</strong> and <strong>Cloudflare services</strong>—a stack that combines innovation with developer-first simplicity. Ready to build it? Let’s get started!</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Before setting up the project let’s review the technologies used to build this app:</p>
<ol>
<li><p><a target="_blank" href="https://nuxt.com">Nuxt</a>: Vue.js framework for the application foundation</p>
</li>
<li><p><a target="_blank" href="https://ui3.nuxt.com">Nuxt UI (v3)</a>: For creating a polished and professional frontend</p>
</li>
<li><p><a target="_blank" href="https://orm.drizzle.team">Drizzle</a>: Database ORM</p>
</li>
<li><p><a target="_blank" href="https://zod.dev">Zod</a>: For client/server side data validation</p>
</li>
<li><p><a target="_blank" href="https://hub.nuxt.com">NuxtHub</a>: Backend (<code>database</code>, <code>storage</code>, <code>AI</code> etc.), deployment and administration platform for Nuxt</p>
</li>
<li><p><a target="_blank" href="https://developers.cloudflare.com">Cloudflare</a>: Powers NuxtHub to provide various services</p>
</li>
</ol>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To follow along, apart from basic necessities like Node.js, npm, and some Nuxt knowledge, you’ll need:</p>
<ol>
<li><p>A <strong>Cloudflare account</strong> to use Workers AI and deploy your project. If you don’t have one, you can set it up <a target="_blank" href="https://www.cloudflare.com/"><strong>here</strong></a>.</p>
</li>
<li><p>A <strong>NuxtHub Admin Account</strong> for managing apps via the NuxtHub dashboard. Sign up <a target="_blank" href="https://admin.hub.nuxt.com/"><strong>here</strong></a>.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">ℹ</div>
<div data-node-type="callout-text"><strong>Note:</strong> Workers AI models will run in your Cloudflare account even during local development. Check out their <a target="_self" href="https://developers.cloudflare.com/workers-ai/platform/pricing">pricing and free quota</a>.</div>
</div>

<h3 id="heading-project-init">Project <strong>Init</strong></h3>
<p>We’ll start with the NuxtHub starter template. Run the following command to create and navigate to your new project directory:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create project and change into the project dir</span>
npx nuxthub init voice-notes &amp;&amp; <span class="hljs-built_in">cd</span> <span class="hljs-variable">$_</span>
</code></pre>
<p>If you plan to use <strong>pnpm</strong> as your package manager, add a <code>.npmrc</code> file at the root of your project with this line to hoist dependencies:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># .npmrc</span>
shamefully-hoist=<span class="hljs-literal">true</span>
</code></pre>
<p>Now, install the dependencies:</p>
<ol>
<li><p>Nuxt modules:</p>
<pre><code class="lang-bash"> pnpm add @nuxt/ui@next
</code></pre>
</li>
<li><p>Drizzle and related tools:</p>
<pre><code class="lang-bash"> pnpm add drizzle-orm drizzle-zod @vueuse/core
</code></pre>
</li>
<li><p>Icon packs:</p>
<pre><code class="lang-bash"> pnpm add @iconify-json/lucide @iconify-json/simple-icons
</code></pre>
</li>
<li><p>Dev dependencies:</p>
<pre><code class="lang-bash"> pnpm add -D drizzle-kit
</code></pre>
</li>
</ol>
<p>Update your <code>nuxt.config.ts</code> file as follows:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  modules: [<span class="hljs-string">"@nuxthub/core"</span>, <span class="hljs-string">"@nuxt/eslint"</span>, <span class="hljs-string">"nuxt-auth-utils"</span>, <span class="hljs-string">"@nuxt/ui"</span>],

  devtools: { enabled: <span class="hljs-literal">true</span> },

  runtimeConfig: {
    <span class="hljs-keyword">public</span>: {
      helloText: <span class="hljs-string">"Hello from the Edge 👋"</span>,
    },
  },

  future: { compatibilityVersion: <span class="hljs-number">4</span> },
  compatibilityDate: <span class="hljs-string">"2024-07-30"</span>,

  hub: {
    ai: <span class="hljs-literal">true</span>,
    blob: <span class="hljs-literal">true</span>,
    database: <span class="hljs-literal">true</span>,
  },

  css: [<span class="hljs-string">"~/assets/css/main.css"</span>],

  eslint: {
    config: {
      stylistic: <span class="hljs-literal">false</span>,
    },
  },
});
</code></pre>
<p>We’ve made the following changes to the Nuxt config file:</p>
<ol>
<li><p>Updated the Nuxt modules used in the app</p>
</li>
<li><p>Enabled required NuxtHub features</p>
</li>
<li><p>And, added the <code>main.css</code> file path.</p>
</li>
</ol>
<p>Create the <code>main.css</code> file in the <code>app/assets/css</code> folder with this content:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"@nuxt/ui"</span>;
</code></pre>
<h3 id="heading-testing-the-setup">Testing the Setup</h3>
<p>Run the development server:</p>
<pre><code class="lang-bash">pnpm dev
</code></pre>
<p>Visit <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a> in your browser. If everything is set up correctly, you’ll see the message: <em>“Hello from the Edge 👋”</em> with a refresh button.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>Troubleshooting Tip:</strong> If you encounter issues with TailwindCSS, try deleting <code>node_modules</code> and <code>pnpm-lock.yaml</code>, and then run <code>pnpm install</code> to re-install the dependecies.</div>
</div>

<h2 id="heading-building-the-basic-backend">Building the Basic Backend</h2>
<p>With the project setup complete, let’s dive into building the backend. We’ll begin by creating API endpoints to handle core functionalities, followed by configuring the database and integrating validation.</p>
<p>But before jumping to code, let’s understand how you’ll interact with various Cloudflare offerings. If you’ve been attentive, you should know the answer, NuxrHub, but what is NuxtHub?</p>
<h3 id="heading-what-is-nuxthub">What is NuxtHub?</h3>
<p>NuxtHub is a developer-friendly interface built on top of Cloudflare’s robust services. It simplifies the process of creating, binding, and managing services for your project, offering a seamless development experience (DX).</p>
<p>You started with a NuxtHub template, so the project comes preconfigured with the <code>@nuxthub/core</code> module. During the setup, you also enabled the required Cloudflare services: AI, Database, and Blob. The NuxtHub core module exposes these services through interfaces prefixed with <code>hub</code>. For example, <code>hubAI</code> is used for AI features, <code>hubBlob</code> for object storage, and so on.</p>
<p>Time is ripe now to work on the first API endpoint.</p>
<h3 id="heading-apitranscribe-endpoint"><code>/api/transcribe</code> Endpoint</h3>
<p>Create a new file named <code>transcribe.post.ts</code> inside the <code>server/api</code> directory, and add the following code to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/api/transcribe.post.ts </span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> form = <span class="hljs-keyword">await</span> readFormData(event);
  <span class="hljs-keyword">const</span> blob = form.get(<span class="hljs-string">"audio"</span>) <span class="hljs-keyword">as</span> Blob;
  <span class="hljs-keyword">if</span> (!blob) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">400</span>,
      message: <span class="hljs-string">"Missing audio blob to transcribe"</span>,
    });
  }

  ensureBlob(blob, { maxSize: <span class="hljs-string">"8MB"</span>, types: [<span class="hljs-string">"audio"</span>] });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> hubAI().run(<span class="hljs-string">"@cf/openai/whisper"</span>, {
      audio: [...new <span class="hljs-built_in">Uint8Array</span>(<span class="hljs-keyword">await</span> blob.arrayBuffer())],
    });

    <span class="hljs-keyword">return</span> response.text;
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error transcribing audio:"</span>, err);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      message: <span class="hljs-string">"Failed to transcribe audio. Please try again."</span>,
    });
  }
});
</code></pre>
<p>The above code does the following:</p>
<ol>
<li><p>Parses incoming form data to extract the audio as a <code>Blob</code></p>
</li>
<li><p>Verifies that it’s an audio blob and is less than <code>8MB</code> in size using a <code>@nuxthub/core</code> utility function <code>ensureBlob</code></p>
</li>
<li><p>Passes on the array buffer to the <code>Whisper</code> model through <code>hubAI</code> for transcription</p>
</li>
<li><p>Returns the transcribed text to the client</p>
</li>
</ol>
<p>Before you can use Workers AI in development, you’ll need to link it to your Cloudflare project. As we’re using NuxtHub as the interface, running the following command will create/link a new or existing NuxtHub project with this project.</p>
<pre><code class="lang-bash">npx nuxthub link
</code></pre>
<h3 id="heading-apiupload-endpoint"><code>/api/upload</code> Endpoint</h3>
<p>Next, create an endpoint to upload the audio recordings to the R2 storage. Create a new file <code>upload.put.ts</code> in your <code>/server/api</code> folder and add the following code to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/api/upload.put.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">return</span> hubBlob().handleUpload(event, {
    formKey: <span class="hljs-string">"files"</span>,
    multiple: <span class="hljs-literal">true</span>,
    ensure: {
      maxSize: <span class="hljs-string">"8MB"</span>,
      types: [<span class="hljs-string">"audio"</span>],
    },
    put: {
      addRandomSuffix: <span class="hljs-literal">true</span>,
      prefix: <span class="hljs-string">"recordings"</span>,
    },
  });
});
</code></pre>
<p>The above code uses another utility method from the NuxtHub core module to upload the incoming audio files to R2. <code>handleUpload</code> does the following:</p>
<ol>
<li><p>Looks for the <code>files</code> key in the incoming form data to extract blob data</p>
</li>
<li><p>Supports multiple files per event</p>
</li>
<li><p>Ensures that the files are audio and under <code>8MB</code> in size</p>
</li>
<li><p>And, finally uploads them to your R2 bucket inside <code>recordings</code> folder while also adding a random suffix to the final names</p>
</li>
<li><p>Returns a promise to the client that resolves once all the files are uploaded</p>
</li>
</ol>
<p>Now we just need <code>/notes</code> endpoints to create &amp; fetch notes entries before the basic backend is done. But to do that we need to create the needed tables. Let’s tackle this in next section.</p>
<h3 id="heading-defining-the-notes-table-schema">Defining the <code>notes</code> Table Schema</h3>
<p>As we will use <code>drizzle</code> to manage and interact with the database, we need to configure it first. Create a new file <code>drizzle.config.ts</code> in the project root, and add the following to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// drizzle.config.ts</span>
<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'drizzle-kit'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  dialect: <span class="hljs-string">'sqlite'</span>,
  schema: <span class="hljs-string">'./server/database/schema.ts'</span>,
  out: <span class="hljs-string">'./server/database/migrations'</span>,
});
</code></pre>
<p>The config above mentions where the database schema is located, and where should the database migrations be generated. The database dialect is set to <code>sqlite</code> as that is what Cloudflare’s D1 database supports.</p>
<p>Next, create a new file <code>schema.ts</code> in the <code>server/database</code> folder, and add the following to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/database/schema.ts</span>
<span class="hljs-keyword">import</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">"node:crypto"</span>;
<span class="hljs-keyword">import</span> { sql } <span class="hljs-keyword">from</span> <span class="hljs-string">"drizzle-orm"</span>;
<span class="hljs-keyword">import</span> { sqliteTable, text } <span class="hljs-keyword">from</span> <span class="hljs-string">"drizzle-orm/sqlite-core"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> notes = sqliteTable(<span class="hljs-string">"notes"</span>, {
  id: text(<span class="hljs-string">"id"</span>)
    .primaryKey()
    .$defaultFn(<span class="hljs-function">() =&gt;</span> <span class="hljs-string">"nt_"</span> + crypto.randomBytes(<span class="hljs-number">12</span>).toString(<span class="hljs-string">"hex"</span>)),
  text: text(<span class="hljs-string">"text"</span>).notNull(),
  createdAt: text(<span class="hljs-string">"created_at"</span>)
    .notNull()
    .default(sql<span class="hljs-string">`(CURRENT_TIMESTAMP)`</span>),
  updatedAt: text(<span class="hljs-string">"updated_at"</span>)
    .notNull()
    .default(sql<span class="hljs-string">`(CURRENT_TIMESTAMP)`</span>)
    .$onUpdate(<span class="hljs-function">() =&gt;</span> sql<span class="hljs-string">`(CURRENT_TIMESTAMP)`</span>),
  audioUrls: text(<span class="hljs-string">"audio_urls"</span>, { mode: <span class="hljs-string">"json"</span> }).$type&lt;<span class="hljs-built_in">string</span>[]&gt;(),
});
</code></pre>
<p>The <code>notes</code> table schema is straightforward. It includes the note text and optional audio recording URLs stored as a JSON string array.</p>
<p>Finally, create a new file <code>drizzle.ts</code> in the <code>server/utils</code> folder, and add the following to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/utils/drizzle.ts</span>
<span class="hljs-keyword">import</span> { drizzle } <span class="hljs-keyword">from</span> <span class="hljs-string">"drizzle-orm/d1"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> schema <span class="hljs-keyword">from</span> <span class="hljs-string">"../database/schema"</span>;

<span class="hljs-keyword">export</span> { sql, eq, and, or, desc } <span class="hljs-keyword">from</span> <span class="hljs-string">"drizzle-orm"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> tables = schema;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDrizzle</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> drizzle(hubDatabase(), { schema });
}
</code></pre>
<p>Here we hook up <code>hubDatabase</code> with the tables schema through <code>drizzle</code> and export the server composable <code>useDrizzle</code> along with the needed operators.</p>
<p>Now we are ready to create the <code>/api/notes</code> endpoints which we will be doing in the next section.</p>
<h3 id="heading-apinotes-endpoints"><code>/api/notes</code> Endpoints</h3>
<p>Create two new files <code>index.post.ts</code> and <code>index.get.ts</code> in the <code>server/api/notes</code> folder and add the respective codes to them as shown below.</p>
<p><strong>index.post.ts</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/api/notes/index.post.ts</span>
<span class="hljs-keyword">import</span> { noteSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">"#shared/schemas/note.schema"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> { user } = <span class="hljs-keyword">await</span> requireUserSession(event);

  <span class="hljs-keyword">const</span> { text, audioUrls } = <span class="hljs-keyword">await</span> readValidatedBody(event, noteSchema.parse);

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> useDrizzle()
      .insert(tables.notes)
      .values({
        text,
        audioUrls: audioUrls ? audioUrls.map(<span class="hljs-function">(<span class="hljs-params">url</span>) =&gt;</span> <span class="hljs-string">`/audio/<span class="hljs-subst">${url}</span>`</span>) : <span class="hljs-literal">null</span>,
      });

    <span class="hljs-keyword">return</span> setResponseStatus(event, <span class="hljs-number">201</span>);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error creating note:"</span>, err);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      message: <span class="hljs-string">"Failed to create note. Please try again."</span>,
    });
  }
});
</code></pre>
<p>The above code reads the validated event body, and creates a new note entry in the database using the drizzle composable we created earlier. We will get to the validation part in a bit.</p>
<p><strong>index.get.ts</strong></p>
<pre><code class="lang-typescript"><span class="hljs-comment">// server/api/notes/index.get.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> notes = <span class="hljs-keyword">await</span> useDrizzle()
      .select()
      .from(tables.notes)
      .orderBy(desc(tables.notes.updatedAt));

    <span class="hljs-keyword">return</span> notes;
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error retrieving note:"</span>, err);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      message: <span class="hljs-string">"Failed to get notes. Please try again."</span>,
    });
  }
});
</code></pre>
<p>Here we fetch the notes entries from the table in descending order of <code>updatedAt</code> field.</p>
<p><strong>Incoming data validation</strong></p>
<p>As mentioned in the beginning, we’ll use <code>Zod</code> for data validation. Here is the relevant code from <code>index.post.ts</code> that validates the incoming client data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { text, audioUrls } = <span class="hljs-keyword">await</span> readValidatedBody(event, noteSchema.parse);
</code></pre>
<p>Create a new file <code>note.schema.ts</code> in the <code>shared/schemas</code> folder in the project root directory with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// shared/schemas/note.schema.ts</span>
<span class="hljs-keyword">import</span> { createInsertSchema, createSelectSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">"drizzle-zod"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> { notes } <span class="hljs-keyword">from</span> <span class="hljs-string">"~~/server/database/schema"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> noteSchema = createInsertSchema(notes, {
  text: <span class="hljs-function">(<span class="hljs-params">schema</span>) =&gt;</span>
    schema.text
      .min(<span class="hljs-number">3</span>, <span class="hljs-string">"Note must be at least 3 characters long"</span>)
      .max(<span class="hljs-number">5000</span>, <span class="hljs-string">"Note cannot exceed 5000 characters"</span>),
  audioUrls: z.string().array().optional(),
}).pick({
  text: <span class="hljs-literal">true</span>,
  audioUrls: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> noteSelectSchema = createSelectSchema(notes, {
  audioUrls: z.string().array().optional(),
});
</code></pre>
<p>The above code uses the <code>drizzle-zod</code> plugin to create the <code>zod</code> schema needed for validation (The above validation error messages are more suitable for the client side. Feel free to adapt these validation rules to suit your specific project requirements.).</p>
<h3 id="heading-creating-db-migrations">Creating DB Migrations</h3>
<p>With the table schema and API endpoints defined, the final step is to create and apply database migrations to bring everything together. Add the following command to your <code>package.json</code>'s scripts:</p>
<pre><code class="lang-json"><span class="hljs-comment">// ..</span>
<span class="hljs-string">"scripts"</span>: {
  <span class="hljs-comment">// ..</span>
  <span class="hljs-attr">"db:generate"</span>: <span class="hljs-string">"drizzle-kit generate"</span>
}
<span class="hljs-comment">// ..</span>
</code></pre>
<p>Next, run <code>pnpm run db:generate</code> to create the database migrations. These migrations are auto applied by NuxtHub when you run or deploy your project. You can test it by running <code>pnpm dev</code> and checking the Nuxt Dev Tools as shown below (this is a local sqlite database that is used in the dev mode).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732723442066/b7106851-cba6-4dd0-b3a8-758f6746ef37.png" alt="Nuxt Dev Tools showing empty notes table" class="image--center mx-auto" /></p>
<p>We are done with the basic backend of the project. In the next section, we will code the frontend components and pages to complete the whole thing,</p>
<h2 id="heading-creating-the-basic-frontend">Creating the Basic Frontend</h2>
<p>We’ll start with the most important feature first: recording the user voice, and then we’ll move on to creating the needed components and pages.</p>
<h3 id="heading-usemediarecorder-composable"><code>useMediaRecorder</code> Composable</h3>
<p>Let’s create a composable to handle the media recording functionality. Create a new file <code>useMediaRecorder.ts</code> in your <code>app/composables</code> folder and add the following code to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/composables/useMediaRecorder.ts</span>
<span class="hljs-keyword">interface</span> MediaRecorderState {
  isRecording: <span class="hljs-built_in">boolean</span>;
  recordingDuration: <span class="hljs-built_in">number</span>;
  audioData: <span class="hljs-built_in">Uint8Array</span> | <span class="hljs-literal">null</span>;
  updateTrigger: <span class="hljs-built_in">number</span>;
}

<span class="hljs-keyword">const</span> getSupportedMimeType = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> types = [
    <span class="hljs-string">"audio/mp4"</span>,
    <span class="hljs-string">"audio/mp4;codecs=mp4a"</span>,
    <span class="hljs-string">"audio/mpeg"</span>,
    <span class="hljs-string">"audio/webm;codecs=opus"</span>,
    <span class="hljs-string">"audio/webm"</span>,
  ];

  <span class="hljs-keyword">return</span> (
    types.find(<span class="hljs-function">(<span class="hljs-params"><span class="hljs-keyword">type</span></span>) =&gt;</span> MediaRecorder.isTypeSupported(<span class="hljs-keyword">type</span>)) || <span class="hljs-string">"audio/webm"</span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useMediaRecorder</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> state = ref&lt;MediaRecorderState&gt;({
    isRecording: <span class="hljs-literal">false</span>,
    recordingDuration: <span class="hljs-number">0</span>,
    audioData: <span class="hljs-literal">null</span>,
    updateTrigger: <span class="hljs-number">0</span>,
  });

  <span class="hljs-keyword">let</span> mediaRecorder: MediaRecorder | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> audioContext: AudioContext | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> analyser: AnalyserNode | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> animationFrame: <span class="hljs-built_in">number</span> | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">let</span> audioChunks: Blob[] | <span class="hljs-literal">undefined</span> = <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">const</span> updateAudioData = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (!analyser || !state.value.isRecording || !state.value.audioData) {
      <span class="hljs-keyword">if</span> (animationFrame) {
        cancelAnimationFrame(animationFrame);
        animationFrame = <span class="hljs-literal">null</span>;
      }

      <span class="hljs-keyword">return</span>;
    }

    analyser.getByteTimeDomainData(state.value.audioData);
    state.value.updateTrigger += <span class="hljs-number">1</span>;
    animationFrame = requestAnimationFrame(updateAudioData);
  };

  <span class="hljs-keyword">const</span> startRecording = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">await</span> navigator.mediaDevices.getUserMedia({ audio: <span class="hljs-literal">true</span> });

      audioContext = <span class="hljs-keyword">new</span> AudioContext();
      analyser = audioContext.createAnalyser();

      <span class="hljs-keyword">const</span> source = audioContext.createMediaStreamSource(stream);
      source.connect(analyser);

      <span class="hljs-keyword">const</span> options = {
        mimeType: getSupportedMimeType(),
        audioBitsPerSecond: <span class="hljs-number">64000</span>,
      };

      mediaRecorder = <span class="hljs-keyword">new</span> MediaRecorder(stream, options);
      audioChunks = [];

      mediaRecorder.ondataavailable = <span class="hljs-function">(<span class="hljs-params">e: BlobEvent</span>) =&gt;</span> {
        audioChunks?.push(e.data);
        state.value.recordingDuration += <span class="hljs-number">1</span>;
      };

      state.value.audioData = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Uint8Array</span>(analyser.frequencyBinCount);
      state.value.isRecording = <span class="hljs-literal">true</span>;
      state.value.recordingDuration = <span class="hljs-number">0</span>;
      state.value.updateTrigger = <span class="hljs-number">0</span>;
      mediaRecorder.start(<span class="hljs-number">1000</span>);

      updateAudioData();
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing microphone:"</span>, err);
      <span class="hljs-keyword">throw</span> err;
    }
  };

  <span class="hljs-keyword">const</span> stopRecording = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>&lt;Blob&gt;(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (mediaRecorder &amp;&amp; state.value.isRecording) {
        <span class="hljs-keyword">const</span> mimeType = mediaRecorder.mimeType;
        mediaRecorder.onstop = <span class="hljs-function">() =&gt;</span> {
          <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">new</span> Blob(audioChunks, { <span class="hljs-keyword">type</span>: mimeType });
          audioChunks = <span class="hljs-literal">undefined</span>;

          state.value.recordingDuration = <span class="hljs-number">0</span>;
          state.value.updateTrigger = <span class="hljs-number">0</span>;
          state.value.audioData = <span class="hljs-literal">null</span>;

          resolve(blob);
        };

        state.value.isRecording = <span class="hljs-literal">false</span>;
        mediaRecorder.stop();
        mediaRecorder.stream.getTracks().forEach(<span class="hljs-function">(<span class="hljs-params">track</span>) =&gt;</span> track.stop());

        <span class="hljs-keyword">if</span> (animationFrame) {
          cancelAnimationFrame(animationFrame);
          animationFrame = <span class="hljs-literal">null</span>;
        }

        audioContext?.close();
        audioContext = <span class="hljs-literal">null</span>;
      }
    });
  };

  onUnmounted(<span class="hljs-function">() =&gt;</span> {
    stopRecording();
  });

  <span class="hljs-keyword">return</span> {
    state: <span class="hljs-keyword">readonly</span>(state),
    startRecording,
    stopRecording,
  };
}
</code></pre>
<p>The above code does the following:</p>
<ol>
<li><p>Exposes recording start/stop functionality along with the current recording readonly state</p>
</li>
<li><p>Captures user’s voice using the <code>MediaRecorder</code> API when <code>startRecording</code> function is invoked. The <code>MediaRecorder</code> API is a simple and efficient way to handle media capture in modern browsers, making it ideal for our use case.</p>
</li>
<li><p>Captures audio visualization data using <code>AudioContext</code> and <code>AnalyserNode</code> and updates it in real-time using animation frames</p>
</li>
<li><p>Cleans up resources and returns the captured audio as a <code>Blob</code> when <code>stopRecording</code> is called or if the component unmounts</p>
</li>
</ol>
<h3 id="heading-noteeditormodal-component"><code>NoteEditorModal</code> Component</h3>
<p>Next, create a new file <code>NoteEditorModal.vue</code> in the <code>app/components</code> folder and add the following code to it:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- app/components/NoteEditorModal.vue --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UModal</span>
    <span class="hljs-attr">fullscreen</span>
    <span class="hljs-attr">:close</span>=<span class="hljs-string">"{
      disabled: isSaving || noteRecorder?.isBusy,
    }"</span>
    <span class="hljs-attr">:prevent-close</span>=<span class="hljs-string">"isSaving || noteRecorder?.isBusy"</span>
    <span class="hljs-attr">title</span>=<span class="hljs-string">"Create Note"</span>
    <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{
      body: 'flex-1 w-full max-w-7xl mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 overflow-hidden',
    }"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">body</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 flex flex-col"</span> <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ body: 'flex-1' }"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">header</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-8 font-medium text-gray-600 dark:text-gray-300"</span>&gt;</span>
            Note transcript
          <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">UTextarea</span>
          <span class="hljs-attr">v-model</span>=<span class="hljs-string">"noteText"</span>
          <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Type your note here, or use voice recording..."</span>
          <span class="hljs-attr">size</span>=<span class="hljs-string">"lg"</span>
          <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"isSaving || noteRecorder?.isBusy"</span>
          <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ root: 'w-full h-full', base: ['h-full resize-none'] }"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">NoteRecorder</span>
        <span class="hljs-attr">ref</span>=<span class="hljs-string">"recorder"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"md:h-full md:flex md:flex-col md:w-96 shrink-0 order-first md:order-none"</span>
        @<span class="hljs-attr">transcription</span>=<span class="hljs-string">"handleTranscription"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">footer</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
        <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-lucide-undo-2"</span>
        <span class="hljs-attr">color</span>=<span class="hljs-string">"neutral"</span>
        <span class="hljs-attr">variant</span>=<span class="hljs-string">"outline"</span>
        <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"isSaving"</span>
        @<span class="hljs-attr">click</span>=<span class="hljs-string">"resetNote"</span>
      &gt;</span>
        Reset
      <span class="hljs-tag">&lt;/<span class="hljs-name">UButton</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
        <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-lucide-cloud-upload"</span>
        <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"!noteText.trim() || noteRecorder?.isBusy || isSaving"</span>
        <span class="hljs-attr">:loading</span>=<span class="hljs-string">"isSaving"</span>
        @<span class="hljs-attr">click</span>=<span class="hljs-string">"saveNote"</span>
      &gt;</span>
        Save Note
      <span class="hljs-tag">&lt;/<span class="hljs-name">UButton</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UModal</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { NoteRecorder } <span class="hljs-keyword">from</span> <span class="hljs-string">"#components"</span>;

<span class="hljs-keyword">const</span> props = defineProps&lt;{ <span class="hljs-attr">onNewNote</span>: <span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">void</span> }&gt;();

type NoteRecorderType = InstanceType&lt;<span class="hljs-keyword">typeof</span> NoteRecorder&gt;;
<span class="hljs-keyword">const</span> noteRecorder = useTemplateRef&lt;NoteRecorderType&gt;(<span class="hljs-string">"recorder"</span>);
<span class="hljs-keyword">const</span> resetNote = <span class="hljs-function">() =&gt;</span> {
  noteText.value = <span class="hljs-string">""</span>;
  noteRecorder.value?.resetRecordings();
};

<span class="hljs-keyword">const</span> noteText = ref(<span class="hljs-string">""</span>);
<span class="hljs-keyword">const</span> handleTranscription = <span class="hljs-function">(<span class="hljs-params">text: string</span>) =&gt;</span> {
  noteText.value += noteText.value ? <span class="hljs-string">"\n\n"</span> : <span class="hljs-string">""</span>;
  noteText.value += text ?? <span class="hljs-string">""</span>;
};

<span class="hljs-keyword">const</span> modal = useModal();
<span class="hljs-keyword">const</span> isSaving = ref(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> saveNote = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = noteText.value.trim();
  <span class="hljs-keyword">if</span> (!text) <span class="hljs-keyword">return</span>;

  isSaving.value = <span class="hljs-literal">true</span>;

  <span class="hljs-keyword">const</span> audioUrls = <span class="hljs-keyword">await</span> noteRecorder.value?.uploadRecordings();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> $fetch(<span class="hljs-string">"/api/notes"</span>, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">body</span>: { text, audioUrls },
    });

    useToast().add({
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Note Saved"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Your note was saved successfully."</span>,
      <span class="hljs-attr">color</span>: <span class="hljs-string">"success"</span>,
    });

    <span class="hljs-keyword">if</span> (props.onNewNote) {
      props.onNewNote();
    }

    modal.close();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error saving note:"</span>, err);
    useToast().add({
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Save Failed"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Failed to save the note."</span>,
      <span class="hljs-attr">color</span>: <span class="hljs-string">"error"</span>,
    });
  }

  isSaving.value = <span class="hljs-literal">false</span>;
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>The above modal component does the following:</p>
<ol>
<li><p>Displays a <code>textarea</code> for allowing a manual note entry</p>
</li>
<li><p>The modal integrates the <code>NoteRecorder</code> component for voice recordings and manages the data flow between the recordings and the <code>textarea</code> for user notes.</p>
</li>
<li><p>Whenever a new recording is created, it captures the emitted event from the note recorder component, and appends the transcription text to the <code>textarea</code> content</p>
</li>
<li><p>When the user clicks the save note button, its first uploads all recordings (if any) by calling the note recorder’s <code>uploadRecordings</code> method, and then save the note by calling the <code>notes</code> API endpoint created earlier.</p>
</li>
<li><p>The save note button first uploads all recordings (if any) asynchronously by calling the <code>uploadRecordings</code> method, then sends the note data to the <code>/api/notes</code> endpoint. Upon success, it notifies the parent by executing the callback passed by it, and then closes the modal.</p>
</li>
</ol>
<h3 id="heading-noterecorder-component"><code>NoteRecorder</code> Component</h3>
<p>Create a new file <code>NoteRecorder.vue</code> in the <code>app/components</code> folder and add the following content to it:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- app/components/NoteRecorder.vue --&gt;</span> 
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span>
    <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{
      body: 'max-h-36 md:max-h-none md:flex-1 overflow-y-auto',
    }"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">header</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-medium text-gray-600 dark:text-gray-300"</span>&gt;</span>Recordings<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center gap-x-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"state.isRecording"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-2 h-2 rounded-full bg-red-500 animate-pulse"</span> /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mr-2 text-sm"</span>&gt;</span>
            {{ formatDuration(state.recordingDuration) }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
          <span class="hljs-attr">:icon</span>=<span class="hljs-string">"state.isRecording ? 'i-lucide-circle-stop' : 'i-lucide-mic'"</span>
          <span class="hljs-attr">:color</span>=<span class="hljs-string">"state.isRecording ? 'error' : 'primary'"</span>
          <span class="hljs-attr">:loading</span>=<span class="hljs-string">"isTranscribing"</span>
          @<span class="hljs-attr">click</span>=<span class="hljs-string">"toggleRecording"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">AudioVisualizer</span>
      <span class="hljs-attr">v-if</span>=<span class="hljs-string">"state.isRecording"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-14 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2"</span>
      <span class="hljs-attr">:audio-data</span>=<span class="hljs-string">"state.audioData"</span>
      <span class="hljs-attr">:data-update-trigger</span>=<span class="hljs-string">"state.updateTrigger"</span>
    /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"isTranscribing"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-center h-14 gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2 text-gray-500 dark:text-gray-400"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UIcon</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"i-lucide-refresh-cw"</span> <span class="hljs-attr">size</span>=<span class="hljs-string">"size-6"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"animate-spin"</span> /&gt;</span>
      Transcribing...
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"space-y-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"recording in recordings"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"recording.id"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg"</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"recording.url"</span> <span class="hljs-attr">controls</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full h-10"</span> /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
          <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-lucide-trash-2"</span>
          <span class="hljs-attr">color</span>=<span class="hljs-string">"error"</span>
          <span class="hljs-attr">variant</span>=<span class="hljs-string">"ghost"</span>
          <span class="hljs-attr">size</span>=<span class="hljs-string">"sm"</span>
          @<span class="hljs-attr">click</span>=<span class="hljs-string">"removeRecording(recording)"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!recordings.length &amp;&amp; !state.isRecording &amp;&amp; !isTranscribing"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full flex flex-col items-center justify-center text-gray-500 dark:text-gray-400"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>No recordings...!<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm mt-1"</span>&gt;</span>Tap the mic icon to create one.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> emit = defineEmits&lt;{ <span class="hljs-attr">transcription</span>: [text: string] }&gt;();

<span class="hljs-keyword">const</span> { state, startRecording, stopRecording } = useMediaRecorder();
<span class="hljs-keyword">const</span> toggleRecording = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (state.value.isRecording) {
    handleRecordingStop();
  } <span class="hljs-keyword">else</span> {
    handleRecordingStart();
  }
};

<span class="hljs-keyword">const</span> handleRecordingStart = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> startRecording();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error accessing microphone:"</span>, err);
    useToast().add({
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Error"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Could not access microphone. Please check permissions."</span>,
      <span class="hljs-attr">color</span>: <span class="hljs-string">"error"</span>,
    });
  }
};

<span class="hljs-keyword">const</span> { recordings, addRecording, removeRecording, resetRecordings } =
  useRecordings();

<span class="hljs-keyword">const</span> handleRecordingStop = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">let</span> blob: Blob | <span class="hljs-literal">undefined</span>;

  <span class="hljs-keyword">try</span> {
    blob = <span class="hljs-keyword">await</span> stopRecording();
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error stopping recording:"</span>, err);
    useToast().add({
      <span class="hljs-attr">title</span>: <span class="hljs-string">"Error"</span>,
      <span class="hljs-attr">description</span>: <span class="hljs-string">"Failed to record audio. Please try again."</span>,
      <span class="hljs-attr">color</span>: <span class="hljs-string">"error"</span>,
    });
  }

  <span class="hljs-keyword">if</span> (blob) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> transcription = <span class="hljs-keyword">await</span> transcribeAudio(blob);

      <span class="hljs-keyword">if</span> (transcription) {
        emit(<span class="hljs-string">"transcription"</span>, transcription);

        addRecording({
          <span class="hljs-attr">url</span>: URL.createObjectURL(blob),
          blob,
          <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now()}</span>`</span>,
        });
      }
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error transcribing audio:"</span>, err);
      useToast().add({
        <span class="hljs-attr">title</span>: <span class="hljs-string">"Error"</span>,
        <span class="hljs-attr">description</span>: <span class="hljs-string">"Failed to transcribe audio. Please try again."</span>,
        <span class="hljs-attr">color</span>: <span class="hljs-string">"error"</span>,
      });
    }
  }
};

<span class="hljs-keyword">const</span> isTranscribing = ref(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> transcribeAudio = <span class="hljs-keyword">async</span> (blob: Blob) =&gt; {
  <span class="hljs-keyword">try</span> {
    isTranscribing.value = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();
    formData.append(<span class="hljs-string">"audio"</span>, blob);

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> $fetch(<span class="hljs-string">"/api/transcribe"</span>, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">body</span>: formData,
    });
  } <span class="hljs-keyword">finally</span> {
    isTranscribing.value = <span class="hljs-literal">false</span>;
  }
};

<span class="hljs-keyword">const</span> uploadRecordings = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">if</span> (!recordings.value.length) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();
  recordings.value.forEach(<span class="hljs-function">(<span class="hljs-params">recording</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (recording.blob) {
      formData.append(
        <span class="hljs-string">"files"</span>,
        recording.blob,
        <span class="hljs-string">`<span class="hljs-subst">${recording.id}</span>.<span class="hljs-subst">${recording.blob.type.split(<span class="hljs-string">"/"</span>)[<span class="hljs-number">1</span>]}</span>`</span>,
      );
    }
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> $fetch(<span class="hljs-string">"/api/upload"</span>, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"PUT"</span>,
      <span class="hljs-attr">body</span>: formData,
    });

    <span class="hljs-keyword">return</span> result.map(<span class="hljs-function">(<span class="hljs-params">obj</span>) =&gt;</span> obj.pathname);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to upload audio recordings"</span>, error);
  }
};

<span class="hljs-keyword">const</span> isBusy = computed(<span class="hljs-function">() =&gt;</span> state.value.isRecording || isTranscribing.value);

defineExpose({ uploadRecordings, resetRecordings, isBusy });

<span class="hljs-keyword">const</span> formatDuration = <span class="hljs-function">(<span class="hljs-params">seconds: number</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> mins = <span class="hljs-built_in">Math</span>.floor(seconds / <span class="hljs-number">60</span>);
  <span class="hljs-keyword">const</span> secs = seconds % <span class="hljs-number">60</span>;
  <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${mins}</span>:<span class="hljs-subst">${secs.toString().padStart(<span class="hljs-number">2</span>, <span class="hljs-string">"0"</span>)}</span>`</span>;
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This component does the following:</p>
<ol>
<li><p>Allows recording the user’s voice with the help of <code>useMediaRecorder</code> composable created earlier. It also integrates the <code>AudioVisualizer</code> component to enhance the user experience by providing real-time audio feedback during recordings.</p>
</li>
<li><p>On a new recording, sends the recorded blob for transcription to the <code>transcribe</code> API endpoint, and emits the transcription text on success</p>
</li>
<li><p>Displays all recordings as <code>audio</code> elements for users perusal (using <code>URL.createObjectURL(blob)</code>). It utilizes the <code>useRecordings</code> composable to manage the recordings</p>
</li>
<li><p>Uploads the final recordings to R2 (the local disk in dev mode) using the <code>/api/upload</code> endpoint, and returns the pathnames of these recordings to the caller (the <code>NoteEditorModal</code> component)</p>
</li>
</ol>
<h3 id="heading-audiovisualizer-component"><code>AudioVisualizer</code> Component</h3>
<p>This component uses an HTML canvas element to represent the audio waveform along a horizontal line. The canvas element is used for its flexibility and efficiency in rendering real-time visualizations, making it suitable for audio waveforms.</p>
<p>The visualization dynamically adjusts based on the amplitude of the captured audio, providing a real-time feedback loop for the user during recording. To do that, it watches the <code>updateTrigger</code> state variable exposed by <code>useMediaRecorder</code> to redraw the canvas on audio data changes.</p>
<p>Create a new file <code>AudioVisualizer.vue</code> in the <code>app/components</code> folder and add the following code to it:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- app/components/AudioVisualizer.vue --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">canvas</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"canvas"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"640"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"100"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> props = defineProps&lt;{
  <span class="hljs-attr">audioData</span>: <span class="hljs-built_in">Uint8Array</span> | <span class="hljs-literal">null</span>;
  dataUpdateTrigger: number;
}&gt;();

<span class="hljs-keyword">let</span> width = <span class="hljs-number">0</span>;
<span class="hljs-keyword">let</span> height = <span class="hljs-number">0</span>;
<span class="hljs-keyword">const</span> audioCanvas = useTemplateRef&lt;HTMLCanvasElement&gt;(<span class="hljs-string">"canvas"</span>);
<span class="hljs-keyword">const</span> canvasCtx = ref&lt;CanvasRenderingContext2D | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

onMounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (audioCanvas.value) {
    canvasCtx.value = audioCanvas.value.getContext(<span class="hljs-string">"2d"</span>);
    width = audioCanvas.value.width;
    height = audioCanvas.value.height;
  }
});

<span class="hljs-keyword">const</span> drawCanvas = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (!canvasCtx.value || !props.audioData) {
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">const</span> data = props.audioData;
  <span class="hljs-keyword">const</span> ctx = canvasCtx.value;
  <span class="hljs-keyword">const</span> sliceWidth = width / data.length;

  ctx.clearRect(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, width, height);
  ctx.lineWidth = <span class="hljs-number">2</span>;
  ctx.strokeStyle = <span class="hljs-string">"rgb(221, 72, 49)"</span>;
  ctx.beginPath();

  <span class="hljs-keyword">let</span> x = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; data.length; i++) {
    <span class="hljs-keyword">const</span> v = (data[i] ?? <span class="hljs-number">0</span>) / <span class="hljs-number">128.0</span>;
    <span class="hljs-keyword">const</span> y = (v * height) / <span class="hljs-number">2</span>;

    <span class="hljs-keyword">if</span> (i === <span class="hljs-number">0</span>) {
      ctx.moveTo(x, y);
    } <span class="hljs-keyword">else</span> {
      ctx.lineTo(x, y);
    }

    x += sliceWidth;
  }

  ctx.lineTo(width, height / <span class="hljs-number">2</span>);
  ctx.stroke();
};

watch(
  <span class="hljs-function">() =&gt;</span> props.dataUpdateTrigger,
  <span class="hljs-function">() =&gt;</span> {
    drawCanvas();
  },
  { <span class="hljs-attr">immediate</span>: <span class="hljs-literal">true</span> },
);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<h3 id="heading-userecordings-composable"><code>useRecordings</code> Composable</h3>
<p>The <code>NoteRecorder</code> component uses the <code>useRecordings</code> composable to manage the list of recordings, and to clear any used resources. Create a new file <code>useRecordings.ts</code> in the <code>app/composables</code> folder and add the following code to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/composables/useRecordings.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useRecordings = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> recordings = ref&lt;Recording[]&gt;([]);

  <span class="hljs-keyword">const</span> cleanupResource = <span class="hljs-function">(<span class="hljs-params">recording: Recording</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (recording.blob) {
      URL.revokeObjectURL(recording.url);
    }
  };

  <span class="hljs-keyword">const</span> cleanupResources = <span class="hljs-function">() =&gt;</span> {
    recordings.value.forEach(<span class="hljs-function">(<span class="hljs-params">recording</span>) =&gt;</span> {
      cleanupResource(recording);
    });
  };

  <span class="hljs-keyword">const</span> addRecording = <span class="hljs-function">(<span class="hljs-params">recording: Recording</span>) =&gt;</span> {
    recordings.value.unshift(recording);
  };

  <span class="hljs-keyword">const</span> removeRecording = <span class="hljs-function">(<span class="hljs-params">recording: Recording</span>) =&gt;</span> {
    recordings.value = recordings.value.filter(<span class="hljs-function">(<span class="hljs-params">r</span>) =&gt;</span> r.id !== recording.id);
    cleanupResource(recording);
  };

  <span class="hljs-keyword">const</span> resetRecordings = <span class="hljs-function">() =&gt;</span> {
    cleanupResources();

    recordings.value = [];
  };

  onUnmounted(cleanupResources);

  <span class="hljs-keyword">return</span> {
    recordings,
    addRecording,
    removeRecording,
    resetRecordings,
  };
};
</code></pre>
<p>You can define the <code>Recording</code> type definition in the <code>shared/types/index.ts</code> file. This allows for auto import of type definitions in both client &amp; server sides (The intended purpose of the shared folder is for sharing common types &amp; utils between the app &amp; server). Also, while you’re at it, you can also define the <code>Note</code> type.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// shared/types/index.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { noteSelectSchema } <span class="hljs-keyword">from</span> <span class="hljs-string">"#shared/schemas/note.schema"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Recording = {
  url: <span class="hljs-built_in">string</span>;
  blob?: Blob;
  id: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> Note = z.output&lt;<span class="hljs-keyword">typeof</span> noteSelectSchema&gt;;
</code></pre>
<h3 id="heading-creating-the-home-page">Creating the Home Page</h3>
<p>Now that we have all the pieces ready for the basic app, it is time to put everything together in a page. Delete the content of the home page (<code>app/pages/index.vue</code>), and put the following content to it:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- app/pages/index.vue --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UContainer</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-screen flex justify-center items-center"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full max-h-full overflow-hidden max-w-4xl mx-auto"</span>
      <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ body: 'h-[calc(100vh-4rem)] overflow-y-auto' }"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">header</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold text-xl md:text-2xl"</span>&gt;</span>Voice Notes<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span> <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-lucide-plus"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"showNoteModal"</span>&gt;</span>
          New Note
        <span class="hljs-tag">&lt;/<span class="hljs-name">UButton</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"notes?.length"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"space-y-4"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">NoteCard</span> <span class="hljs-attr">v-for</span>=<span class="hljs-string">"note in notes"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"note.id"</span> <span class="hljs-attr">:note</span>=<span class="hljs-string">"note"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
        <span class="hljs-attr">v-else</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"my-12 text-center text-gray-500 dark:text-gray-400 space-y-2"</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-2xl md:text-3xl"</span>&gt;</span>No notes created<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Get started by creating your first note<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UContainer</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { LazyNoteEditorModal } <span class="hljs-keyword">from</span> <span class="hljs-string">"#components"</span>;

<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: notes, refresh } = <span class="hljs-keyword">await</span> useFetch(<span class="hljs-string">"/api/notes"</span>);

<span class="hljs-keyword">const</span> modal = useModal();
<span class="hljs-keyword">const</span> showNoteModal = <span class="hljs-function">() =&gt;</span> {
  modal.open(LazyNoteEditorModal, {
    <span class="hljs-attr">onNewNote</span>: refresh,
  });
};

watch(modal.isOpen, <span class="hljs-function">(<span class="hljs-params">newState</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!newState) {
    modal.reset();
  }
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>On this page we’re doing the following:</p>
<ol>
<li><p>Fetch the list of existing notes from the database and display them using the <code>NoteCard</code> component</p>
</li>
<li><p>Shows a new note button which when clicked opens the <code>NoteEditorModal</code>. On successful note creation the <code>refresh</code> function is called to refetch the notes</p>
</li>
<li><p>The modal state is reset on closure to ensure a clean slate for the next note creation</p>
</li>
</ol>
<p>The cards and modals headers/footers used in the app follow a global style that is defined in the app config file. Centralizing styles in the app configuration ensures consistent theming and reduces redundancy across components.</p>
<p>Create a new file <code>app.config.ts</code> inside the <code>app</code> folder, and add the following to it:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/app.config.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineAppConfig({
  ui: {
    card: {
      slots: {
        header: <span class="hljs-string">"flex items-center justify-between gap-3 flex-wrap"</span>,
      },
    },
    modal: {
      slots: {
        footer: <span class="hljs-string">"justify-end gap-x-3"</span>,
      },
    },
  },
});
</code></pre>
<p>You’ll also need to wrap your <code>NuxtPage</code> component with the <code>UApp</code> component for the modals and toast notifications to work as shown below:</p>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- app/app.vue --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">NuxtRouteAnnouncer</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">NuxtLoadingIndicator</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UApp</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">NuxtPage</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UApp</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<h3 id="heading-notecard-component"><code>NoteCard</code> component</h3>
<p>This component displays the note text and the attached audio recordings of a note. The note text is clamped to 3 lines with a show more/less button to show/hide rest of the text. Text clamping ensures that the UI remains clean and uncluttered, while the show more/less button gives users full control over note visibility.</p>
<p>Create a new file <code>NoteCard.vue</code> in the <code>app/components</code> folder, and add the following code to it:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hover:shadow-lg transition-shadow"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>
        <span class="hljs-attr">ref</span>=<span class="hljs-string">"text"</span>
        <span class="hljs-attr">:class</span>=<span class="hljs-string">"['whitespace-pre-wrap', !showFullText &amp;&amp; 'line-clamp-3']"</span>
      &gt;</span>
        {{ note.text }}
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
        <span class="hljs-attr">v-if</span>=<span class="hljs-string">"shouldShowExpandBtn"</span>
        <span class="hljs-attr">variant</span>=<span class="hljs-string">"link"</span>
        <span class="hljs-attr">:padded</span>=<span class="hljs-string">"false"</span>
        @<span class="hljs-attr">click</span>=<span class="hljs-string">"showFullText = !showFullText"</span>
      &gt;</span>
        {{ showFullText ? "Show less" : "Show more" }}
      <span class="hljs-tag">&lt;/<span class="hljs-name">UButton</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">v-if</span>=<span class="hljs-string">"note.audioUrls &amp;&amp; note.audioUrls.length &gt; 0"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4 flex gap-x-2 overflow-x-auto"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">audio</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"url in note.audioUrls"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">:src</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">controls</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-60 shrink-0 h-10"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center text-sm text-gray-500 dark:text-gray-400 gap-x-2 mt-6"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UIcon</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"i-lucide-clock"</span> <span class="hljs-attr">size</span>=<span class="hljs-string">"size-4"</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>
        {{
          note.updatedAt &amp;&amp; note.updatedAt !== note.createdAt
            ? `Updated ${updated}`
            : `Created ${created}`
        }}
      <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { useTimeAgo } <span class="hljs-keyword">from</span> <span class="hljs-string">"@vueuse/core"</span>;

<span class="hljs-keyword">const</span> props = defineProps&lt;{ <span class="hljs-attr">note</span>: Note }&gt;();

<span class="hljs-keyword">const</span> createdAt = computed(<span class="hljs-function">() =&gt;</span> props.note.createdAt + <span class="hljs-string">"Z"</span>);
<span class="hljs-keyword">const</span> updatedAt = computed(<span class="hljs-function">() =&gt;</span> props.note.updatedAt + <span class="hljs-string">"Z"</span>);

<span class="hljs-keyword">const</span> created = useTimeAgo(createdAt);
<span class="hljs-keyword">const</span> updated = useTimeAgo(updatedAt);

<span class="hljs-keyword">const</span> showFullText = ref(<span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> shouldShowExpandBtn = ref(<span class="hljs-literal">false</span>);
<span class="hljs-keyword">const</span> noteText = useTemplateRef&lt;HTMLParagraphElement&gt;(<span class="hljs-string">"text"</span>);
<span class="hljs-keyword">const</span> checkTextExpansion = <span class="hljs-function">() =&gt;</span> {
  nextTick(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (noteText.value) {
      shouldShowExpandBtn.value =
        noteText.value.scrollHeight &gt; noteText.value.clientHeight;
    }
  });
};

onMounted(checkTextExpansion);

watch(<span class="hljs-function">() =&gt;</span> props.note.text, checkTextExpansion);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>And we are done here. Try running the application and create some notes. You should be able to create notes, add multiple recordings to the same note etc. Everything should be working now, or is it?</p>
<p>Try playing the audio recordings of the saved notes, are these playable?</p>
<p><img src="https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExbTFvbTQ4Z3h1aXlyNHhvOW9ibW9oM2M5OWE2Y3MyczRxazRqN3E2YSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3oEjHWzZQaCrZW2aWs/giphy.gif" alt="Houston, we have a problem" class="image--center mx-auto" /></p>
<h3 id="heading-serving-the-audio-recordings">Serving the Audio Recordings</h3>
<p>We can’t play the audio recordings because these are saved in R2 (local disk in dev mode), and nowhere we are serving these files. It is time to fix that.</p>
<p>If you look at the <code>/api/notes</code> code, we save the audio urls/pathnames with an <code>audio</code> prefix</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> useDrizzle()
  .insert(tables.notes)
  .values({
    text,
    audioUrls: audioUrls ? audioUrls.map(<span class="hljs-function">(<span class="hljs-params">url</span>) =&gt;</span> <span class="hljs-string">`/audio/<span class="hljs-subst">${url}</span>`</span>) : <span class="hljs-literal">null</span>,
  });
</code></pre>
<p>The reason to do so was to serve all audio recordings through an <code>/audio</code> path. Create a new file <code>[…pathname].get.ts</code> in the <code>server/routes/audio</code> folder and add the following to it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> { pathname } = getRouterParams(event);

  <span class="hljs-keyword">return</span> hubBlob().serve(event, pathname);
});
</code></pre>
<p>What we’ve done above is to catch all requests to the <code>/audio</code> path (by using the wildcard <code>[…pathname]</code> in the filename), and serve the requested recording from the storage using <code>hubBlob</code>.</p>
<p>With this, the frontend is complete, and all functionalities should now work seamlessly.</p>
<h2 id="heading-further-enhancements">Further Enhancements</h2>
<p>What you’ve created here is a basic version of the application—with all must-have features—that you saw in the beginning of the article. You can further refine the app and take it closer to the demo by:</p>
<p>What you’ve created here is a solid foundation for the application, complete with the core features introduced earlier. To further enhance the app and bring it closer to the full demo version, consider implementing the following features:</p>
<ol>
<li><p>Adding a settings page to save post processing settings</p>
</li>
<li><p>Handle post processing in the <code>/transcribe</code> api route</p>
</li>
<li><p>Allowing edit/delete of saved notes</p>
</li>
<li><p>Experimenting with additional features that fit your use case or user needs.</p>
</li>
</ol>
<p>If you get stuck while implementing these features, do not hesitate to look at the application source code. The complete source code of the final application is shared at the end of the article.</p>
<h2 id="heading-deploying-the-application">Deploying the Application</h2>
<p>You can deploy the application using either the NuxtHub admin dashboard or through the NuxtHub CLI.</p>
<h3 id="heading-deploy-via-nuxthub-admin">Deploy via NuxtHub Admin</h3>
<ul>
<li><p>Push your code to a GitHub repository.</p>
</li>
<li><p>Link the repository with NuxtHub.</p>
</li>
<li><p>Deploy from the Admin console.</p>
</li>
</ul>
<p><a target="_blank" href="https://hub.nuxt.com/docs/getting-started/deploy#cloudflare-pages-ci">Learn more about NuxtHub Git integration</a></p>
<h3 id="heading-deploy-via-nuxthub-cli">Deploy via NuxtHub CLI</h3>
<pre><code class="lang-bash">npx nuxthub deploy
</code></pre>
<p><a target="_blank" href="https://hub.nuxt.com/docs/getting-started/deploy#nuxthub-cli">Learn more about CLI deployment</a></p>
<h2 id="heading-source-code">Source Code</h2>
<p>You can find the source code of <code>Vhisper</code> application on GitHub. The source code includes all the features discussed in this article, along with additional configurations and optimizations shown in the demo.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/vhisper">https://github.com/ra-jeev/vhisper</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You've built a powerful application that records and transcribes audio, stores recordings, and manages notes with an intuitive interface. Along the way you’ve touched upon various aspects of Nuxt, NuxtHub and Cloudflare services. As you continue to refine and expand Vhisper, consider exploring additional features and optimizations to further enhance its functionality and user experience. Keep experimenting and innovating, and let this project be a stepping stone to even more ambitious endeavors.</p>
<p>Thank you for sticking with me until the end! I hope you’ve picked up some new concepts along the way. I’d love to hear what you learned or any thoughts you have in the comments section. Your feedback is not only valuable to me, but to the entire developer community exploring this exciting field.</p>
<p>Until next time!</p>
<hr />
<blockquote>
<p><em>Keep adding the bits and soon you'll have a lot of bytes to share with the world.</em></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Rethinking GitHub Search: How I built a Chat Interface to search GitHub]]></title><description><![CDATA[Here I am again, exploring the world of chat interfaces. If you've been following my journey (I mean my last post), you might think, 'Rajeev, you're becoming obsessed with chatting!' And you wouldn't be entirely wrong. While I'm not always the most t...]]></description><link>https://rajeev.dev/building-a-chat-interface-to-search-github</link><guid isPermaLink="true">https://rajeev.dev/building-a-chat-interface-to-search-github</guid><category><![CDATA[Nuxt]]></category><category><![CDATA[nuxthub]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[openai]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Fri, 04 Oct 2024 11:53:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728041902174/5f866b99-3c0b-4c3d-859b-fa98566ea7fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here I am again, exploring the world of chat interfaces. If you've been following my journey (I mean my <a target="_blank" href="https://rajeev.dev/create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui">last post</a>), you might think, 'Rajeev, you're becoming obsessed with chatting!' And you wouldn't be entirely wrong. While I'm not always the most talkative person in a room, give me the right topic - like simplifying how we interact with technology - and my excitement becomes difficult to contain.</p>
<p>This time, my enthusiasm has led me to tackle a challenge many developers face often: searching GitHub efficiently. Let me introduce Chat GitHub, a tool where the complexity of code repositories meets the simplicity of conversation.</p>
<h2 id="heading-why-chat-github">Why Chat GitHub?</h2>
<p>So, why a chat interface for GitHub search? Well, GitHub search, as powerful as it is with all its filters, can’t beat the simplicity of natural language queries. Let’s be real—it won’t answer simple questions like, <em>“When did I make my first commit?”</em> (What filters would you even use to find this information with the current GitHub Search?). GitHub certainly knows (reminds me of <em>“I Know What You Did Last Summer”</em>), but it just won’t answer you directly.</p>
<p>Natural language makes the experience frictionless. Instead of crafting complex queries, you can just <em>ask</em> GitHub, like you would a colleague.</p>
<p>When <a class="user-mention" href="https://hashnode.com/@atinuxt">Sébastien Chopin</a> mentioned the idea to me, I was onboard immediately (also because I was itching to build something…).</p>
<h2 id="heading-how-it-works">How it Works?</h2>
<p>At its core, Chat GitHub leverages the power of OpenAI's language models to interpret your natural language queries and translate them into GitHub's search syntax. Here's a simplified breakdown of the process:</p>
<ol>
<li><p>You enter a query in plain English</p>
</li>
<li><p>The AI model interprets your request</p>
</li>
<li><p>It then selects the appropriate search endpoint, and generates the necessary GitHub API query parameters</p>
</li>
<li><p>We send a request to GitHub's API with these details</p>
</li>
<li><p>The results are fetched and presented to you in a clean, easy-to-digest chat interface</p>
</li>
</ol>
<p>Here's a sneak peek of what the chat interface looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727885067240/2b8c0fe0-0564-4f0f-8609-19d249549829.png" alt="chat github's chatting interface" class="image--center mx-auto" /></p>
<p>You can try it out live here: <a target="_blank" href="https://chat-github.nuxt.dev/">https://chat-github.nuxt.dev</a></p>
<p>We’ll explore the key aspects of this process in the sections to follow. Ready? Let’s get started.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>This project follows a similar setup to my last one <a target="_blank" href="https://hub-chat.nuxt.dev">Hub Chat</a> (<a target="_blank" href="https://github.com/ra-jeev/hub-chat">GitHub link</a>), and I’ve reused several components with some slight modifications. I won't bore you by repeating the same details, but if you’re new here, feel free to follow along with both posts (<a target="_blank" href="https://rajeev.dev/create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui">previous post</a>) for a more complete picture.</p>
<h3 id="heading-tech-stack">Tech Stack</h3>
<p>Here’s a quick breakdown of the tech stack:</p>
<ul>
<li><p><strong>Nuxt 3</strong>: For the overall framework and routing.</p>
</li>
<li><p><strong>OpenAI APIs</strong>: To handle the natural language processing.</p>
</li>
<li><p><strong>GitHub API</strong>: To fetch the data you’re looking for—remember? Only <em>it</em> knows what you did last summer.</p>
</li>
<li><p><strong>NuxtUI</strong>: To make sure the UI is smooth and responsive.</p>
</li>
<li><p><strong>Nuxt-Auth-Utils</strong>: For user authentication and handling GitHub login (Only authenticated users can start a chat).</p>
</li>
<li><p><strong>Nuxt MDC:</strong> For parsing and displaying the markdown responses</p>
</li>
<li><p><strong>NuxtHub</strong>: For deployment, database, and caching (all powered by Cloudflare).</p>
</li>
</ul>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p><strong>GitHub account:</strong> You'll need this to generate a <code>GITHUB_TOKEN</code> for API queries, and to create an OAuth App for authentication.</p>
</li>
<li><p><strong>OpenAI account:</strong> For creating an OpenAI API key, so the app can process your queries.</p>
</li>
<li><p><strong>Optional</strong>: If you're looking to deploy the project yourself, you'll need Cloudflare and a NuxtHub account.</p>
</li>
</ul>
<h3 id="heading-setting-up-the-project"><strong>Setting up the project</strong></h3>
<p>Follow the setup process from my previous article. Just remember to add the <code>nuxt-auth-utils</code>, <code>@octokit/rest</code> and <code>OpenAI</code> dependencies.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add the nuxt-auth-utils module</span>
npx nuxi module add auth-utils

<span class="hljs-comment"># Add the Octokit Rest &amp; OpenAI libraries</span>
pnpm add @octokit/rest openai
</code></pre>
<p>Once the dependencies are set up, you should be able to run the project with <code>pnpm dev</code> and see the default app UI at localhost:3000.</p>
<h3 id="heading-configs-and-environment-variables">Configs and Environment Variables</h3>
<p>At this point, you can enable the hub database and cache in the <code>nuxt.config.ts</code> file for later use, as well as create the necessary API tokens and keys to place in the <code>.env</code> file.</p>
<p>Enabling <code>database</code> and <code>cache</code> in <code>nuxt.config.ts</code>:</p>
<pre><code class="lang-typescript">hub: {
  cache: <span class="hljs-literal">true</span>,
  database: <span class="hljs-literal">true</span>,
},
</code></pre>
<p>Next, generate the following tokens and keys, and store them in your <code>.env</code> file (located in the root of your project):</p>
<ul>
<li><p><strong>GitHub Token</strong>: Create a <a target="_blank" href="https://github.com/settings/personal-access-tokens/new">GitHub token</a> (no special scope required) for making API calls.</p>
</li>
<li><p><strong>OpenAI API Key</strong>: Generate a key from your <a target="_blank" href="https://platform.openai.com/api-keys">OpenAI dashboard</a>.</p>
</li>
<li><p><strong>GitHub OAuth App</strong>: Create a <a target="_blank" href="https://github.com/settings/applications/new">GitHub OAuth app</a> and get its <code>CLIENT_ID</code> and <code>CLIENT_SECRET</code>. This will be used to authenticate users (only authenticated users can start a chat). For more information on how to create a GitHub OAuth app, refer to the <a target="_blank" href="https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app">GitHub documentation.</a></p>
</li>
</ul>
<p>Your <code>.env</code> should contain the following entries:</p>
<pre><code class="lang-bash">NUXT_SESSION_PASSWORD=at_least_32_chars_string
NUXT_OAUTH_GITHUB_CLIENT_ID=github_oauth_client_id
NUXT_OAUTH_GITHUB_CLIENT_SECRET=github_oauth_client_secret
NUXT_GITHUB_TOKEN=your_personal_access_token
OPENAI_API_KEY=your_openai_api_key
</code></pre>
<p><em>Note:</em> <code>NUXT_SESSION_PASSWORD</code> <em>will automatically be created by</em> <code>nuxt-auth-utils</code> <em>in development if you haven’t set it manually.</em></p>
<p>And that’s it for the setup—phew!</p>
<p>In the next section, we’ll explore the core of the chat process: making sense of the user query, converting it to a GitHub API call amd generating a final response.</p>
<h2 id="heading-from-user-query-to-github-api-call">From User Query to GitHub API Call</h2>
<p>Now, let’s break down how Chat GitHub processes your query, identifies the necessary actions, and makes the appropriate GitHub API call. This involves three main steps: interpreting the user query, calling the necessary tools (the GitHub API), and generating the final response.</p>
<h3 id="heading-1-interpreting-the-user-query">1. Interpreting the User Query</h3>
<p>The first challenge is understanding what the user is asking for. To do this, the system relies on OpenAI’s language models to parse natural language inputs. It takes into account several factors:</p>
<ul>
<li><p><strong>Intent Recognition</strong>: What is the user trying to achieve? Are they searching for commits, issues, repositories, or user profiles?</p>
</li>
<li><p><strong>Parameter Extraction</strong>: Once the intent is clear, the model extracts necessary parameters like repo name, user, dates, and other filters. These details are critical for constructing a meaningful API call.</p>
</li>
<li><p><strong>Tool Selection</strong>: Based on the query, the AI determines if a GitHub API tool needs to be invoked (for example, searching commits or issues).</p>
</li>
</ul>
<p>To guide the AI, I created a detailed system prompt to provide context. Here’s a portion of that prompt:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> systemPropmt = <span class="hljs-string">`You are a concise assistant who helps \
users find information on GitHub. Use the supplied tools to \
find information when asked.

Available endpoints and key parameters:

// ...

2. issues (Also searches PRs):
  - Sort options: comments, reactions, reactions-+1, ...
  - Query qualifiers: type, is, state, author, assignee, ...
  - use "type" or "is" qualifier to search issues or PRs (type:issue/type:pr)

// ...

When using searchGithub function:
1. Choose the appropriate search endpoint.
2. Formulate a concise query (q) as per the user's request.
3. Add any relevant sort or order parameters if needed.
4. Always use appropriate per_page value to limit the number of results.

// ...

Examples: 
// ...

2. Find the total number of repositories of a user
arguments: 
{
  "endpoint": "repositories",
  "q": "user:&lt;user_login&gt;",
  "per_page": 1
}

Summarize final response concisely using markdown when appropriate \
(for all links add {target="_blank"} at the end). Do not include \
images, commit SHA or hashes etc. in your summary.`</span>
</code></pre>
<p><em>Note: I restricted the AI to only the most important GitHub search endpoints</em> <code>/search/commits</code><em>,</em> <code>/search/issues</code><em>,</em> <code>/search/repositories</code> <em>and</em> <code>/search/users</code><em>.</em></p>
<p>In addition to the system prompt, we create tools definitions that lists the types of tools, their names, and their specific parameters (in this case I only create one function tool, <code>searchGithub</code>).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> tools: OpenAI.ChatCompletionTool[] = [
  {
    <span class="hljs-keyword">type</span>: <span class="hljs-string">'function'</span>,
    <span class="hljs-function"><span class="hljs-keyword">function</span>: </span>{
      name: <span class="hljs-string">'searchGithub'</span>,
      description:
        <span class="hljs-string">'Searches GitHub for information using the GitHub API. Call this if you need to find information on GitHub.'</span>,
      parameters: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'object'</span>,
        properties: {
          endpoint: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
            description: <span class="hljs-string">`The specific search endpoint to use. One of ['commits', 'issues', 'repositories', 'users']`</span>,
          },
          q: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
            description: <span class="hljs-string">'the search query using applicable qualifiers'</span>,
          },
          sort: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
            description: <span class="hljs-string">'The sort field (optional, depends on the endpoint)'</span>,
          },
          order: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
            description: <span class="hljs-string">'The sort order (optional, asc or desc)'</span>,
          },
          per_page: {
            <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>,
            description:
              <span class="hljs-string">'Number of results to fetch per page (max 25)'</span>,
          },
        },
        required: [<span class="hljs-string">'endpoint'</span>, <span class="hljs-string">'q'</span>, <span class="hljs-string">'per_page'</span>],
        additionalProperties: <span class="hljs-literal">false</span>,
      },
    },
  },
];
</code></pre>
<p>A couple of key design decisions we made:</p>
<ul>
<li><p><strong>Complimentary System Prompt &amp; Tool Definition</strong>: The system prompt gives context, while the tool definition ensures the API queries are correctly structured.</p>
</li>
<li><p><strong>Result Limit</strong>: While GitHub allows more items per response, we restrict results to 25 to keep things manageable.</p>
</li>
<li><p><strong>No Pagination</strong>: We decided to skip pagination, keeping things simple and avoiding unnecessary complexity.</p>
</li>
</ul>
<p>Now, the AI is ready to handle the user query and transform it into a structured format that the system can use. Here is the relevant code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> _openai: OpenAI;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useOpenAI</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (!_openai) {
    _openai = <span class="hljs-keyword">new</span> OpenAI({
      apiKey: process.env.OPENAI_API_KEY,
    });
  }

  <span class="hljs-keyword">return</span> _openai;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handleMessageWithOpenAI = <span class="hljs-keyword">async</span> (
  event: H3Event,
  messages: OpenAI.Chat.ChatCompletionMessageParam[]
) =&gt; {
  <span class="hljs-keyword">const</span> openai = useOpenAI();
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> openai.chat.completions.create({
    model: MODEL, <span class="hljs-comment">// used gpt-4o</span>
    messages, <span class="hljs-comment">// contains system prompt and the complete chat history</span>
    tools, <span class="hljs-comment">// defined above</span>
  });

  <span class="hljs-keyword">const</span> responseMessage = response.choices[<span class="hljs-number">0</span>].message;
  <span class="hljs-keyword">const</span> toolCalls = responseMessage.tool_calls;

  <span class="hljs-keyword">if</span> (toolCalls) {
    messages.push(responseMessage);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> toolCall <span class="hljs-keyword">of</span> toolCalls) {
      <span class="hljs-keyword">const</span> functionName = toolCall.function.name;
      <span class="hljs-keyword">if</span> (functionName === <span class="hljs-string">'searchGithub'</span>) {
        <span class="hljs-keyword">const</span> functionArgs = <span class="hljs-built_in">JSON</span>.parse(toolCall.function.arguments);
        <span class="hljs-keyword">const</span> functionResponse = <span class="hljs-keyword">await</span> searchGithub(
          functionArgs.endpoint,
          {
            q: functionArgs.q,
            sort: functionArgs.sort,
            order: functionArgs.order,
            per_page: functionArgs.per_page,
          }
        );

        <span class="hljs-comment">// ..</span>
      }
    }

    <span class="hljs-comment">// ...</span>
  }

  <span class="hljs-keyword">return</span> responseMessage.content;
}
</code></pre>
<h3 id="heading-2-using-tool-calls-for-github-search">2. Using Tool Calls for GitHub Search</h3>
<p>Once the AI determines the need for a GitHub API call, it generates a <code>tool_call</code> with the necessary parameters. Here’s how we handle this with the <code>searchGitHub</code> function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> SearchParams = {
  endpoint: <span class="hljs-built_in">string</span>;
  q: <span class="hljs-built_in">string</span>;
  order?: <span class="hljs-built_in">string</span>;
  sort?: <span class="hljs-built_in">string</span>;
  per_page: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">const</span> allowedEndpoints = {
  commits: <span class="hljs-string">'GET /search/commits'</span>,
  issues: <span class="hljs-string">'GET /search/issues'</span>,
  repositories: <span class="hljs-string">'GET /search/repositories'</span>,
  users: <span class="hljs-string">'GET /search/users'</span>,
} <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>;

<span class="hljs-keyword">type</span> EndpointType = keyof <span class="hljs-keyword">typeof</span> allowedEndpoints;

<span class="hljs-keyword">let</span> _octokit: Octokit;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useOctokit</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">if</span> (!_octokit) {
    _octokit = <span class="hljs-keyword">new</span> Octokit({
      auth: process.env.NUXT_GITHUB_TOKEN,
    });
  }

  <span class="hljs-keyword">return</span> _octokit;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> searchGithub = <span class="hljs-keyword">async</span> (
  endpoint: <span class="hljs-built_in">string</span>,
  params: Omit&lt;SearchParams, <span class="hljs-string">'endpoint'</span>&gt;
) =&gt; {
  <span class="hljs-keyword">if</span> (!endpoint || !allowedEndpoints[endpoint <span class="hljs-keyword">as</span> EndpointType]) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">404</span>,
      message: <span class="hljs-string">'Endpoint not supported'</span>,
    });
  }

  <span class="hljs-keyword">const</span> octokit = useOctokit();
  <span class="hljs-keyword">const</span> endpointToUse = allowedEndpoints[endpoint <span class="hljs-keyword">as</span> EndpointType];

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> octokit.request(endpointToUse <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>, params);

    <span class="hljs-keyword">return</span> response.data;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(error);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      message: <span class="hljs-string">'Error searching GitHub'</span>,
    });
  }
};
</code></pre>
<p>The function is a straightforward GitHub API search using the <code>@octokit/rest</code> library. The endpoint and parameters are passed from the <code>tool_call</code>, and we make the request to GitHub’s API, fetching the appropriate data. You can learn more about the GitHub search APIs from the official <a target="_blank" href="https://docs.github.com/en/rest/search/search?apiVersion=2022-11-28">GitHub Docs</a>.</p>
<h3 id="heading-3-generating-the-final-response">3. Generating the Final Response</h3>
<p>Once the GitHub API returns its results, the final step is to package them into a user-friendly response. The AI processes the raw data—whether commits, issues, repos, or users—and generates readable summaries. Here’s the relevant code snippet:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">if</span> (tool_calls) {
  <span class="hljs-comment">// ...</span>

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> toolCall <span class="hljs-keyword">of</span> toolCalls) {
    <span class="hljs-comment">// ...</span>

    messages.push({
      tool_call_id: toolCall.id,
      role: <span class="hljs-string">'tool'</span>,
      content: <span class="hljs-built_in">JSON</span>.stringify(functionResponse),
    });
  }

  <span class="hljs-keyword">const</span> finalResponse = <span class="hljs-keyword">await</span> openai.chat.completions.create({
    model: MODEL,
    messages: messages,
  });

  <span class="hljs-keyword">return</span> finalResponse.choices[<span class="hljs-number">0</span>].message.content;
}

<span class="hljs-comment">// ..</span>
</code></pre>
<p>In the code above, you can see how we take the API response and push it to the messages array, preparing it for the AI to format into a concise response that’s easy for the user to understand.</p>
<h2 id="heading-managing-github-api-rate-limits">Managing GitHub API Rate Limits</h2>
<p>If you’ve used the GitHub API (or any third-party API), you’ll know that most of them come with rate limits. So, how can we minimize GitHub API calls? The answer is simple: we avoid making duplicate calls by caching every GitHub response. If a user requests the same information that another user (or even themselves) asked for earlier, we pull the data from the cache instead of making another API call.</p>
<p>Now, the question is: how do we implement this in Nuxt? It’s actually quite easy, thanks to <a target="_blank" href="https://nitro.unjs.io/guide/cache#cached-functions">Nitro’s Cached Functions</a> (Nitro is an open source framework to build web servers which Nuxt uses internally). Let’s take a look at the revised <code>searchGithub</code> function below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> searchGithub = defineCachedFunction(
  <span class="hljs-keyword">async</span> (
    event: H3Event,
    endpoint: <span class="hljs-built_in">string</span>,
    params: Omit&lt;SearchParams, <span class="hljs-string">'endpoint'</span>&gt;
  ) =&gt; {
    <span class="hljs-keyword">if</span> (!endpoint || !allowedEndpoints[endpoint <span class="hljs-keyword">as</span> EndpointType]) {
      <span class="hljs-keyword">throw</span> createError({
        statusCode: <span class="hljs-number">404</span>,
        message: <span class="hljs-string">'Endpoint not supported'</span>,
      });
    }

    <span class="hljs-keyword">const</span> octokit = useOctokit();
    <span class="hljs-keyword">const</span> endpointToUse = allowedEndpoints[endpoint <span class="hljs-keyword">as</span> EndpointType];

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> octokit.request(endpointToUse <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>, params);

      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(error);
      <span class="hljs-keyword">throw</span> createError({
        statusCode: <span class="hljs-number">500</span>,
        message: <span class="hljs-string">'Error searching GitHub'</span>,
      });
    }
  },
  {
    maxAge: <span class="hljs-number">60</span> * <span class="hljs-number">60</span>, <span class="hljs-comment">// 1 hour</span>
    group: <span class="hljs-string">'github'</span>,
    name: <span class="hljs-string">'search'</span>,
    getKey: <span class="hljs-function">(<span class="hljs-params">
      event: H3Event,
      endpoint: <span class="hljs-built_in">string</span>,
      params: Omit&lt;SearchParams, <span class="hljs-string">'endpoint'</span>&gt;
    </span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> q = params.q.trim().toLowerCase().split(<span class="hljs-string">' '</span>);

      <span class="hljs-keyword">const</span> mainQuery = q.filter(<span class="hljs-function">(<span class="hljs-params">term</span>) =&gt;</span> !term.includes(<span class="hljs-string">':'</span>)).join(<span class="hljs-string">' '</span>);
      <span class="hljs-keyword">const</span> qualifiers = q.filter(<span class="hljs-function">(<span class="hljs-params">term</span>) =&gt;</span> term.includes(<span class="hljs-string">':'</span>)).sort();

      <span class="hljs-keyword">const</span> finalQuery = [...(mainQuery ? [mainQuery] : []), ...qualifiers]
        .join(<span class="hljs-string">' '</span>)
        .replace(<span class="hljs-regexp">/[\s:]/g</span>, <span class="hljs-string">'_'</span>);

      <span class="hljs-keyword">let</span> key = endpoint + <span class="hljs-string">'_q_'</span> + finalQuery;

      <span class="hljs-keyword">if</span> (params.per_page) {
        key += <span class="hljs-string">'_per_page_'</span> + params.per_page;
      }

      <span class="hljs-keyword">if</span> (params.order) {
        key += <span class="hljs-string">'_order_'</span> + params.order;
      }

      <span class="hljs-keyword">if</span> (params.sort) {
        key += <span class="hljs-string">'_sort_'</span> + params.sort;
      }

      <span class="hljs-keyword">return</span> key;
    },
  }
);
</code></pre>
<p>We’ve modified our earlier function to use <code>cachedFunction</code>, and added <code>H3Event</code> (from the <code>/chat</code> API endpoint call) as the first parameter—this is needed because the app is deployed on the edge with Cloudflare (more details <a target="_blank" href="https://nitro.unjs.io/guide/cache#edge-workers">here</a>). The most important part here is how we create a unique cache key. Here’s a breakdown:</p>
<ol>
<li><p><strong>Query Qualifiers</strong>: First, we break down the <code>q</code> parameter into its qualifier pairs (e.g., for finding the first PR of a GitHub user like <code>ra-jeev</code>—yes, that’s me—the <code>q</code> value would be <code>author:ra-jeev type:pr</code>). Sometimes, it may contain the direct query string too, and the code accounts for this.</p>
</li>
<li><p><strong>Sorting Qualifiers</strong>: Next, we sort the qualifiers alphabetically. This is because the AI could create the same query as <code>type:pr author:ra-jeev</code>. We could handle this in the system prompt, but why over-complicate things for the AI?</p>
</li>
<li><p><strong>Creating the Cache Key</strong>: Finally, we combine all the qualifiers and other parameters into a single string, separated by underscores.</p>
</li>
</ol>
<p>We set the cache duration to 1 hour, as seen in the <code>maxAge</code> setting, which means all <code>searchGitHub</code> responses are stored for that time. To use cache in <code>NuxtHub</code> production we’d already enabled <code>cache: true</code> in our <code>nuxt.config.ts</code>.</p>
<p>Now that we’ve tackled rate limiting, it’s time to shift our focus to response streaming. In the next section, we’ll explore how to implement streaming for a more seamless and efficient user experience.</p>
<h2 id="heading-adding-ai-response-streaming">Adding AI Response Streaming</h2>
<p>Enabling AI response streaming is usually straightforward: you pass a parameter when making the API call, and the AI returns the response as a stream. In our <strong>Hub Chat</strong> project, for example, we handled the stream chunks directly client-side, ensuring that responses trickled in smoothly for the user.</p>
<p>But in the case of <strong>Chat GitHub</strong>, there’s an additional complexity—<strong>tool_call</strong> responses. So, we have two approaches to manage:</p>
<ol>
<li><p>Enable streaming only for the final response generation, but this limits us when there’s no tool_call, wasting an opportunity to stream from the start.</p>
</li>
<li><p>Enable stream response for both the OpenAI calls, but this requires us to manage a stream more carefully on the server side (as we need to handle the <code>tool_call</code>, if any, there itself).</p>
</li>
</ol>
<p>I took the seemingly complex path and went ahead with the second approach—<em>”Two roads diverged in a wood, and I—I took the one less traveled by, And that has made all the difference.”</em></p>
<h3 id="heading-enable-streaming-in-openai-calls">Enable Streaming in OpenAI Calls</h3>
<p>Here’s how the OpenAI API call was modified to enable streaming:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handleMessageWithOpenAI = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>* (<span class="hljs-params">
  event: H3Event,
  messages: OpenAI.ChatCompletionMessageParam[],
</span>) </span>{
  <span class="hljs-keyword">const</span> openai = useOpenAI();
  <span class="hljs-keyword">const</span> responseStream = <span class="hljs-keyword">await</span> openai.chat.completions.create({
    model: MODEL,
    messages,
    tools,
    stream: <span class="hljs-literal">true</span>, <span class="hljs-comment">// enable streaming</span>
  });

  <span class="hljs-keyword">const</span> currentToolCalls: OpenAI.ChatCompletionMessageToolCall[] = [];

  <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> chunk <span class="hljs-keyword">of</span> responseStream) {
    <span class="hljs-keyword">const</span> choice = chunk.choices[<span class="hljs-number">0</span>];

    <span class="hljs-comment">// if it is normal text chunk just yield it</span>
    <span class="hljs-keyword">if</span> (choice.delta.content) {
      <span class="hljs-keyword">yield</span> choice.delta.content;
    }

    <span class="hljs-comment">// if the delta contains tool_calls, then collect all chunks</span>
    <span class="hljs-keyword">if</span> (choice.delta.tool_calls) {
      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> toolCall <span class="hljs-keyword">of</span> choice.delta.tool_calls) {
        <span class="hljs-keyword">if</span> (toolCall.index !== <span class="hljs-literal">undefined</span>) {
          <span class="hljs-keyword">if</span> (!currentToolCalls[toolCall.index]) {
            currentToolCalls[toolCall.index] = {
              id: toolCall.id || <span class="hljs-string">''</span>,
              <span class="hljs-keyword">type</span>: <span class="hljs-string">'function'</span> <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>,
              <span class="hljs-function"><span class="hljs-keyword">function</span>: </span>{
                name: toolCall.function?.name || <span class="hljs-string">''</span>,
                <span class="hljs-built_in">arguments</span>: <span class="hljs-string">''</span>,
              },
            };
          }

          <span class="hljs-keyword">if</span> (toolCall.function?.arguments) {
            currentToolCalls[toolCall.index].function.arguments +=
              toolCall.function.arguments;
          }
        }
      }
    }

    <span class="hljs-comment">// once it has returned all chunks with tool_calls, we will</span>
    <span class="hljs-comment">// get the final finish_reason as 'tool_calls'. We can call</span>
    <span class="hljs-comment">// the mentioned tool now</span>
    <span class="hljs-keyword">if</span> (choice.finish_reason === <span class="hljs-string">'tool_calls'</span>) {
      messages.push({
        role: <span class="hljs-string">'assistant'</span>,
        tool_calls: currentToolCalls,
      });

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> toolCall <span class="hljs-keyword">of</span> currentToolCalls) {
        <span class="hljs-keyword">if</span> (toolCall.function.name === <span class="hljs-string">'searchGithub'</span>) {
          <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> functionArgs = <span class="hljs-built_in">JSON</span>.parse(toolCall.function.arguments);
            <span class="hljs-keyword">const</span> toolResult = <span class="hljs-keyword">await</span> searchGithub(
              event,
              functionArgs.endpoint,
              {
                q: functionArgs.q,
                sort: functionArgs.sort,
                order: functionArgs.order,
                per_page: functionArgs.per_page,
              }
            );

            messages.push({
              role: <span class="hljs-string">'tool'</span>,
              tool_call_id: toolCall.id,
              content: <span class="hljs-built_in">JSON</span>.stringify(toolResult),
            });
          } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error parsing tool call arguments:'</span>, error);
            <span class="hljs-keyword">throw</span> error;
          }
        }
      }

      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> finalResponse = <span class="hljs-keyword">await</span> openai.chat.completions.create({
          model: MODEL,
          messages,
          stream: <span class="hljs-literal">true</span>,
        });

        <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> chunk <span class="hljs-keyword">of</span> finalResponse) {
          <span class="hljs-keyword">if</span> (chunk.choices[<span class="hljs-number">0</span>].delta.content) {
            <span class="hljs-keyword">yield</span> chunk.choices[<span class="hljs-number">0</span>].delta.content;
          }
        }
      } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.error(
          <span class="hljs-string">'Error generating final response or saving user query :'</span>,
          error
        );

        <span class="hljs-keyword">throw</span> error;
      }
    }
  }
};
</code></pre>
<p>The revised <code>handleMessageWithOpenAI</code> function works like this:</p>
<ul>
<li><p><strong>Converted it to an AsyncGenerator</strong>: This allows the function to yield data chunks progressively as they are received.</p>
</li>
<li><p><strong>Added</strong> <code>stream: true</code> to both OpenAI API calls: This tells OpenAI to stream the response back to us.</p>
</li>
<li><p><strong>Yielding Response Chunks</strong>: For each chunk of text that we get from the stream, we simply <code>yield</code> it to the caller.</p>
</li>
<li><p><strong>Handling tool_calls</strong>: Since streaming is enabled, the <code>tool_call</code> information also arrives in chunks. We collect these chunks until the OpenAI API signals the completion of this part (<code>finish_reason === 'tool_calls'</code>), and then invoke the <code>searchGitHub</code> function using the parameters provided by the <code>tool_call</code>.</p>
</li>
<li><p><strong>Final Response</strong>: After the GitHub search is done, we <code>yield</code> the response in chunks in the same way.</p>
</li>
</ul>
<h3 id="heading-converting-to-a-readablestream">Converting to a ReadableStream</h3>
<p>To complete the process, the chunks from <code>handleMessageWithOpenAI</code> are converted into a <strong>ReadableStream</strong> format, which is then returned to the client (not shown here). Here’s how that works:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> asyncGeneratorToStream = <span class="hljs-function">(<span class="hljs-params">
  asyncGenerator: AsyncGenerator&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">void</span>, unknown&gt;
</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> cancelled = <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> encoder = <span class="hljs-keyword">new</span> TextEncoder();
  <span class="hljs-keyword">const</span> stream = <span class="hljs-keyword">new</span> ReadableStream({
    <span class="hljs-keyword">async</span> start(controller) {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> value <span class="hljs-keyword">of</span> asyncGenerator) {
          <span class="hljs-keyword">if</span> (cancelled) {
            <span class="hljs-keyword">break</span>;
          }

          controller.enqueue(
            encoder.encode(<span class="hljs-string">`data: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify({ response: value }</span>)}\n\n`</span>)
          );
        }

        <span class="hljs-comment">// Send done to signal end of stream</span>
        controller.enqueue(encoder.encode(<span class="hljs-string">`data: [DONE]\n\n`</span>));

        controller.close();
      } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Error in stream:'</span>, err);

        <span class="hljs-comment">/* eslint-disable @stylistic/operator-linebreak */</span>
        <span class="hljs-keyword">const</span> errorMessage =
          err <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span>
            ? err.message
            : <span class="hljs-string">'An error occurred in the stream'</span>;
        <span class="hljs-comment">/* eslint-enable @stylistic/operator-linebreak */</span>

        controller.enqueue(
          encoder.encode(
            <span class="hljs-string">`event: error\ndata: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify({
              message: errorMessage,
            }</span>)}\n\n`</span>
          )
        );

        controller.close();
      }
    },
    cancel(reason) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Client closed connection. Reason:'</span>, reason);
      cancelled = <span class="hljs-literal">true</span>;
    },
  });

  <span class="hljs-keyword">return</span> stream;
};
</code></pre>
<p>This code transforms the <code>AsyncGenerator</code> we created earlier into a <strong>ReadableStream</strong>. Here’s a breakdown of what’s happening:</p>
<ul>
<li><p><strong>AsyncGenerator</strong>: We pass the AsyncGenerator from <code>handleMessageWithOpenAI</code> as an argument to this function.</p>
</li>
<li><p><strong>Creating a ReadableStream</strong>: Inside the <code>start</code> method of the ReadableStream, we wait for chunks from the AsyncGenerator.</p>
</li>
<li><p><strong>Formatting Chunks</strong>: For each text chunk received, we format it according to the Server-Sent Events (SSE) convention (You can read more about SSE in my <a target="_blank" href="https://rajeev.dev/create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui#heading-consuming-server-sent-events">previous post</a>). Each chunk is wrapped in this format: <code>data: ${JSON.stringify({ response: value })}\n\n</code>.</p>
</li>
<li><p><strong>Encoding the Stream</strong>: Using <code>TextEncoder</code>, we encode the chunks before sending them to the client. This encoding step is crucial, especially in production environments, as it ensures that the client correctly receives the stream in real-time.</p>
</li>
<li><p><strong>Error Handling</strong>: If an error occurs (as we throw errors from the AsyncGenerator), we send an <code>event: error</code> message to the client, signaling that something went wrong, and then close the stream to terminate the connection cleanly.</p>
</li>
</ul>
<p>And then this stream is returned to the client from the API endpoint</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// inside /api/chat event handler</span>
<span class="hljs-keyword">return</span> asyncGeneratorToStream(
  handleMessageWithOpenAI(event, llmMessages)
);
</code></pre>
<h3 id="heading-handling-the-stream-on-the-client-side">Handling the Stream on the Client-Side</h3>
<p>To handle the streamed response, we’ll use a refactored version of the <code>useChat</code> composable, initially created for the Hub Chat project. This time, we’ll extend it to handle error events. Here's the updated code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useChat</span>(<span class="hljs-params">apiBase: <span class="hljs-built_in">string</span>, body: Record&lt;<span class="hljs-built_in">string</span>, unknown&gt;</span>) </span>{
  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>* <span class="hljs-title">chat</span>(<span class="hljs-params"></span>): <span class="hljs-title">AsyncGenerator</span>&lt;<span class="hljs-title">string</span>, <span class="hljs-title">void</span>, <span class="hljs-title">unknown</span>&gt; </span>{
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> $fetch(apiBase, {
        method: <span class="hljs-string">'POST'</span>,
        body,
        responseType: <span class="hljs-string">'stream'</span>,
      });

      <span class="hljs-keyword">let</span> buffer = <span class="hljs-string">''</span>;
      <span class="hljs-keyword">const</span> reader = (response <span class="hljs-keyword">as</span> ReadableStream)
        .pipeThrough(<span class="hljs-keyword">new</span> TextDecoderStream())
        .getReader();

      <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
        <span class="hljs-keyword">const</span> { value, done } = <span class="hljs-keyword">await</span> reader.read();

        <span class="hljs-keyword">if</span> (done) {
          <span class="hljs-keyword">if</span> (buffer.trim()) {
            <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Stream ended with unparsed data:'</span>, buffer);
          }

          <span class="hljs-keyword">return</span>;
        }

        buffer += value;
        <span class="hljs-keyword">const</span> messages = buffer.split(<span class="hljs-string">'\n\n'</span>);
        buffer = messages.pop() || <span class="hljs-string">''</span>;

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> message <span class="hljs-keyword">of</span> messages) {
          <span class="hljs-keyword">const</span> lines = message.split(<span class="hljs-string">'\n'</span>);
          <span class="hljs-keyword">let</span> event = <span class="hljs-string">''</span>;
          <span class="hljs-keyword">let</span> data = <span class="hljs-string">''</span>;

          <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
            <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'event:'</span>)) {
              event = line.slice(<span class="hljs-string">'event:'</span>.length).trim();
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'data:'</span>)) {
              data = line.slice(<span class="hljs-string">'data:'</span>.length).trim();
            }
          }

          <span class="hljs-keyword">if</span> (event === <span class="hljs-string">'error'</span>) {
            <span class="hljs-keyword">const</span> parsedError = <span class="hljs-built_in">JSON</span>.parse(data);
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Stream error:'</span>, parsedError);

            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
              parsedError.message ?? <span class="hljs-string">'Failed to generate response'</span>
            );
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (data) {
            <span class="hljs-keyword">if</span> (data === <span class="hljs-string">'[DONE]'</span>) <span class="hljs-keyword">return</span>;

            <span class="hljs-keyword">try</span> {
              <span class="hljs-keyword">const</span> jsonData = <span class="hljs-built_in">JSON</span>.parse(data);
              <span class="hljs-keyword">if</span> (jsonData.response) {
                <span class="hljs-keyword">yield</span> jsonData.response;
              }
            } <span class="hljs-keyword">catch</span> (parseError) {
              <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Error parsing JSON:'</span>, parseError);
            }
          }
        }
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error sending message:'</span>, error);
      <span class="hljs-keyword">throw</span> error;
    }
  }

  <span class="hljs-keyword">return</span> chat;
}
</code></pre>
<p>Here’s a breakdown of what the code does:</p>
<ul>
<li><p>We use Nuxt’s <code>$fetch</code> to make a <code>POST</code> request to the API endpoint, passing the <code>responseType: 'stream'</code>. This tells the client to expect a streaming response.</p>
</li>
<li><p>Once we receive the <code>ReadableStream</code>, we create a <code>streamReader</code> for it. This allows us to process the chunks one at a time as they arrive. We also pass the chunks through a <code>TextDecoder</code> to convert the raw bytes into readable text.</p>
</li>
<li><p>The stream is in Server-Sent Events (SSE) format, so we parse and handle the <code>event</code> and <code>data</code> parts appropriately. Each data chunk is parsed and returned to the client component for rendering.</p>
</li>
<li><p>The code also listens for and handles any <code>error</code> events that may occur, ensuring a smoother user experience by gracefully handling stream interruptions or API errors.</p>
</li>
</ul>
<p>And this concludes the road less traveled that we took earlier. In the next section, we’ll cover how we can authenticate our users.</p>
<h2 id="heading-user-authentication-with-github-oauth">User Authentication with GitHub OAuth</h2>
<p>Now that our backend is ready to handle client requests, how do we restrict access to authenticated users? And how do we provide context to the AI, like answering a query such as, <em>“When did I make my first ever commit?”</em> To control who can access the backend, we use authentication. And to give the AI context about the user, we rely on GitHub OAuth for authentication.</p>
<p>We’re leveraging <code>nuxt-auth-utils</code>, which simplifies integrating GitHub OAuth into our Nuxt app. This allows us to authenticate users with their GitHub accounts and manage sessions effortlessly.</p>
<h3 id="heading-server-side-implementation">Server-Side Implementation</h3>
<p>On the server side, we need to create a route that handles the GitHub access token when the user logs in. Make sure to create a GitHub OAuth app and add the <code>CLIENT_ID</code> and <code>CLIENT_SECRET</code> to the <code>.env</code> as mentioned in the setup section.</p>
<p>To implement this, create a <code>github.get.ts</code> file in the <code>route/auth</code> folder of the server directory with the following content:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> oauthGitHubEventHandler({
  <span class="hljs-keyword">async</span> onSuccess(event, { user }) {
    <span class="hljs-keyword">await</span> setUserSession(event, {
      user: {
        id: user.id,
        login: user.login,
        name: user.name,
        avatarUrl: user.avatar_url,
        htmlUrl: user.html_url,
        publicRepos: user.public_repos,
      },
    });

    <span class="hljs-keyword">return</span> sendRedirect(event, <span class="hljs-string">'/chat'</span>);
  },
  <span class="hljs-comment">// Optional, will return a json error and 401 status code by default</span>
  onError(event, error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'GitHub OAuth error:'</span>, error);
    <span class="hljs-keyword">return</span> sendRedirect(event, <span class="hljs-string">'/'</span>);
  },
});
</code></pre>
<p>The event handler name is important and should be <code>oauthGitHubEventHandler</code> (more details can be found <a target="_blank" href="https://github.com/atinux/nuxt-auth-utils?tab=readme-ov-file#oauth-event-handlers">here</a>). On successful login, we call the <code>setUserSession</code> utility function to store the user details in an HTTP cookie and redirect them to the chat page.</p>
<p>For our API routes, we can then call the <code>requireUserSession</code> utility to ensure only authenticated users can make requests. Below is the full <code>/api/chat</code> endpoint handler:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> userSession = <span class="hljs-keyword">await</span> requireUserSession(event);

  <span class="hljs-keyword">const</span> { messages } = <span class="hljs-keyword">await</span> readBody(event);
  <span class="hljs-keyword">if</span> (!messages) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">400</span>,
      message: <span class="hljs-string">'User messages are required'</span>,
    });
  }

  <span class="hljs-keyword">const</span> llmMessages = [
    {
      role: <span class="hljs-string">'system'</span>,
      content: getSystemPrompt(userSession.user.login),
    },
    ...messages,
  ];

  <span class="hljs-keyword">return</span> asyncGeneratorToStream(
    handleMessageWithOpenAI(event, llmMessages, userSession.user.login)
  );
});
</code></pre>
<p>As you can see, we retrieve the currently logged-in GitHub user’s details and pass the login info into the system prompt. This gives OpenAI the context it needs to answer queries like, “When did I make my first commit?” The <code>getSystemPrompt</code> utility helps with that:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getSystemPrompt = <span class="hljs-function">(<span class="hljs-params">loggedInUserName: <span class="hljs-built_in">string</span></span>) =&gt;</span>
  systemPrompt +
  <span class="hljs-string">`\n\nNote: The currently logged in github user is "<span class="hljs-subst">${loggedInUserName}</span>".`</span>;
</code></pre>
<h3 id="heading-client-side-handling">Client-Side Handling</h3>
<p>On the client side, we use the built-in <code>AuthState</code> component from <code>nuxt-auth-utils</code> to manage authentication flows, like logging in and checking if a user is signed in.</p>
<p>Here’s how to handle sign-in:</p>
<pre><code class="lang-typescript">&lt;AuthState v-slot=<span class="hljs-string">"{ loggedIn }"</span>&gt;
  &lt;UButton
    v-<span class="hljs-keyword">if</span>=<span class="hljs-string">"loggedIn"</span>
    size=<span class="hljs-string">"lg"</span>
    trailing-icon=<span class="hljs-string">"i-heroicons-arrow-right-16-solid"</span>
    to=<span class="hljs-string">"/chat"</span>
  &gt;
    Go to Chat
  &lt;/UButton&gt;

  &lt;div v-<span class="hljs-keyword">else</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center gap-y-2"</span>&gt;
    &lt;UButton
      size=<span class="hljs-string">"lg"</span>
      icon=<span class="hljs-string">"i-simple-icons-github"</span>
      to=<span class="hljs-string">"/auth/github"</span>
      external
    &gt;
      Sign <span class="hljs-keyword">in</span> <span class="hljs-keyword">with</span> GitHub
    &lt;/UButton&gt;
    &lt;p <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-sm text-gray-600 dark:text-gray-300 text-center"</span>&gt;
      Start Chatting Now!
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/AuthState&gt;
</code></pre>
<p>Notice the <code>external</code> attribute on the Sign-In button. This attribute is essential—it tells the framework to treat the GitHub authentication as an external process. Without it, the framework will try to redirect you to the <code>/auth/github</code> route on the client side, causing errors (It did get me for sure).</p>
<p>This completes the server-side and client-side setup for user authentication. You can go through the shared GitHub repo to see how we also restrict navigation on the client side by using an <code>auth</code> middleware, ensuring that only authenticated users can access the chat page.</p>
<h2 id="heading-bonus-enhancing-engagement">Bonus: Enhancing Engagement</h2>
<p>As the project neared completion, I kept thinking about how to make it more engaging. I wanted to show what users were querying about and who they were querying for. However, I didn’t want to save every type of query—especially those like “When did I make my first commit?”—as they were both pointless and numerous. Instead, I decided to save only the queries made about other users, repositories, and other entities, excluding self-referential queries.</p>
<h3 id="heading-database-setup">Database Setup</h3>
<p>To achieve this, we needed a database, which is why we enabled <code>database: true</code> in our <code>nuxt.config.ts</code> file. This setting binds Cloudflare’s D1 database in production and uses its platform proxy during development.</p>
<p>First, we create the necessary database tables using the <code>hubDatabase</code> server composable:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> hubDatabase().exec(
  <span class="hljs-string">`CREATE TABLE IF NOT EXISTS queries (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    response TEXT NOT NULL,
    github_request TEXT NOT NULL,
    github_response TEXT NOT NULL,
    queried_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`</span>.replace(<span class="hljs-regexp">/\n/g</span>, <span class="hljs-string">''</span>)
);

<span class="hljs-comment">// ... other tables and indexes</span>
</code></pre>
<p>Next, we utilize utility functions to save the queries. The <code>saveUserQuery</code> function is invoked only if a tool call was made from the <code>handleMessageWithOpenAI</code> function we discussed earlier.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ToolCallDetails = {
  <span class="hljs-comment">// eslint-disable-next-line @typescript-eslint/no-explicit-any</span>
  response: <span class="hljs-built_in">any</span>;
  request: SearchParams;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> UserQuery = {
  userMessage: <span class="hljs-built_in">string</span>;
  toolCalls: ToolCallDetails[];
  assistantReply: <span class="hljs-built_in">string</span>;
};

<span class="hljs-keyword">const</span> getAvatarUrl = <span class="hljs-function">(<span class="hljs-params">toolCall: ToolCallDetails</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> avatarUrl;
  <span class="hljs-keyword">const</span> responseItem = toolCall.response.items[<span class="hljs-number">0</span>];

  <span class="hljs-keyword">if</span> (responseItem) {
    <span class="hljs-keyword">if</span> (responseItem.author) {
      avatarUrl = responseItem.author.avatar_url;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (responseItem.user) {
      avatarUrl = responseItem.user.avatar_url;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (responseItem.owner) {
      avatarUrl = responseItem.owner.avatar_url;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (responseItem.avatar_url) {
      avatarUrl = responseItem.avatar_url;
    }
  }

  <span class="hljs-keyword">return</span> avatarUrl;
};

<span class="hljs-keyword">const</span> shouldSaveUserQuery = <span class="hljs-function">(<span class="hljs-params">
  toolCall: ToolCallDetails,
  loggedInUser: <span class="hljs-built_in">string</span>
</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> responseItem = toolCall.response.items[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">if</span> (
    responseItem &amp;&amp;
    ((responseItem.author &amp;&amp; responseItem.author.login === loggedInUser) ||
      (responseItem.user &amp;&amp; responseItem.user.login === loggedInUser) ||
      (responseItem.owner &amp;&amp; responseItem.owner.login === loggedInUser) ||
      responseItem.login === loggedInUser)
  ) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
  }

  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> saveUserQuery = <span class="hljs-keyword">async</span> (
  loggedInUser: <span class="hljs-built_in">string</span>,
  userQuery: UserQuery
) =&gt; {
  <span class="hljs-keyword">const</span> toolCall = userQuery.toolCalls[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">const</span> matchedUser = toolCall.request.q.match(<span class="hljs-regexp">/(?:author:|user:)(\S+)/</span>);
  <span class="hljs-keyword">if</span> (matchedUser) {
    <span class="hljs-keyword">const</span> queriedUser = matchedUser[<span class="hljs-number">1</span>].toLowerCase();
    <span class="hljs-keyword">if</span> (queriedUser !== loggedInUser) {
      <span class="hljs-keyword">const</span> avatarUrl = getAvatarUrl(toolCall);

      <span class="hljs-keyword">await</span> storeQuery(
        userQuery.userMessage,
        userQuery.assistantReply,
        toolCall,
        { login: queriedUser, avatarUrl }
      );
    }
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (shouldSaveUserQuery(toolCall, loggedInUser)) {
    <span class="hljs-keyword">await</span> storeQuery(userQuery.userMessage, userQuery.assistantReply, toolCall);
  }
};
</code></pre>
<p>It checks if the query involves the currently logged-in user. If it does, we simply return without logging anything; otherwise, we store the query details in the database using:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> storeQuery = <span class="hljs-keyword">async</span> (
  queryText: <span class="hljs-built_in">string</span>,
  assistantReply: <span class="hljs-built_in">string</span>,
  toolCall: ToolCallDetails,
  queriedUser?: { login: <span class="hljs-built_in">string</span>; avatarUrl?: <span class="hljs-built_in">string</span> }
) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> db = hubDatabase();

    <span class="hljs-keyword">const</span> queryStmt = db
      .prepare(
        <span class="hljs-string">'INSERT INTO queries (text, response, github_request, github_response) VALUES (?1, ?2, ?3, ?4)'</span>
      )
      .bind(
        queryText,
        assistantReply,
        <span class="hljs-built_in">JSON</span>.stringify(toolCall.request),
        <span class="hljs-built_in">JSON</span>.stringify(toolCall.response)
      );
    <span class="hljs-keyword">if</span> (queriedUser) {
      <span class="hljs-keyword">const</span> [batchRes1, batchRes2] = <span class="hljs-keyword">await</span> db.batch([
        queryStmt,
        db
          .prepare(
            <span class="hljs-string">`INSERT INTO trending_users (username, search_count, last_searched, avatar_url)
              VALUES (?1, 1, CURRENT_TIMESTAMP, ?2)
              ON CONFLICT(username)
              DO UPDATE SET search_count = search_count + 1, last_searched = CURRENT_TIMESTAMP, avatar_url = COALESCE(?2, avatar_url)`</span>
          )
          .bind(queriedUser.login, queriedUser.avatarUrl),
      ]);

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'storeQuery: '</span>, batchRes1, batchRes2);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> queryStmt.run();

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'storeQuery: '</span>, res);
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to store query: '</span>, error);
  }
};
</code></pre>
<h3 id="heading-api-endpoints">API Endpoints</h3>
<p>We then create API endpoints to retrieve the trending users and recent queries, as shown below (note the use of endpoint caching):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getRecentQueries = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> db = hubDatabase();
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'getRecentQueries'</span>);
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> db
    .prepare(
      <span class="hljs-string">'SELECT id, text, response, queried_at FROM queries ORDER BY queried_at DESC LIMIT ?'</span>
    )
    .bind(<span class="hljs-number">10</span>)
    .all&lt;RecentQuery&gt;();

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'getRecentQueries: '</span>, result);
  <span class="hljs-keyword">return</span> result.results;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineCachedEventHandler(
  <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> getRecentQueries();

    <span class="hljs-keyword">return</span> results;
  },
  {
    maxAge: <span class="hljs-number">10</span> * <span class="hljs-number">60</span>, <span class="hljs-comment">// 10 minutes</span>
  }
);
</code></pre>
<h3 id="heading-frontend-integration">Frontend Integration</h3>
<p>This setup allows us to display the data in the frontend, providing users with insights into trending queries and recently searched users, as illustrated in the screenshot below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728036487996/2944e305-f533-4eec-8e7f-02a33daf79a2.png" alt="Chat GitHub home page showing recent queries" class="image--center mx-auto" /></p>
<p>You can go through the shared GitHub repo to see the whole implementation in detail.</p>
<p>And with that, now you can also know what you did last summer—phew!</p>
<h2 id="heading-source-code">Source Code</h2>
<p>The project’s code is open source and can be checked here</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/chat-github">https://github.com/ra-jeev/chat-github</a></div>
<p> </p>
<h2 id="heading-deployment">Deployment</h2>
<p>You can deploy the app using your NuxtHub admin panel or manually through the NuxtHub CLI. For more details on deploying an app through NuxtHub, refer to the <a target="_blank" href="https://hub.nuxt.com/docs/getting-started/deploy">official documentation</a>.</p>
<p>The best part is that this project is now listed as a template on the NuxtHub Templates page. So, if you already have a NuxtHub account, you can deploy this project in one click using the button below (Just remember to add the necessary environment variables in the panel).</p>
<p><a target="_blank" href="https://hub.nuxt.com/new?template=chat-github"><img src="https://hub.nuxt.com/button.svg" alt="One click NuxtHub deploy button" /></a></p>
<h2 id="heading-further-enhancements">Further Enhancements</h2>
<p>Currently, we rely on the AI's ability to generate GitHub API queries from natural language input. While we’ve provided it with details about various qualifiers, it can still struggle with more complex or intricate queries. I also experimented with tool-calling models from <code>Cloudflare’s Workers AI</code> and <code>Groq API</code>, and found that <code>gpt-4o</code> performed better for these tasks. <code>Claude 3.5 Sonnet</code> should definitely offer even better results, but it tends to be more expensive.</p>
<p>Here are a few alternative approaches we could explore to improve query accuracy and results:</p>
<ul>
<li><p><strong>Fine-tune/train a tool-calling model</strong> specifically on the GitHub Search API documentation.</p>
</li>
<li><p><strong>Create embeddings</strong> from the GitHub Search documentation and store them in a vector database. Cloudflare offers a free tier for its vector database now, which we could leverage. When a user query is made, we could retrieve relevant information from the embeddings and include it in the system prompt.</p>
</li>
</ul>
<p>If you have any other ideas for improving this project—or any feedback—please feel free to share them in the comments!</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>And there you have it—a GitHub search tool, powered by OpenAI, wrapped in a chat interface, and ready for action. We’ve gone through setting up the project, streaming responses, authenticating users, and even added a sprinkle of engagement to make things interesting.</p>
<p>While the project has plenty of room for improvements, it’s a solid foundation to build upon. And who knows—maybe with a few tweaks, it’ll be predicting your next GitHub repo before you even think of it :-).</p>
<p>May all your commits be bug-free!</p>
<p>Until next time!</p>
<hr />
<blockquote>
<p><strong><em>Keep adding the bits and soon you'll have a lot of bytes to share with the world.</em></strong></p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Create Your Own Cloudflare Workers AI LLM Playground Using NuxtHub and NuxtUI]]></title><description><![CDATA[You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly evolving, and every day one new tool or another pops up. One such AI off...]]></description><link>https://rajeev.dev/create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui</link><guid isPermaLink="true">https://rajeev.dev/create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui</guid><category><![CDATA[Workers AI]]></category><category><![CDATA[nuxtui]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[nuxthub]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Thu, 29 Aug 2024 07:46:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724870968747/6ae3905c-72e8-4e71-8ce0-b22680856f0d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly evolving, and every day one new tool or another pops up. One such AI offering from <a target="_blank" href="https://hub.nuxt.com/">NuxtHub</a> was launched recently, and I couldn’t resist taking it for a spin. And let’s be honest—some of us just can't resist adding a 'dark mode', something the Cloudflare Workers AI models playground hasn’t embraced yet.</p>
<p>But beyond these practical reasons, there’s something even more satisfying: the joy of creation and learning. So, if you're ready to dive in, maybe you'll pick up some new concepts like Server-Sent Events, response streaming, markdown parsing, and, of course, deploying your own chat playground without any of the usual fuss. So, get comfy—however you like—and let's get started!</p>
<h2 id="heading-project-overview">Project Overview</h2>
<p>Alright, so what exactly are we going to create here? As you might have guessed already, we will be creating a chat interface to talk to different <a target="_blank" href="https://developers.cloudflare.com/workers-ai/models#text-generation">text generation models</a> supported by <a target="_blank" href="https://developers.cloudflare.com/workers-ai/">Cloudflare Workers AI</a>. Below is a brief list of capabilities we will build along the way:</p>
<ul>
<li><p>Ability to set different LLM params, like temperature, max tokens, system prompt, top_p, top_k etc while keeping some of these optional</p>
</li>
<li><p>Ability to turn LLM response streaming on/off</p>
</li>
<li><p>Handle streaming/non-streaming LLM responses on both, the server and the client side</p>
</li>
<li><p>Parsing LLM responses for markdown and display it appropriately</p>
</li>
<li><p>Auto-scrolling the chat container as the response is streamed from the LLM endpoint</p>
</li>
<li><p>Adding the dark mode (this one is trivial but let's add it here for completeness)</p>
</li>
</ul>
<p>This is how the interface will look when we are through this article:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724775792125/4dc571f5-3c82-4496-a75b-6a70d4dbbd0c.png" alt="LLM playground chat interface " class="image--center mx-auto" /></p>
<p>You can try it out live here: <a target="_blank" href="https://hub-chat.nuxt.dev/">https://hub-chat.nuxt.dev/</a></p>
<p>We will cover each of the tasks in detail in the following sections.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Now that we've set the stage for our LLM playground project, let's dive into the technologies we'll be using and get our development environment ready.</p>
<h3 id="heading-technologies-well-use">Technologies we'll use</h3>
<ol>
<li><p><a target="_blank" href="https://nuxt.com/">Nuxt 3</a>: Nuxt 3 is a powerful Vue.js framework that will serve as the foundation of our application.</p>
</li>
<li><p><a target="_blank" href="https://ui.nuxt.com/">Nuxt UI</a>: A Nuxt module that will help us create a sleek and responsive interface.</p>
</li>
<li><p><a target="_blank" href="https://hub.nuxt.com/">NuxtHub</a>: NuxtHub is a deployment and administration platform for Nuxt, powered by Cloudflare. We can use NuxtHub to access different Cloudflare offerings like D1 database, Workers KV, R2 Storage, Workers AI etc. In this project, we'll use it to access the LLMs as well as to deploy our project</p>
</li>
<li><p><a target="_blank" href="https://github.com/nuxt-modules/mdc">Nuxt MDC</a>: For parsing and displaying the chat messages</p>
</li>
</ol>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Apart from the basic prerequisites like Node/Npm, Code Editors, and some VueJs/Nuxt knowledge, you'll need the following to follow along:</p>
<ol>
<li><p><strong>A Cloudflare account:</strong> To use the Workers AI models, as well as to deploy your project on Cloudflare Pages for free. If you don't have it already, you can set it up <a target="_blank" href="https://www.cloudflare.com/">here</a>.</p>
</li>
<li><p><strong>A NuxtHub Admin Account:</strong> NuxtHub admin is a web based dashboard to manage NuxtHub apps. You can create your account <a target="_blank" href="https://admin.hub.nuxt.com/">here</a>.</p>
</li>
</ol>
<h3 id="heading-setting-up-the-project">Setting up the project</h3>
<p>We can either start with a Nuxt (or Nuxt UI) template and add NuxtHub on top of it, or we can use the NuxtHub starter template and add Nuxt UI to it. We'll take the second approach:</p>
<ol>
<li><p>Create a new NuxtHub project</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Init the project and install dependencies</span>
 npx nuxthub init cf-playground

 <span class="hljs-comment"># Change into the created dir</span>
 <span class="hljs-built_in">cd</span> cf-playground
</code></pre>
</li>
<li><p>Add the Nuxt UI module. The below command will install the @nuxt/ui dependency as well as add it as a module in your nuxt config file</p>
<pre><code class="lang-bash"> npx nuxi module add ui
</code></pre>
</li>
<li><p>Similarly, add the Nuxt MDC module</p>
<pre><code class="lang-bash"> npx nuxi module add mdc
</code></pre>
</li>
</ol>
<p>Now we have setup everything that we need for this project. You can try running the project with <code>pnpm dev</code> (or an equivalent command if you're using a different package manager) and visit http://localhost:3000 in your browser. If you've dark mode enabled on your system then you'll notice that dark mode is already up and running in the project (We just need a way to toggle it).</p>
<p>With our development environment set up, we're ready to start building our LLM playground. In the next section, we'll begin implementing our user interface using NuxtUI components.</p>
<h2 id="heading-creating-the-ui-components">Creating the UI Components</h2>
<p>First, we will create a component that will let us set numerical values for LLM settings using either a range slider or an input box. Let's call it <code>RangeInput</code>.</p>
<h3 id="heading-the-rangeinput-component">The RangeInput Component</h3>
<p>We utilize a FormGroup component from NuxtUI to create this component. We put a Range slider in the default slot and an input in the hint slot to achieve the desired outcome. Here is the complete component code</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UFormGroup</span> <span class="hljs-attr">:label</span>=<span class="hljs-string">"label"</span> <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ container: 'mt-2' }"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">hint</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UInput</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"model"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-[72px]"</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"number"</span>
        <span class="hljs-attr">:min</span>=<span class="hljs-string">"min"</span>
        <span class="hljs-attr">:max</span>=<span class="hljs-string">"max"</span>
        <span class="hljs-attr">:step</span>=<span class="hljs-string">"step"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">URange</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"model"</span> <span class="hljs-attr">:min</span>=<span class="hljs-string">"min"</span> <span class="hljs-attr">:max</span>=<span class="hljs-string">"max"</span> <span class="hljs-attr">:step</span>=<span class="hljs-string">"step"</span> <span class="hljs-attr">size</span>=<span class="hljs-string">"sm"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">UFormGroup</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> model = defineModel({ <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>, <span class="hljs-attr">default</span>: <span class="hljs-literal">undefined</span> });

defineProps({
  <span class="hljs-attr">label</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
    <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
  },
  <span class="hljs-attr">min</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-literal">undefined</span>,
  },
  <span class="hljs-attr">max</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-literal">undefined</span>,
  },
  <span class="hljs-attr">step</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">Number</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-literal">undefined</span>,
  },
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Instead of taking the model value as a prop and then emitting the changes manually, we use the <code>defineModel</code> macro to achieve two way binding. If the initial model value is undefined the input box will be empty. This will help in making some of the LLM params optional.</p>
<h3 id="heading-the-llmsettings-component">The LLMSettings Component</h3>
<p>LLMSettings component utilizes many RangeInputs that we created above as most of the settings are numerical values. Apart from RangeInputs we use a textarea for the system prompt, a toggle for enabling/disabling response streaming, a select menu for choosing the LLM model, and an accordion for hiding the optional params.</p>
<p>Here are the relevant parts of the component</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full flex flex-col overflow-hidden"</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Settings Header Code --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">UDivider</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-4 flex-1 space-y-6 overflow-y-auto"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UFormGroup</span> <span class="hljs-attr">label</span>=<span class="hljs-string">"Model"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">USelectMenu</span>
          <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.model"</span>
          <span class="hljs-attr">size</span>=<span class="hljs-string">"md"</span>
          <span class="hljs-attr">:options</span>=<span class="hljs-string">"models"</span>
          <span class="hljs-attr">value-attribute</span>=<span class="hljs-string">"id"</span>
          <span class="hljs-attr">option-attribute</span>=<span class="hljs-string">"name"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UFormGroup</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">RangeInput</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.temperature"</span>
        <span class="hljs-attr">label</span>=<span class="hljs-string">"Temperature"</span>
        <span class="hljs-attr">:min</span>=<span class="hljs-string">"0"</span>
        <span class="hljs-attr">:max</span>=<span class="hljs-string">"5"</span>
        <span class="hljs-attr">:step</span>=<span class="hljs-string">"0.1"</span>
      /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">RangeInput</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.maxTokens"</span>
        <span class="hljs-attr">label</span>=<span class="hljs-string">"Max Tokens"</span>
        <span class="hljs-attr">:min</span>=<span class="hljs-string">"1"</span>
        <span class="hljs-attr">:max</span>=<span class="hljs-string">"4096"</span>
      /&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">UFormGroup</span> <span class="hljs-attr">label</span>=<span class="hljs-string">"System Prompt"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">UTextarea</span>
          <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.systemPrompt"</span>
          <span class="hljs-attr">:rows</span>=<span class="hljs-string">"3"</span>
          <span class="hljs-attr">:maxrows</span>=<span class="hljs-string">"8"</span>
          <span class="hljs-attr">autoresize</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UFormGroup</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-between"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Stream Response<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">UToggle</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.stream"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">UAccordion</span>
        <span class="hljs-attr">:items</span>=<span class="hljs-string">"accordionItems"</span>
        <span class="hljs-attr">color</span>=<span class="hljs-string">"white"</span>
        <span class="hljs-attr">variant</span>=<span class="hljs-string">"solid"</span>
        <span class="hljs-attr">size</span>=<span class="hljs-string">"md"</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">item</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span> <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ body: { base: 'space-y-6', padding: 'p-4 sm:p-4' } }"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">RangeInput</span>
              <span class="hljs-attr">v-model</span>=<span class="hljs-string">"llmParams.topP"</span>
              <span class="hljs-attr">label</span>=<span class="hljs-string">"Top P"</span>
              <span class="hljs-attr">:min</span>=<span class="hljs-string">"0"</span>
              <span class="hljs-attr">:max</span>=<span class="hljs-string">"2"</span>
              <span class="hljs-attr">:step</span>=<span class="hljs-string">"0.1"</span>
            /&gt;</span>

            <span class="hljs-comment">&lt;!-- Other optional params --&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UAccordion</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
type LlmParams = {
  <span class="hljs-attr">model</span>: string;
  temperature: number;
  maxTokens: number;
  topP?: number;
  topK?: number;
  frequencyPenalty?: number;
  presencePenalty?: number;
  systemPrompt: string;
  stream: boolean;
};

<span class="hljs-keyword">const</span> llmParams = defineModel(<span class="hljs-string">'llmParams'</span>, {
  <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span> <span class="hljs-keyword">as</span> () =&gt; LlmParams,
  <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
});

defineEmits([<span class="hljs-string">'hideDrawer'</span>, <span class="hljs-string">'reset'</span>]);

<span class="hljs-keyword">const</span> accordionItems = [
  {
    <span class="hljs-attr">label</span>: <span class="hljs-string">'Advanced Settings'</span>,
    <span class="hljs-attr">defaultOpen</span>: <span class="hljs-literal">false</span>,
  },
];

<span class="hljs-keyword">const</span> models = [
  {
    <span class="hljs-attr">name</span>: <span class="hljs-string">'deepseek-coder-6.7b-base-awq'</span>,
    <span class="hljs-attr">id</span>: <span class="hljs-string">'@hf/thebloke/deepseek-coder-6.7b-base-awq'</span>,
  },
  { 
    <span class="hljs-attr">name</span>: <span class="hljs-string">'llama-3-8b-instruct'</span>, 
    <span class="hljs-attr">id</span>: <span class="hljs-string">'@cf/meta/llama-3-8b-instruct'</span>, 
  },
  <span class="hljs-comment">// ...other models</span>
]
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<h3 id="heading-the-chatpanel-component">The ChatPanel Component</h3>
<p>This is the most important component of all as it handles the core chat functionality of the app. It consists of three parts:</p>
<p><strong>Chat Header</strong></p>
<p>It displays the app name/label apart from some global buttons for clearing chats, dark mode toggling and for showing the settings drawer on mobile devices</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-between p-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center gap-x-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-xl md:text-2xl text-primary font-bold"</span>&gt;</span>Hub Chat<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UTooltip</span> <span class="hljs-attr">text</span>=<span class="hljs-string">"Clear chat"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
          <span class="hljs-attr">color</span>=<span class="hljs-string">"gray"</span>
          <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-heroicons-trash"</span>
          <span class="hljs-attr">size</span>=<span class="hljs-string">"xs"</span>
          <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"clearDisabled"</span>
          @<span class="hljs-attr">click</span>=<span class="hljs-string">"$emit('clear')"</span>
        /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UTooltip</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center gap-x-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ColorMode</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
        <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-heroicons-cog-6-tooth"</span>
        <span class="hljs-attr">color</span>=<span class="hljs-string">"gray"</span>
        <span class="hljs-attr">variant</span>=<span class="hljs-string">"ghost"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"md:hidden"</span>
        @<span class="hljs-attr">click</span>=<span class="hljs-string">"$emit('showDrawer')"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
defineEmits([<span class="hljs-string">'clear'</span>, <span class="hljs-string">'showDrawer'</span>]);

defineProps({
  <span class="hljs-attr">clearDisabled</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-built_in">Boolean</span>,
    <span class="hljs-attr">default</span>: <span class="hljs-literal">true</span>,
  },
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p><strong>Chats Container</strong></p>
<p>For parsing and displaying the chat messages. It also shows a message loading skeleton when streaming is off, as well as a NoChats placeholder when needed.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">"chatContainer"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 overflow-y-auto p-4 space-y-5"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
    <span class="hljs-attr">v-for</span>=<span class="hljs-string">"(message, index) in chatHistory"</span>
    <span class="hljs-attr">:key</span>=<span class="hljs-string">"`message-${index}`"</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-start gap-x-4"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"w-12 h-12 p-2 rounded-full"</span>
      <span class="hljs-attr">:class</span>=<span class="hljs-string">"`${
        message.role === 'user' ? 'bg-primary/20' : 'bg-blue-500/20'
      }`"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UIcon</span>
        <span class="hljs-attr">:name</span>=<span class="hljs-string">"`${
          message.role === 'user'
            ? 'i-mdi-user'
            : 'i-heroicons-sparkles-solid'
        }`"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-8 h-8"</span>
        <span class="hljs-attr">:class</span>=<span class="hljs-string">"`${
          message.role === 'user' ? 'text-primary-400' : 'text-blue-400'
        }`"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"message.role === 'user'"</span>&gt;</span>
      {{ message.content }}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">AssistantMessage</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">:content</span>=<span class="hljs-string">"message.content"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">ChatLoadingSkeleton</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"loading === 'message'"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">NoChats</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"chatHistory.length === 0"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>To keep the user informed of the progress, we show a message loading skeleton when streaming is off. To do this we utilize a loading variable which can have three states: <code>idle</code>, <code>message</code> &amp; <code>stream</code>. So when a non-streaming request is made we set the ref to message and the loading skeleton is shown.</p>
<p><strong>User Message Textbox</strong></p>
<p>For entering user message. It shows up as a single line textarea that resizes automatically when needed.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-start p-3.5 relative"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">UTextarea</span>
    <span class="hljs-attr">v-model</span>=<span class="hljs-string">"userMessage"</span>
    <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"How can I help you today?"</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full"</span>
    <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ padding: { xl: 'pr-11' } }"</span>
    <span class="hljs-attr">:rows</span>=<span class="hljs-string">"1"</span>
    <span class="hljs-attr">:maxrows</span>=<span class="hljs-string">"5"</span>
    <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"loading !== 'idle'"</span>
    <span class="hljs-attr">autoresize</span>
    <span class="hljs-attr">size</span>=<span class="hljs-string">"xl"</span>
    @<span class="hljs-attr">keydown.enter.exact.prevent</span>=<span class="hljs-string">"sendMessage"</span>
    @<span class="hljs-attr">keydown.enter.shift.exact.prevent</span>=<span class="hljs-string">"userMessage += '\n'"</span>
  /&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
    <span class="hljs-attr">icon</span>=<span class="hljs-string">"i-heroicons-arrow-up-20-solid"</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute top-5 right-5"</span>
    <span class="hljs-attr">:disabled</span>=<span class="hljs-string">"loading !== 'idle'"</span>
    @<span class="hljs-attr">click</span>=<span class="hljs-string">"sendMessage"</span>
  /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>For allowing the user to send message on hitting enter, we use the keydown event listener with the needed modifiers (<code>@keydown.enter.exact.prevent</code>). Similarly, for adding a newline we use <code>enter + shift</code> keys.</p>
<p>Now that we have our UI components in place, let's shift our focus to the backend. We'll set up the AI integration and create an API endpoint to handle our chat requests. This will bridge the gap between our frontend interface and the AI model.</p>
<h2 id="heading-setting-up-ai-and-api-endpoint">Setting up AI and API Endpoint</h2>
<p>Integrating AI into our project is very simple thanks to NuxtHub. Let's look at our <code>nuxt.config.ts</code> file in the root dir of the project.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// https://nuxt.com/docs/api/configuration/nuxt-config</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineNuxtConfig({
  compatibilityDate: <span class="hljs-string">'2024-07-30'</span>,
  <span class="hljs-comment">// https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-4</span>
  future: { compatibilityVersion: <span class="hljs-number">4</span> },

  <span class="hljs-comment">// https://nuxt.com/modules</span>
  modules: [<span class="hljs-string">'@nuxthub/core'</span>, <span class="hljs-string">'@nuxt/eslint'</span>, <span class="hljs-string">'@nuxtjs/mdc'</span>, <span class="hljs-string">"@nuxt/ui"</span>],

  <span class="hljs-comment">// https://hub.nuxt.com/docs/getting-started/installation#options</span>
  hub: {},

  <span class="hljs-comment">// Env variables - https://nuxt.com/docs/getting-started/configuration#environment-variables-and-private-tokens</span>
  runtimeConfig: {
    <span class="hljs-keyword">public</span>: {
      <span class="hljs-comment">// Can be overridden by NUXT_PUBLIC_HELLO_TEXT environment variable</span>
      helloText: <span class="hljs-string">'Hello from the Edge 👋'</span>,
    },
  },

  <span class="hljs-comment">// https://eslint.nuxt.com</span>
  eslint: {
    config: {
      stylistic: {
        quotes: <span class="hljs-string">'single'</span>,
      },
    },
  },

  <span class="hljs-comment">// https://devtools.nuxt.com</span>
  devtools: { enabled: <span class="hljs-literal">true</span> },
});
</code></pre>
<p>To enable AI, we just need to add the <code>ai: true</code> flag under hub config options above. If needed, we can enable other NuxtHub Cloudflare integrations like <code>D1 database</code> (<code>database: true</code>), <code>Workers KV</code> (<code>kv: true</code>) etc. While we are here, we can remove runtimeConfig as we don't need it. (You can also remove/modify the <code>eslint</code> config as per your code editor settings).</p>
<p>If we want to use AI in dev mode (which we definitely do), we need to link this project to a Cloudflare project. This is needed as we will be talking directly to the Cloudflare APIs as no workers are running yet (and also because the AI usage will be linked to your Cloudflare account). This is a straight forward task, just run the following command</p>
<pre><code class="lang-bash">npx nuxthub link
</code></pre>
<p>Running the link command will make sure that we are logged into to our NuxtHub account, and will allow us to create a new NuxtHub project (backed by Cloudflare) or choose an existing one.</p>
<h3 id="heading-creating-chat-api-endpoint">Creating Chat API Endpoint</h3>
<p>Let's create a chat api endpoint. Create a new file <code>chat.post.ts</code> (<code>"post"</code> in the file name signifies that this endpoint will only accept <code>HTTP POST</code> requests) in the <code>server/api</code> directory and add the following code to it</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-keyword">const</span> { messages, params } = <span class="hljs-keyword">await</span> readBody(event);
  <span class="hljs-keyword">if</span> (!messages || messages.length === <span class="hljs-number">0</span> || !params) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">400</span>,
      statusMessage: <span class="hljs-string">'Missing messages or LLM params'</span>,
    });
  }

  <span class="hljs-keyword">const</span> config = {
    max_tokens: params.maxTokens,
    temperature: params.temperature,
    top_p: params.topP,
    top_k: params.topK,
    frequency_penalty: params.frequencyPenalty,
    presence_penalty: params.presencePenalty,
    stream: params.stream,
  };

  <span class="hljs-keyword">const</span> ai = hubAI();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> ai.run(params.model, {
      messages: params.systemPrompt
        ? [{ role: <span class="hljs-string">'system'</span>, content: params.systemPrompt }, ...messages]
        : messages,
      ...config,
    });

    <span class="hljs-keyword">return</span> params.stream
      ? sendStream(event, result <span class="hljs-keyword">as</span> ReadableStream)
      : (
          result <span class="hljs-keyword">as</span> {
            response: <span class="hljs-built_in">string</span>;
          }
        ).response;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(error);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      statusMessage: <span class="hljs-string">'Error processing request'</span>,
    });
  }
});
</code></pre>
<p><code>hubAI</code> is a server composable that returns a Workers AI client for interacting with the LLMs. We pass the messages as well as the LLM params that we received from the frontend and run the requested model.</p>
<p>If you look at the above code carefully you'll notice that this endpoint supports both: streaming and non-streaming responses. To return a stream from the endpoint we just need to call the <code>sendStream</code> utility function.</p>
<p>We are through with our server side code. Let's look at how to handle the stream response on the client side in the next section.</p>
<h2 id="heading-consuming-server-sent-events">Consuming Server Sent Events</h2>
<p>Before diving into how to handle stream responses, let's understand why streaming is crucial for LLM interactions and how Server Sent Events (SSE) facilitate this process.</p>
<h3 id="heading-why-stream-llm-responses-with-server-sent-events">Why Stream LLM Responses with Server Sent Events?</h3>
<p>Large Language Models (LLMs) can take considerable time to generate complete responses, especially for complex queries. Traditionally, web applications wait for the entire response before displaying anything to the user. However, this approach can lead to long waiting times and a less engaging user experience.</p>
<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">Server Sent Events (SSE)</a> offer a solution to this problem.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">SSE is a technology that allows a server to push data to a web page at any time, enabling real-time updates without the need for the client to constantly request new information.</div>
</div>

<p>Here's how SSE benefits LLM responses:</p>
<ol>
<li><p>Immediate Feedback: As soon as the LLM starts generating content, it can be sent to the client and displayed, providing immediate feedback to the user.</p>
</li>
<li><p>Improved Perceived Performance: Users see content appearing progressively, giving the impression of a faster, more responsive system.</p>
</li>
<li><p>Real-time Interaction: The gradual appearance of text mimics human typing, creating a more natural and engaging conversational experience.</p>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Think of it like ordering at a restaurant: Instead of waiting for all dishes to be prepared before serving, SSE allows the "kitchen" (server) to send out each "dish" (piece of the response) as soon as it's ready. This way, you start "eating" (reading and processing) sooner, and the overall experience feels faster and more satisfying.</div>
</div>

<p>Technically, SSE works by establishing a unidirectional channel from the server to the client. The server sends text data encoded in UTF-8, separated by newline characters. This simple yet effective mechanism allows for real-time updates in web applications, making it ideal for streaming LLM responses.</p>
<p>Now that we understand the importance of streaming for LLM responses and how SSE enables this, let's look at how to implement this in our Nuxt 3 application.</p>
<h3 id="heading-handling-server-sent-events-with-nuxt-3-post-requests">Handling Server Sent Events with Nuxt 3 POST Requests</h3>
<p>Since ours is a POST request, we need handle it differently. <a target="_blank" href="https://nuxt.com/docs/getting-started/data-fetching#consuming-sse-server-sent-events-via-post-request">Nuxt 3 docs</a> gives you an excellent starting point to do this. Reproducing the code from the docs</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Make a POST request to the SSE endpoint</span>
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> $fetch&lt;ReadableStream&gt;(<span class="hljs-string">'/chats/ask-ai'</span>, {
  method: <span class="hljs-string">'POST'</span>,
  body: {
    query: <span class="hljs-string">"Hello AI, how are you?"</span>,
  },
  responseType: <span class="hljs-string">'stream'</span>,
})

<span class="hljs-comment">// Create a new ReadableStream from the response with TextDecoderStream to get the data as text</span>
<span class="hljs-keyword">const</span> reader = response.pipeThrough(<span class="hljs-keyword">new</span> TextDecoderStream()).getReader()

<span class="hljs-comment">// Read the chunk of data as we get it</span>
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
  <span class="hljs-keyword">const</span> { value, done } = <span class="hljs-keyword">await</span> reader.read()

  <span class="hljs-keyword">if</span> (done)
    <span class="hljs-keyword">break</span>

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Received:'</span>, value)
}
</code></pre>
<p>We need to set the request <code>responseType</code> flag as <code>stream</code>, and set the type of <code>$fetch</code> response as <code>ReadableStream</code>. Then we create a stream reader while decoding the received chunks by piping the events through a <code>TextDecoder</code>.</p>
<p>Below is a glimpse of the events data received from our chat endpoint (sent by the LLM)</p>
<pre><code class="lang-plaintext">data: {"response":"Hello","p":"abcdefghijklmnopqrstuvwxyz0123456789abcdefghij"}

data: {"response":"!","p":"abcdefgh"}

data: [DONE]
</code></pre>
<p>These events may contain more than one data line per event, and some events may not contain a complete JSON object (rest of the object will arrive with the next event). To handle this stream we can create a composable with a <code>streamResponse</code> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*">generator function</a> as shown below</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useChat</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>* <span class="hljs-title">streamResponse</span>(<span class="hljs-params">
    url: <span class="hljs-built_in">string</span>,
    messages: ChatMessage[],
    llmParams: LlmParams
  </span>) </span>{
    <span class="hljs-keyword">let</span> buffer = <span class="hljs-string">''</span>;

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> $fetch&lt;ReadableStream&gt;(url, {
        method: <span class="hljs-string">'POST'</span>,
        body: {
          messages,
          params: llmParams,
        },
        responseType: <span class="hljs-string">'stream'</span>,
      });

      <span class="hljs-keyword">const</span> reader = response.pipeThrough(<span class="hljs-keyword">new</span> TextDecoderStream()).getReader();

      <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
        <span class="hljs-keyword">const</span> { value, done } = <span class="hljs-keyword">await</span> reader.read();

        <span class="hljs-keyword">if</span> (done) {
          <span class="hljs-keyword">if</span> (buffer.trim()) {
            <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Stream ended with unparsed data:'</span>, buffer);
          }

          <span class="hljs-keyword">return</span>;
        }

        buffer += value;
        <span class="hljs-keyword">const</span> lines = buffer.split(<span class="hljs-string">'\n'</span>);
        buffer = lines.pop() || <span class="hljs-string">''</span>;

        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
          <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'data: '</span>)) {
            <span class="hljs-keyword">const</span> data = line.slice(<span class="hljs-string">'data: '</span>.length).trim();
            <span class="hljs-keyword">if</span> (data === <span class="hljs-string">'[DONE]'</span>) {
              <span class="hljs-keyword">return</span>;
            }

            <span class="hljs-keyword">try</span> {
              <span class="hljs-keyword">const</span> jsonData = <span class="hljs-built_in">JSON</span>.parse(data);
              <span class="hljs-keyword">if</span> (jsonData.response) {
                <span class="hljs-keyword">yield</span> jsonData.response;
              }
            } <span class="hljs-keyword">catch</span> (parseError) {
              <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Error parsing JSON:'</span>, parseError);
            }
          }
        }
      }
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error sending message:'</span>, error);

      <span class="hljs-keyword">throw</span> error;
    }
  }

  <span class="hljs-comment">// For handling non-streaming responses</span>
  <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getResponse</span>(<span class="hljs-params"></span>) </span>{}

  <span class="hljs-keyword">return</span> {
    getResponse,
    streamResponse,
  };
}
</code></pre>
<p>To handle non streaming responses we can also create a simple <code>$fetch</code> call wrapper function in the same composable (check out the code in the linked Github Repo).</p>
<p>Now that we understand how to consume Server Sent Events and handle streaming responses, let's put all the pieces together in our main chat interface. We'll integrate the components we've built, set up the chat API call, and use our <code>useChat</code> composable to manage the LLM responses.</p>
<h3 id="heading-final-chat-interface-page">Final Chat Interface Page</h3>
<p>The below is the final code of the index page (our app has only one page). Here we use all the components that we created before, and call the chat api endpoint. Then we use the <code>useChat</code> composable to handle the LLM responses.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-screen flex flex-col md:flex-row"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">USlideover</span>
      <span class="hljs-attr">v-model</span>=<span class="hljs-string">"isDrawerOpen"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"md:hidden"</span>
      <span class="hljs-attr">:ui</span>=<span class="hljs-string">"{ width: 'max-w-xs' }"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">LlmSettings</span>
        <span class="hljs-attr">v-model:llmParams</span>=<span class="hljs-string">"llmParams"</span>
        @<span class="hljs-attr">hide-drawer</span>=<span class="hljs-string">"isDrawerOpen = false"</span>
        @<span class="hljs-attr">reset</span>=<span class="hljs-string">"resetSettings"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">USlideover</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden md:block md:w-1/3 lg:w-1/4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">LlmSettings</span> <span class="hljs-attr">v-model:llmParams</span>=<span class="hljs-string">"llmParams"</span> @<span class="hljs-attr">reset</span>=<span class="hljs-string">"resetSettings"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">UDivider</span> <span class="hljs-attr">orientation</span>=<span class="hljs-string">"vertical"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden md:block"</span> /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-grow md:w-2/3 lg:w-3/4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ChatPanel</span>
        <span class="hljs-attr">:chat-history</span>=<span class="hljs-string">"chatHistory"</span>
        <span class="hljs-attr">:loading</span>=<span class="hljs-string">"loading"</span>
        @<span class="hljs-attr">clear</span>=<span class="hljs-string">"chatHistory = []"</span>
        @<span class="hljs-attr">message</span>=<span class="hljs-string">"sendMessage"</span>
        @<span class="hljs-attr">show-drawer</span>=<span class="hljs-string">"isDrawerOpen = true"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> type { ChatMessage, LlmParams, LoadingType } <span class="hljs-keyword">from</span> <span class="hljs-string">'~~/types'</span>;

<span class="hljs-keyword">const</span> isDrawerOpen = ref(<span class="hljs-literal">false</span>);

<span class="hljs-keyword">const</span> defaultSettings: LlmParams = {
  <span class="hljs-attr">model</span>: <span class="hljs-string">'@cf/meta/llama-3.1-8b-instruct'</span>,
  <span class="hljs-attr">temperature</span>: <span class="hljs-number">0.6</span>,
  <span class="hljs-attr">maxTokens</span>: <span class="hljs-number">512</span>,
  <span class="hljs-attr">systemPrompt</span>: <span class="hljs-string">'You are a helpful assistant.'</span>,
  <span class="hljs-attr">stream</span>: <span class="hljs-literal">true</span>,
};

<span class="hljs-keyword">const</span> llmParams = reactive&lt;LlmParams&gt;({ ...defaultSettings });
<span class="hljs-keyword">const</span> resetSettings = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">Object</span>.assign(llmParams, defaultSettings);
};

<span class="hljs-keyword">const</span> { getResponse, streamResponse } = useChat();
<span class="hljs-keyword">const</span> chatHistory = ref&lt;ChatMessage[]&gt;([]);
<span class="hljs-keyword">const</span> loading = ref&lt;LoadingType&gt;(<span class="hljs-string">'idle'</span>);
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendMessage</span>(<span class="hljs-params">message: string</span>) </span>{
  chatHistory.value.push({ <span class="hljs-attr">role</span>: <span class="hljs-string">'user'</span>, <span class="hljs-attr">content</span>: message });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">if</span> (llmParams.stream) {
      loading.value = <span class="hljs-string">'stream'</span>;
      <span class="hljs-keyword">const</span> messageGenerator = streamResponse(
        <span class="hljs-string">'/api/chat'</span>,
        chatHistory.value,
        llmParams
      );

      <span class="hljs-keyword">let</span> responseAdded = <span class="hljs-literal">false</span>;
      <span class="hljs-keyword">for</span> <span class="hljs-keyword">await</span> (<span class="hljs-keyword">const</span> chunk <span class="hljs-keyword">of</span> messageGenerator) {
        <span class="hljs-keyword">if</span> (responseAdded) {
          <span class="hljs-comment">// add the response to the current message</span>
          chatHistory.value[chatHistory.value.length - <span class="hljs-number">1</span>]!.content += chunk;
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-comment">// add a new message to the chat history</span>
          chatHistory.value.push({
            <span class="hljs-attr">role</span>: <span class="hljs-string">'assistant'</span>,
            <span class="hljs-attr">content</span>: chunk,
          });

          responseAdded = <span class="hljs-literal">true</span>;
        }
      }
    } <span class="hljs-keyword">else</span> {
      loading.value = <span class="hljs-string">'message'</span>;
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> getResponse(
        <span class="hljs-string">'/api/chat'</span>,
        chatHistory.value,
        llmParams
      );

      chatHistory.value.push({ <span class="hljs-attr">role</span>: <span class="hljs-string">'assistant'</span>, <span class="hljs-attr">content</span>: response });
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error sending message:'</span>, error);
  } <span class="hljs-keyword">finally</span> {
    loading.value = <span class="hljs-string">'idle'</span>;
  }
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This completes bulk of the coding. The only things remaining are:</p>
<ol>
<li><p>Response parsing for markdown and display</p>
</li>
<li><p>Auto scrolling the chat container</p>
</li>
</ol>
<p>Let's tackle these in the next section.</p>
<h2 id="heading-polishing-the-chat-interface">Polishing the Chat Interface</h2>
<p>We will finish the remaining items in our task list now and call it a day. Stay with me, it won't take long now.</p>
<h3 id="heading-using-nuxt-mdc-to-parse-amp-display-messages">Using Nuxt MDC to Parse &amp; Display Messages</h3>
<p>If you look at the <code>Chats Container</code> code in one of the previous sections you'll notice a component <code>AssistantMessage</code> for displaying the response. Reproducing the relevant code here</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"message.role === 'user'"</span>&gt;</span>
  {{ message.content }}
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">AssistantMessage</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">:content</span>=<span class="hljs-string">"message.content"</span> /&gt;</span>
</code></pre>
<p>We use the <code>parseMarkdown</code> utility function from the <code>MDC module</code> that we included earlier to parse the content, and then use the <code>MDCRenderer</code> component from the same module for displaying it. To take care of streaming response we add a <code>watch</code> for the message content and redo the parsing with <code>useAsyncData</code> composable.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">MDCRenderer</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-1 prose dark:prose-invert"</span> <span class="hljs-attr">:body</span>=<span class="hljs-string">"ast?.body"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { parseMarkdown } <span class="hljs-keyword">from</span> <span class="hljs-string">'#imports'</span>;

<span class="hljs-keyword">const</span> props = defineProps&lt;{
  <span class="hljs-attr">content</span>: string;
}&gt;();

<span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: ast, refresh } = <span class="hljs-keyword">await</span> useAsyncData(useId(), <span class="hljs-function">() =&gt;</span>
  parseMarkdown(props.content)
);

watch(
  <span class="hljs-function">() =&gt;</span> props.content,
  <span class="hljs-function">() =&gt;</span> {
    refresh();
  }
);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">We could have used the <code>&lt;MDC&gt;</code> component instead of using <code>parseMarkdown + &lt;MDCRenderer&gt;</code> combination. <code>&lt;MDC&gt;</code> handles the parsing internally using the same <code>parseMarkdown</code> function, then why we didn't use it?</div>
</div>

<p>The <code>&lt;MDC&gt;</code> component was causing overwriting of previous messages that were similar (the start of the message) to the latest message from the API endpoint. This happens because we don't have a unique key in our messages. Here is the relevant code from the <code>&lt;MDC&gt;</code> component.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> key = computed(<span class="hljs-function">() =&gt;</span> hash(props.value))

<span class="hljs-keyword">const</span> { data, refresh, error } = <span class="hljs-keyword">await</span> useAsyncData(key.value, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> props.value !== <span class="hljs-string">'string'</span>) {
    <span class="hljs-keyword">return</span> props.value
  }
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> parseMarkdown(props.value, props.parserOptions)
})
</code></pre>
<p>As you can see, the key for <code>useAsyncData</code> is generated based on the content props value. So if the server streaming responses start with the same letters (e.g., Here's a joke..., Here's another...), the key will be same, and all similar previous responses from the server gets overwritten in the UI.</p>
<p>In the <code>AssistantMessage</code> component we are using <code>useId</code> composable to generate a unique id for each <code>useAsyncData</code> call, so the issue doesn't occur.</p>
<h3 id="heading-using-mutationobserver-to-auto-scroll-the-chats-container">Using MutationObserver to Auto Scroll the Chats Container</h3>
<p>In the ChatPanel component, the only scrolling part is the chats container. There can be other ways to observe the changes in the container but the simplest one I found is a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver"><code>MutationObserver</code></a>.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">The <code>MutationObserver</code> interface provides the ability to watch for changes being made to the DOM tree.</div>
</div>

<p>In the case of streaming, we keep appending new content to the latest message that is being displayed. So to handle this effectively, we create a MutationObserver, give it a target to watch (the ChatsContainer), and some criteria to watch for (<code>childList, subtree &amp; characterData</code>).</p>
<p>Here is the relevant code to do this</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> chatContainer = ref&lt;HTMLElement | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
<span class="hljs-keyword">let</span> observer: MutationObserver | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;

onMounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (chatContainer.value) {
    observer = <span class="hljs-keyword">new</span> MutationObserver(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">if</span> (chatContainer.value) {
        chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
      }
    });

    observer.observe(chatContainer.value, {
      childList: <span class="hljs-literal">true</span>,
      subtree: <span class="hljs-literal">true</span>,
      characterData: <span class="hljs-literal">true</span>,
    });
  }
});

onUnmounted(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (observer) {
    observer.disconnect();
  }
});
</code></pre>
<p>When the ChatsPanel gets mounted we create a new MutationObserver which when based on the given criteria, we make the chats container scroll to its fullest.</p>
<h3 id="heading-bonus-handling-the-dark-mode">Bonus: Handling the Dark Mode</h3>
<p>As previously mentioned, the dark mode is already working; we just need a way to toggle it. We can also change the flavor of gray we want in the app. This can be done by adding an <code>app.config.ts</code> file in the app directory.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app.config.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineAppConfig({
  ui: {
    primary: <span class="hljs-string">'orange'</span>,
    gray: <span class="hljs-string">'slate'</span>,
  },
});
</code></pre>
<p>Add the following in your <code>app.vue</code> file for setting the required background colors.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
useHead({
  <span class="hljs-attr">bodyAttrs</span>: {
    <span class="hljs-attr">class</span>: <span class="hljs-string">'bg-white dark:bg-gray-900'</span>,
  },
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>And add a new <code>ColorMode</code> component in your <code>app/components</code> directory.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">ClientOnly</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span>
      <span class="hljs-attr">:icon</span>=<span class="hljs-string">"isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'"</span>
      <span class="hljs-attr">color</span>=<span class="hljs-string">"gray"</span>
      <span class="hljs-attr">variant</span>=<span class="hljs-string">"ghost"</span>
      <span class="hljs-attr">aria-label</span>=<span class="hljs-string">"Theme"</span>
      @<span class="hljs-attr">click</span>=<span class="hljs-string">"isDark = !isDark"</span>
    /&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">template</span> #<span class="hljs-attr">fallback</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-8 h-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">ClientOnly</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"ts"</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> colorMode = useColorMode();

<span class="hljs-keyword">const</span> isDark = computed({
  get() {
    <span class="hljs-keyword">return</span> colorMode.value === <span class="hljs-string">'dark'</span>;
  },
  set() {
    colorMode.preference = colorMode.value === <span class="hljs-string">'dark'</span> ? <span class="hljs-string">'light'</span> : <span class="hljs-string">'dark'</span>;
  },
});
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Now you can use this color mode toggle button anywhere you like. In our app we've added it in the <code>ChatHeader</code> component.</p>
<p>Phew! And we have completed all the tasks we had set out to complete at the beginning of this article.</p>
<h2 id="heading-deploying-the-app">Deploying the App</h2>
<p>You can deploy the project in multiple ways. I would recommend to do it through the NuxtHub Admin console. Push your code to a Github repository, link this repository with NuxtHub and then deploy from the Admin console.</p>
<p>But if you want to see it live right away then you can use the below command</p>
<pre><code class="lang-bash">npx nuxthub deploy
</code></pre>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">If you do your first deployment with the NuxtHub CLI, you won't be able to attach your GitHub/GitLab repository later on due to a Cloudflare limitation.</div>
</div>

<p>For more details on deployment you can visit the <a target="_blank" href="https://hub.nuxt.com/docs/getting-started/deploy">NuxtHub Docs</a>.</p>
<h2 id="heading-source-code">Source Code</h2>
<p>I only covered the most important parts of the source code here. You can visit the linked GIthub Repo for the complete source code. It should be self explanatory, but if you have question then please feel free to drop a comment below.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/hub-chat/">https://github.com/ra-jeev/hub-chat/</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You've successfully built a feature-rich LLM playground using Nuxt 3, NuxtUI, and Cloudflare Workers AI through NuxtHub. We've covered a wide range of topics, including:</p>
<ul>
<li><p>Handling streaming responses using Server Sent Events</p>
</li>
<li><p>Parsing and displaying markdown content in chat messages</p>
</li>
<li><p>Implementing auto-scrolling for a better user experience etc.</p>
</li>
</ul>
<p>You can take this project as the starting point and improve it further by:</p>
<ul>
<li><p>Adding the ability to talks to other types of LLMs, e.g. text-to-image, image-to-text, speech recognition etc.</p>
</li>
<li><p>Implementing user authentication and session management</p>
</li>
<li><p>Adding support for multiple conversation threads etc.</p>
</li>
</ul>
<p>Thank you for staying till the end. I hope you learned some new concepts from this article. Do let me know your learnings in the comments section. Your feedback and experiences are valuable not just to me, but to the entire community of developers exploring this fascinating field.</p>
<p>Until next time!</p>
<hr />
<blockquote>
<p><em>Keep adding the bits and soon you'll have a lot of bytes to share with the world</em>.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Beyond Gut Feeling: Leveraging AI for Smarter Domain Name Decisions]]></title><description><![CDATA[If you work in tech-related fields, sooner or later there will come a time when you have to pick a domain name for the idea you've been working on. Whether it's an app or a service, it still needs a name. And if you're anything like me, this is one o...]]></description><link>https://rajeev.dev/leveraging-ai-for-smarter-domain-name-decisions</link><guid isPermaLink="true">https://rajeev.dev/leveraging-ai-for-smarter-domain-name-decisions</guid><category><![CDATA[AIForTomorrow]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[#anthropic]]></category><category><![CDATA[claude.ai]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sun, 28 Jul 2024 19:32:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722194509989/894ac1a2-3eb8-423f-99fb-5afa1c592581.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you work in tech-related fields, sooner or later there will come a time when you have to pick a domain name for the idea you've been working on. Whether it's an app or a service, it still needs a name. And if you're anything like me, this is one of the trickiest parts of building that idea. The perfect domain name can make your project more memorable, boost SEO, and even influence user trust. But how do you know if you're making the right choice?</p>
<p>Traditionally, selecting a domain name has been a mix of creativity, gut feeling, and perhaps a quick poll among people in your circle of influence. The post-GPT era might have added another ingredient to the mix: consulting your trusted AI/LLM. But if you have talked to this advisor, it generally throws some names at you without revealing why it chose these names. And this is where <strong>"Name Insights"</strong> steps in, offering not just suggestions, but insights into the 'why' behind those domain name choices.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>I have been thinking about domain names (hoarding to be frank) for quite some time now. Maybe because I have bought more domains in the recent past than my usual appetite allows me to. And the first step to this whole process is: knowing the name that you want to buy.</p>
<p>After the usual availability checks, you wonder whether that weird-looking name is right for the purpose you're buying it for. Let's be honest, all the good ones are already taken (why does it feel like I'm not writing a tech article?) or the price is simply beyond your reach. Sometimes you zero down on more than one name, so which one should you go ahead with? These were the guiding questions behind building Name Insights.</p>
<p>Name Insights aims to address these challenges by providing detailed, contextual answers to help you make informed decisions about domain names.</p>
<h2 id="heading-app-features">App Features</h2>
<p>Name Insights offers three powerful services to assist in your domain name decision-making process. Each service utilizes an AI-driven scoring system that evaluates domains based on six key aspects of what makes a good domain name. Let's explore each feature:</p>
<h3 id="heading-name-insights-amp-scoring">Name Insights &amp; Scoring</h3>
<p>This feature provides an unbiased view of what kind of app or service is best suited for a given domain name. It:</p>
<ul>
<li><p>Evaluates the domain on 6 different parameters, namely:</p>
<ol>
<li><p>Brand Impact: Memorability, brandability, uniqueness, emotional appeal.</p>
</li>
<li><p>Usability: Length, spelling simplicity, pronunciation clarity, absence of numbers/hyphens.</p>
</li>
<li><p>Relevance and SEO: Relevance to purpose, keyword inclusion, extension potential.</p>
</li>
<li><p>Technical Considerations: TLD appropriateness, potential social media availability.</p>
</li>
<li><p>Legal and Cultural Factors: Potential trademark risks, cultural/linguistic considerations.</p>
</li>
<li><p>Market Potential: Ability to target desired audience, scalability for business growth.</p>
</li>
</ol>
</li>
<li><p>Provides an overall score based on weighted parameters.</p>
</li>
<li><p>Offers a brief explanation for each score.</p>
</li>
<li><p>Highlights pros and cons of the domain name.</p>
</li>
</ul>
<h3 id="heading-domain-names-comparison">Domain Names Comparison</h3>
<p>This feature helps you choose between different options you might have. It:</p>
<ul>
<li><p>Compares two different domain names</p>
</li>
<li><p>Uses the same scoring strategy as the insights feature</p>
</li>
<li><p>Determines a "winning" name</p>
</li>
<li><p>Provides a brief summary explaining the choice</p>
</li>
</ul>
<h3 id="heading-domain-name-ideas">Domain Name Ideas</h3>
<p>This feature is most useful when you're starting from scratch. It:</p>
<ul>
<li><p>Builds upon the same domain name scoring strategy, and offers five distinct domain name suggestions based on your app/service idea</p>
</li>
<li><p>Provides an overall score for each suggestion</p>
</li>
<li><p>Breaks down different category scores</p>
</li>
<li><p>Offers a brief summary of strengths and weaknesses for each name</p>
</li>
</ul>
<h2 id="heading-app-demoscreenshots">App Demo/Screenshots</h2>
<p><strong>Home Page</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184525100/b95c841b-c291-4044-8bbc-4f85e1193588.png" alt="name insights home page" class="image--center mx-auto" /></p>
<p><strong>Name insights loading animation</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184817508/7724153f-acb7-45f0-9da1-e3f9a1301357.png" alt="name insights loading animation" class="image--center mx-auto" /></p>
<p><strong>Name Insights Page (for hashnode.com domain)</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184640550/eb979fdb-c7f5-4faf-b03b-41cce3c63f8f.png" alt="name insights for hashnode.com" class="image--center mx-auto" /></p>
<p><strong>Name comparison page</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184746024/15478b0f-18f2-4772-883c-aafc5cb3cfd0.png" alt="name comparison page" class="image--center mx-auto" /></p>
<p><strong>Comparison loading animation</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184899857/1a06b546-b53e-4325-a829-89f24f3af4e1.png" alt="comparison loading animation" class="image--center mx-auto" /></p>
<p><strong>Comparison results for fitnawake.com &amp; fitandawake.com</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722184967616/d376887e-c449-4241-95e1-4ae18be14259.png" alt="comparison results page" class="image--center mx-auto" /></p>
<p><strong>Name ideas results page</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722185027734/e2e0353e-1d35-474e-a3ce-51f801740dea.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-technical-details">Technical Details</h2>
<p>Here is a brief overview of the app stack and approach</p>
<ol>
<li><p><strong>Nuxt3:</strong> Name Insights uses the full-stack capabilities of Nuxt3 as its backbone for a faster turnaround time. You can achieve the same results using another framework of your choice, but for me it made more sense because of where it is hosted (see point 2).</p>
</li>
<li><p><strong>NuxtHub:</strong> The app is hosted on Cloudflare Pages using <a target="_blank" href="https://hub.nuxt.com/">NuxtHub</a>. NuxtHub offers a great DX for hosting serverless Nuxt apps, and for talking to various Cloudflare services. At the time of writing this article it supports Cloudflare features such as KV, D1, and R2.</p>
</li>
<li><p><strong>NuxtUI:</strong> The app UI is built using NuxtUI, a collection of prebuilt components based on HeadlessUI &amp; TailwindCSS.</p>
</li>
<li><p><strong>Claude 3.5 Sonnet / Anthropic AI:</strong> The core features of the app are thanks to the Anthropic AI SDK, using the Claude 3.5 Sonnet model. The best in class reasoning capabilities of the model allows for a good overall experience of the app features.</p>
</li>
</ol>
<p>At the heart of the app features is the domain scoring strategy. At present, the app relies on the advanced reasoning capabilities of the <strong>Claude 3.5 Sonnet</strong> model to grade a domain name. The key to the implementation lies in carefully crafted system prompts (which you call Prompt Engineering). These prompts instruct the AI on how to analyze domain names, what factors to consider, and how to present the results. E.g. the below is the core part of the system prompt for scoring a domain and generating insights:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> nameScorePrompt = <span class="hljs-string">`You are an AI assistant specialized in 
evaluating and scoring domain names. Analyze the given domain name 
as if you're seeing it for the very first time, without any prior 
knowledge of its actual use or purpose.

Evaluate the domain based on these categories and weights:
1. Brand Impact (30%)
2. Usability (20%)
3. Relevance and SEO (20%)
4. Technical Considerations (15%)
5. Legal and Cultural Factors (10%)
6. Market Potential (5%)

Provide a score out of 100 for each category, along with a brief, 
unbiased explanation. Calculate the weighted overall score out of 100 
(rounded to the nearest integer). Include concise lists of strengths 
and weaknesses of the domain name based solely on its characteristics, 
not its known use. 
`</span>

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.anthropic.messages.create({
  model: <span class="hljs-string">"claude-3-5-sonnet-20240620"</span>,
  max_tokens: <span class="hljs-number">1000</span>,
  temperature: <span class="hljs-number">0.4</span>,
  system: systemPrompt,
  messages: [
    {
      role: <span class="hljs-string">"user"</span>,
      content: <span class="hljs-string">`Analyze the domain name: <span class="hljs-subst">${domainName}</span>`</span>,
    },
  ],
});
</code></pre>
<p>The prompt is intentionally detailed and specific to get the desired results from the LLM. We incorporate the required JSON structure into the prompt to receive the output in JSON format. This output is then parsed using the following function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extractAndParseJson</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params">text: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">T</span> </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(text) <span class="hljs-keyword">as</span> T;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error parsing JSON:"</span>, error);
  }

  <span class="hljs-keyword">const</span> backtickPattern = <span class="hljs-regexp">/```(?:json)?\s*([\s\S]*?)\s*```/g</span>;
  <span class="hljs-keyword">const</span> matches = text.match(backtickPattern);

  <span class="hljs-keyword">if</span> (matches) {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> match <span class="hljs-keyword">of</span> matches) {
      <span class="hljs-keyword">const</span> content = match.replace(<span class="hljs-regexp">/```(?:json)?\s*|\s*```/g</span>, <span class="hljs-string">""</span>).trim();
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(content) <span class="hljs-keyword">as</span> T;
      } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-keyword">continue</span>;
      }
    }
  }

  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No valid JSON found in the text"</span>);
}
</code></pre>
<p>To try out different LLMs from other AI services, the backend implementation is kept generic using interfaces which every AI service needs to implement and extend.</p>
<p>Here is the <strong>AIService</strong> interface:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> AIService {
  getDomainScore(domainName: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;
  compareDomains(firstDomain: <span class="hljs-built_in">string</span>, secondDomain: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;
  getDomainSuggestions(purpose: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;
}
</code></pre>
<p>And the <strong>BaseAIService</strong></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">abstract</span> <span class="hljs-keyword">class</span> BaseAIService <span class="hljs-keyword">implements</span> AIService {
  <span class="hljs-keyword">protected</span> getSystemPrompt(promptType: SystemPromptType): <span class="hljs-built_in">string</span> {
    <span class="hljs-keyword">return</span> getSystemPrompt(promptType);
  }

  <span class="hljs-keyword">abstract</span> getDomainScore(domainName: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;

  <span class="hljs-keyword">abstract</span> compareDomains(
    firstDomain: <span class="hljs-built_in">string</span>,
    secondDomain: <span class="hljs-built_in">string</span>
  ): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;

  <span class="hljs-keyword">abstract</span> getDomainSuggestions(purpose: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt;;
}
</code></pre>
<p>This approach provides a common system prompt for all AI services while allowing individual services the flexibility to override and define their own custom system prompts if needed.</p>
<h2 id="heading-app-amp-repo-links">App &amp; Repo Links</h2>
<p>You can try out the app live at <a target="_blank" href="https://name-insights.nuxt.dev/">https://name-insights.nuxt.dev/</a></p>
<p>The complete app code can be found here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/domain-name-insights">https://github.com/ra-jeev/domain-name-insights</a></div>
<p> </p>
<h2 id="heading-limitations">Limitations</h2>
<p>As the app relies entirely on an LLM for its core functionality, the output is not deterministic. This means that if you analyze the same domain name multiple times, you may receive slightly different scores. This variability is inherent to large language models, which can produce different outputs based on subtle differences in how they process the input each time.</p>
<p>However, in my testing, I've found that this limitation doesn't significantly impact the app's utility. The variance in scores tends to be low, and the overall insights remain consistent. Name Insights is an excellent sounding board for giving you a head start in your domain name search.</p>
<h2 id="heading-further-enhancements">Further Enhancements</h2>
<p>This is just the beginning. Below are some of the actions items that can further enhance the app outcome and usefulness:</p>
<ol>
<li><p>Integrating other LLMs and utilize multiple models simultaneously for arriving at a score and generating insights</p>
</li>
<li><p>Adding real world data to the mix. This will make the scoring more trustworthy and accurate.</p>
</li>
<li><p>Adding real time domain name availability checks, and possibly the social media handles.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Selecting the perfect domain name is a critical step in establishing your online presence. With its three distinct features Name Insights aims to make the process easier at various stages of the domain name search journey. It helps you to:</p>
<ul>
<li><p>Gain objective insights into the strengths and weaknesses of potential domain names</p>
</li>
<li><p>Make informed comparisons between different options</p>
</li>
<li><p>Generate fresh ideas tailored to your specific needs</p>
</li>
</ul>
<p>Irrespective of the slight variability in results, the overall consistency and depth of analysis provided by Name Insights make it an invaluable resource for smarter domain name decisions.</p>
<hr />
<p>I hope you liked reading the article. Do try out the app and it would mean the world to me if you can share your feedback with me.</p>
<p>Until next time...!</p>
<blockquote>
<p><em>Keep adding the bits and soon you'll have a lot of bytes to share with the world</em>.</p>
</blockquote>
]]></content:encoded></item><item><title><![CDATA[Creating a Quiz Generator using the ChatGPT Firebase Extension]]></title><description><![CDATA[You often want to test your knowledge whenever you're learning a new topic. You can get ready-made quizzes on the subject, but generally, they tend to be too broad. What if you only want to test yourself on the content you've just read? This is the p...]]></description><link>https://rajeev.dev/creating-a-quiz-generator-using-the-chatgpt-firebase-extension</link><guid isPermaLink="true">https://rajeev.dev/creating-a-quiz-generator-using-the-chatgpt-firebase-extension</guid><category><![CDATA[AI]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[#firebaseExtensions]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Tue, 28 Nov 2023 15:00:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qDgTQOYk6B8/upload/0ec05d12ee0ef77ab9794fffc5e7674a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You often want to test your knowledge whenever you're learning a new topic. You can get ready-made quizzes on the subject, but generally, they tend to be too broad. What if you only want to test yourself on the content you've just read? This is the problem statement we will tackle in this article: given some content, how can you create a quiz based on it?</p>
<h2 id="heading-introduction">Introduction</h2>
<p>While helping my son with his studies, I often struggle with creating questions to ask him to test his knowledge. It has more to do with the idea of reading the content myself first, which I find tedious. I can ask the questions at the chapter's end, but that doesn't feel comprehensive enough. In these situations, I often think, "If only someone could read the chapter for me, comprehend the content, and generate relevant questions along with the answer keys".</p>
<p>With the advancements in AI and the recent interest in its generative capabilities (ChatGPT, Bard, etc.), I wondered if such a solution is possible using AI. To keep the experiment short, I looked for a low-code solution to achieve this, and Firebase extensions seemed like the right choice.</p>
<h2 id="heading-what-is-generative-ai">What is generative AI?</h2>
<p>As the name suggests, generative AI is a type of artificial intelligence that can create new content or data based on its learning from existing data. This new content or data can be similar to the existing data or be entirely original. It usually works by giving some content to the AI along with your instructions (generally called a prompt) on how to use that content, and the AI gives back an appropriate response.</p>
<p>Now that you know about generative AI, you should have a basic idea about the approach you'll need to take to get the desired results. Let's break it down into steps</p>
<ol>
<li><p>Get the text content from the user for which they want to generate questions</p>
</li>
<li><p>Pass the content along with a suitable instruction set to the AI</p>
</li>
<li><p>Parse the response from the AI and present it to the user in a suitable format</p>
</li>
</ol>
<h2 id="heading-implementation">Implementation</h2>
<p>The implementation of the quiz generator is quite simple: use a <code>textarea</code> to get the text content from the user and pass it to the configured ChatGPT Firebase extension. You can configure the said extension as shown below.</p>
<h3 id="heading-configuring-the-chatgpt-extension">Configuring the ChatGPT extension</h3>
<p>Go to the <a target="_blank" href="https://extensions.dev/">Firebase extensions hub</a>, search for chat in the search box, and click "Chatbot with ChatGPT" from the search results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696961521805/21516aa4-ae33-4701-be5d-19db6f15d8a5.png" alt="Searching chat in the Firebase extensions hub" class="image--center mx-auto" /></p>
<p>You can install this extension from the extension page in one of your Firebase projects. Click on the <strong>"Install in Firebase console"</strong> button, select your project from the new tab that opens up, and complete the steps for installing the extension into the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696962447368/b5af5ab3-06a2-4817-bb97-b1408bf74556.png" alt="installing the ChatGPT extension" class="image--center mx-auto" /></p>
<p>Click the <strong>Next</strong> button. This extension creates a cloud function called to generate a response, and we need to enable the <strong>Secrets Manager API</strong> to store our OpenAI API Key securely. Based on the current state of your project, you may need to enable other APIs (Cloud Functions, Artifact Registry, etc.). Click <strong>Enable</strong> and then click <strong>Next</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696963139254/c10fe156-c127-4784-a024-353c06430e7a.png" alt="ChatGPT extension prerequisites" class="image--center mx-auto" /></p>
<p>Next, the extension needs to use <strong>Cloud Firestore</strong> (If you haven't created a Cloud Firestore yet, it will create one for you in datastore mode. For using it with Firebase, you'll need to change it back to the native mode from Google Cloud Console) and <strong>Secrets Manager</strong> to process the input text, the OpenAI API response, and the stored OpenAI API key. Click <strong>Next</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696963600141/3763bc1c-7eca-4ae2-952f-f81cd3d0d6f4.png" alt="ChatGPT extension permissions" class="image--center mx-auto" /></p>
<p>Finally, we need to configure the extension. You need to configure the following</p>
<ol>
<li><p>Add your OpenAI API Key (you can create it <a target="_blank" href="https://platform.openai.com/account/api-keys">here</a>). Make sure to click on the <strong>Create Secret</strong> button to create a secret in the <strong>Secrets Manager</strong></p>
</li>
<li><p>Select the desired model (<strong>GPT 3.5/4</strong>)</p>
</li>
<li><p>Set the <strong>temperature</strong> to a low value (<strong>0.2-0.4</strong>). The temperature value (allowed range 0-2) dictates the randomness of the AI response; higher values make the output more random. I used a temperature of <strong>0.3</strong>.</p>
</li>
<li><p>Set the cloud function location (ideally set it to the exact location where your Cloud Firestore is located)</p>
</li>
<li><p>You'll also need to configure the collection path and the field names where the input text and the API response will be stored. For this test, I used requests as the collection name and left the field names as it is.</p>
</li>
</ol>
<p>We can leave the rest of the settings as it is and click on the <strong>Install extension</strong> button. Wait for the installation to complete; now we're ready to test it.</p>
<h3 id="heading-testing-the-extension">Testing the extension</h3>
<p>To test the extension you need to create the collection that you configured in the last section (<strong>requests</strong> in my case) and add a prompt field to it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696969679798/ead22a09-dfc9-481e-9e48-35db121f0d7a.png" alt="Creating the requests collection" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696969727185/520e6731-6cdc-48b3-81e6-86c4dcac21e3.png" alt="Adding a request to the requests collection" class="image--center mx-auto" /></p>
<p>The way this extension works is:</p>
<ol>
<li><p>You add the input text and your instructions to the prompt field</p>
</li>
<li><p>The cloud function (<code>generateAIResponse</code>) created by the extension gets invoked automatically</p>
</li>
<li><p>The function sends your prompt to the OpenAI API and creates a status field (of type map) with its state key set to <code>PROCESSING</code></p>
</li>
<li><p>On a successful response from the API call, the state is changed to <code>COMPLETED</code> and the API call output is stored in a new field named response</p>
</li>
</ol>
<h3 id="heading-prompt-structure">Prompt Structure</h3>
<p>How do you structure your prompt? A good prompt should start by setting a context so that ChatGPT behaves accordingly. Then, you state your requirement, followed by the input text, and finally, you give a response cue to the AI.</p>
<p>To summarize, you can use the below prompt structure</p>
<p><code>Context + Instruction + Input Text + Response Cue</code></p>
<p>For the quiz generator, you can use the following initial prompt</p>
<pre><code class="lang-markdown">“You're a teacher who needs to create quizzes to test your student's
knowledge. Using the given text content create several distinct MCQs for the
purpose.

The content: {{text<span class="hljs-emphasis">_content}}

Your response:”</span>
</code></pre>
<p>The above prompt generates a response in a format similar to the one below. Please note that the format may vary slightly for you.</p>
<pre><code class="lang-markdown"><span class="hljs-bullet">1.</span> question text
<span class="hljs-code">    a) Option 1
    b) Option 2
    ...
...</span>
</code></pre>
<p>This is a very good start, but the API is not returning the answers to these questions. Also, it is difficult to use it as a quiz as the response lacks structure.</p>
<h3 id="heading-structuring-the-response">Structuring the response</h3>
<p>For better control over the whole thing, you need a structured response, such as a JSON formatted output, from the ChatGPT API call. Tweak the instruction part of your prompt to the following:</p>
<pre><code class="lang-markdown">Using the given text content creates several distinct MCQs for the purpose. 
Return your response as an RFC8259-compliant JSON array using the structure
[{"question": "the question", "choices": ["choice 1", "choice 2", ...],
"answer": "the correct choice"}, {...}]
</code></pre>
<p>Here I’ve intentionally removed any spaces from the JSON to save some tokens from the response. Rerunning the same test with the changed instruction gives us the desired structured result. Now, you can display the questions &amp; the choices in any way you want using your favourite technology. As you have the answer key, you can also convert these questions into a quiz.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Once in a while, you might get invalid API responses, like extra text in the response, and so on. <em>You can further play around with your instructions to eliminate or minimize such cases.</em></div>
</div>

<h2 id="heading-using-images-as-content">Using images as content</h2>
<p>You can extend it further and use images that contain text (say, a snapshot of a book's pages) as the starting point of the quiz generator. The underlying approach of the solution remains the same; you're just adding a step at the beginning. Using another Firebase extension, you can extract the text from an image and use that as input like before.</p>
<h3 id="heading-configuring-the-cloud-vision-extension">Configuring the Cloud Vision extension</h3>
<p>Go back to the <a target="_blank" href="https://extensions.dev/">Firebase extensions hub</a>, search for <strong>vision,</strong> and click on "<strong>Extract Image Text with Cloud Vision AI</strong>" from the search results. Install the extension by following the steps you completed for the previous extension.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697051978965/8126899d-0d7b-41fc-a327-7edf18dfe370.png" alt="Installing the Cloud Vision extension" class="image--center mx-auto" /></p>
<p>This extension enables Google's Cloud Vision API and creates a new cloud function named <code>extractText</code>. Apart from the <strong>Cloud Firestore User</strong> permission, it also needs the <strong>Storage Admin</strong> permission (the extension reads the images from Cloud Storage).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697052364138/94f08ce3-7c10-4ca8-ac27-2d329108d1a7.png" alt="Cloud Vision extension permissions" class="image--center mx-auto" /></p>
<p>Finally, configure the extension as follows (you should keep your cloud function's location near your Cloud Storage/Firestore's location). You can leave the rest of the settings as it is and install the extension</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697052566084/083ad8da-7066-461c-97a7-7e761719f3b6.png" alt="Configuring the cloud vision extension" class="image--center mx-auto" /></p>
<p>Once the extension has finished installing, you can test it out by uploading an image to your cloud storage bucket under the <strong>snapshots</strong> folder. A few seconds after adding the image, you should see a new collection, <code>extractedText</code> with a single document containing the text that was extracted from the image.</p>
<p>For my test, I used the following screenshot</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697053558367/8b06015f-7484-4d05-b06f-84819c33e4d5.png" alt="test screenshot" class="image--center mx-auto" /></p>
<p>And this is the result of the Cloud Vision extension</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697053653067/cde08310-7d9c-4e6c-8afc-3d49859ee972.png" alt="result from Cloud Vision API" class="image--center mx-auto" /></p>
<p>You can use this text as input to the ChatGPT extension and generate questions as before.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Firebase extensions are powerful low-code options for many different use cases. As we saw in this article, leveraging the power of ChatGPT and Cloud Vision extensions could make the basic quiz generator functionality work without writing a single line of code.</p>
<p>Now, you can integrate Cloud Storage and Firestore into your front end to create a personalized and engaging quiz generator that generates relevant questions based on the given content.</p>
<p>I hope you liked reading the article. If you've questions or just want to say hi, just drop a message in the comments section.</p>
<p><em>-- Keep adding the bits, soon you'll have more bytes than you may need. :-)</em></p>
]]></content:encoded></item><item><title><![CDATA[To Outerbase with Bun, ToastUI Editor and ChatGPT]]></title><description><![CDATA[This article is about exploring the new talk of the town, bun, getting to know Outerbase, and hanging out with old buddies markdown and ChatGPT. If you follow along, you'll get to know how the oven was heated, to bake fresh plugins for Outerbase.
Int...]]></description><link>https://rajeev.dev/to-outerbase-with-bun-toastui-editor-and-chatgpt</link><guid isPermaLink="true">https://rajeev.dev/to-outerbase-with-bun-toastui-editor-and-chatgpt</guid><category><![CDATA[Outerbase]]></category><category><![CDATA[outerbasehackathon]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[Bun]]></category><category><![CDATA[markdown]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Mon, 02 Oct 2023 03:28:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/6SbFGnQTE8s/upload/7a3213c083ad21700d87e2fbb2bb66b5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article is about exploring the new talk of the town, <code>bun</code>, getting to know <code>Outerbase</code>, and hanging out with old buddies <code>markdown</code> and <code>ChatGPT</code>. If you follow along, you'll get to know how the oven was heated, to bake fresh plugins for <a target="_blank" href="https://outerbase.com/">Outerbase</a>.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>When I learnt about the <a target="_blank" href="https://beta.outerbase.com/">Outerbase</a> hackathon on <a target="_blank" href="https://hashnode.com/">Hashnode</a> I was intrigued about it. I thought it was another database, "base" being the operative word. But I was only half right, or maybe half wrong, it depends on whom you're asking. Outerbase is an interface to your data (currently only residing in some relational databases), but it adds a lot of bells and whistles to make it interesting.</p>
<p>Some of the features include:</p>
<ol>
<li><p>Commands: These are functions in the cloud (Lambda functions?) that can talk to your database. You can create a chain of nodes to handle different steps of the process (AWS Step functions? Of course, it is not there yet but maybe soon...)</p>
</li>
<li><p>EZQL: It enables you to ask questions to your database in plain text. No more making your own SQL queries.</p>
</li>
<li><p>Plugins: Your data tables are much more than a spreadsheet, Outerbase plugins enable you to visualize that. You can add different plugins for different types of data, and interact with it in a way that feels native.</p>
</li>
</ol>
<p>The last feature is what caught my fancy, and I decided to focus on that for this hackathon. Here is a short video demo of the plugins that I created.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/aErH1bOy73o">https://youtu.be/aErH1bOy73o</a></div>
<p> </p>
<h2 id="heading-preparing-the-base-with-bun">Preparing the base with Bun</h2>
<p>Since I decided to focus on plugins, I wanted a way to quickly create the basic template on top of which I could build upon. One way was to just get the template from the Outerbase repo and copy-paste it to create more of it. But where is the fun in that? There is a saying, "Automate it, silly." (Even if it takes you days to build that automation). So that is the path I took.</p>
<h3 id="heading-creating-templates">Creating templates</h3>
<p>You can create local templates (and maybe publish them later on) with <code>Bun</code> and then run a simple command to use that template. Your local templates should be present inside a <code>.bun-create</code> folder in the following paths</p>
<ul>
<li><p><code>$HOME/.bun-create/&lt;name&gt;</code>: global templates</p>
</li>
<li><p><code>&lt;project root&gt;/.bun-create/&lt;name&gt;</code>: project-specific templates</p>
</li>
</ul>
<p><code>&lt;name&gt;</code> is the template/folder name you want to use. Drop your template files into the template folder and run the following command to use that template</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Using a local template will overwrite the destination folder, so make sure that it doesn't exist, or is empty.</div>
</div>

<pre><code class="lang-bash"><span class="hljs-comment"># Notice the "./" in the beginning. Without that it looks </span>
<span class="hljs-comment"># for the template on Github (bug). </span>
bun create ./&lt;template-name&gt; &lt;destination&gt;
</code></pre>
<p>There are two types of Outerbase plugins;</p>
<ul>
<li><p>Table plugins: these work on the whole database table, and</p>
</li>
<li><p>Cell plugins: made for working with a cell but applied on a column so that they're available to all the cells of that column</p>
</li>
</ul>
<p>An Outerbase plugin consists of at most three views; 1. The configuration view, 2. The data view, and 3. The data editor/dialog view. These views are created using <code>Web Components</code> (Custom HTML Elements). A basic Outerbase component can be represented as follows</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> templateEditor = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"template"</span>);
templateEditor.innerHTML = <span class="hljs-string">`
&lt;style&gt;
  #container {
    max-width: 320px;
  }
&lt;/style&gt;

&lt;div id="container"&gt;&lt;/div&gt;
`</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OuterbasePluginCellEditor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">HTMLElement</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">get</span> <span class="hljs-title">observedAttributes</span>() {
    <span class="hljs-keyword">return</span> [...observed_attributes];
  }

  config = <span class="hljs-keyword">new</span> OuterbasePluginConfig({});

  <span class="hljs-keyword">constructor</span>() {
    <span class="hljs-built_in">super</span>();

    <span class="hljs-built_in">this</span>.shadow = <span class="hljs-built_in">this</span>.attachShadow({ <span class="hljs-attr">mode</span>: <span class="hljs-string">"open"</span> });
    <span class="hljs-built_in">this</span>.shadow.appendChild(
      templateEditor.content.cloneNode(<span class="hljs-literal">true</span>)
    );
  }

  connectedCallback() {
    <span class="hljs-built_in">this</span>.config = <span class="hljs-keyword">new</span> OuterbasePluginConfig(
      decodeAttributeByName(<span class="hljs-built_in">this</span>, <span class="hljs-string">"configuration"</span>)
    );

    <span class="hljs-built_in">this</span>.config.cellValue = <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">"cellvalue"</span>);
    <span class="hljs-built_in">this</span>.render();
  }

  render() {}
}
</code></pre>
<p>Before we can use this component we need to register this component with the window</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">window</span>.customElements.define(
  <span class="hljs-string">"outerbase-plugin-cell-editor"</span>,
  OuterbasePluginCellEditor
);
</code></pre>
<h2 id="heading-the-first-plugin-audio-player">The first plugin: Audio Player</h2>
<p>If your data consists of audio urls, wouldn't it be cool to play the audio from the database view itself? This Outerbase cell plugin allows you to do exactly that.</p>
<p>All the magic of this plugin resides in its <code>Editor</code> view. We simply attach an HTML audio element to the view. Set the audio src and load it. And our audio link is ready to play. Below is the updated <code>render</code> method shown earlier.</p>
<pre><code class="lang-javascript">render() {
  <span class="hljs-keyword">const</span> srcUrl = <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">"cellvalue"</span>);
  <span class="hljs-keyword">if</span> (srcUrl) {
    <span class="hljs-built_in">this</span>.shadow.getElementById(
      <span class="hljs-string">"container"</span>
    ).innerHTML = <span class="hljs-string">`&lt;audio id="audio-player" controls /&gt;`</span>;
    <span class="hljs-keyword">const</span> player = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"audio-player"</span>);
    player.src = srcUrl;
    player.load();
  }
}
</code></pre>
<p>The above code allows us to get this view (the audio player dialog)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696203970794/f7eebf16-0ea5-4ecd-8f78-9f4cea51ff09.png" alt="audio player plugin view" class="image--center mx-auto" /></p>
<h2 id="heading-the-second-plugin-video-player">The second plugin: Video Player</h2>
<p>Since we have an audio player for our database now, it is only logical to do the same thing for videos. But this is not that simple. Most of the video links you encounter are YouTube (and maybe some Vimeo) video links, so I set out to make that work.</p>
<p>Now, the URLs that we see in the browser's address bar (watch URLs) for YouTube videos are different from when you want to embed the YouTube player in your website. Assuming the database will store the watch URLs, we need to create the embed URLs. The below function does exactly that using a regex.</p>
<pre><code class="lang-javascript">getYouTubeEmbedUrl(url) {
  <span class="hljs-keyword">const</span> youtubeRegex =
    <span class="hljs-regexp">/^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&amp;v=)([^#\&amp;\?]*).*/</span>;
  <span class="hljs-keyword">const</span> match = url.match(youtubeRegex);
  <span class="hljs-keyword">if</span> (match &amp;&amp; match[<span class="hljs-number">2</span>].length == <span class="hljs-number">11</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">`https://youtube.com/embed/<span class="hljs-subst">${match[<span class="hljs-number">2</span>]}</span>`</span>;
  }
}
</code></pre>
<p>Now we can create an <code>iframe</code>, set its <code>src</code> as this embed URL and our player should be ready. We do all this in the editor/dialog view of the plugin.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"container"</span>).innerHTML = 
    <span class="hljs-string">`&lt;iframe id="video-player" type="text/html" width="360" height="240" frameborder="0" /&gt;`</span>;
<span class="hljs-keyword">const</span> player = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"video-player"</span>);
player.src = embedUrl;
</code></pre>
<p>The above code works fine locally but when deployed to the Outerbase console it fails to load the player. The reason is Outerbase plugins run inside a sandboxed iframe to keep the data secure. The YouTube player needs to use the browser cache to store its assets but the configured sandbox permissions don't allow it, and the player fails to appear.</p>
<p>But the same technique can be utilized for other video hosting platforms, Vimeo being one of them. The problem with Vimeo is that it doesn't have well structured consistent URLs. So we use its <a target="_blank" href="https://developer.vimeo.com/api/oembed/videos"><code>oEmbed APIs</code></a> to fetch the correct URL.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> getVimeoEmbedUrl(url) {
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`https://vimeo.com/api/oembed.json?url=<span class="hljs-subst">${url}</span>`</span>);

  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
  <span class="hljs-keyword">return</span> <span class="hljs-string">`https://player.vimeo.com/video/<span class="hljs-subst">${data.video_id}</span>?title=0&amp;byline=0&amp;dnt=1`</span>;
}
</code></pre>
<p>And now we can use the same iframe player to play this video.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Notice the query parameter <code>&amp;dnt=1</code> in the created URL. Without that Vimeo player will also follow the YouTube player's way. <code>dnt</code> is "do not track". If it is 0 (the default value), the Vimeo player will try to use browser cookies and will fail as we're sandboxed.</div>
</div>

<p><code>dnt</code> parameter saved the day for this plugin, else all that work would have come to nought. Here is the view we get from the player</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696206894666/24d8e698-fc93-4d7a-8ea6-a084ac47e6a6.png" alt="vimeo player view" class="image--center mx-auto" /></p>
<p>There is still one error present in the browser console for the Vimeo player, and that is for missing the <code>presentation</code> permission. This can be alleviated by sandbox="allow-presentation" on the parent iframe, or maybe there is some other Vimeo URL query param that can help with that. I haven't explored further as even with the error in the console, the video can be played (The first player load doesn't work, but no problem afterwards).</p>
<h2 id="heading-the-third-plugin-markdown-editor">The third plugin: Markdown Editor</h2>
<p>How cool it would be to create blog posts or write docs from the database view itself? This <code>md-editor</code> plugin is exactly what you need for all such cases.</p>
<p>Let's first take a peek at the plugins editor view</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696207770449/555b7179-bf05-404f-875a-ee6666f8ab31.png" alt="md-editor view" class="image--center mx-auto" /></p>
<p>This plugin is different from the other plugins we've seen so far, as here we need to use third-party scripts and CSS to achieve the goal. How do we go about this, and which markdown editor to integrate? These are important questions to answer. Let's tackle these questions one by one</p>
<ol>
<li><p>How to include third-party scripts and CSS: Even though we're using web components, these are still part of the DOM, so maybe we can create script and link tags dynamically to load these. An example would be as follows</p>
<pre><code class="lang-javascript"> <span class="hljs-comment">// JavaScript</span>
 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadCSS</span>(<span class="hljs-params">url</span>) </span>{
   <span class="hljs-keyword">const</span> link = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'link'</span>);
   link.rel = <span class="hljs-string">'stylesheet'</span>;
   link.href = url;
   <span class="hljs-built_in">document</span>.head.appendChild(link);
 }

 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">loadJS</span>(<span class="hljs-params">url</span>) </span>{
   <span class="hljs-keyword">const</span> script = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'script'</span>);
   script.src = url;
   <span class="hljs-built_in">document</span>.body.appendChild(script);
 }

 <span class="hljs-comment">// Example usage:</span>
 loadCSS(<span class="hljs-string">'https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.0/css/bootstrap.min.css'</span>);
 loadJS(<span class="hljs-string">'https://code.jquery.com/jquery-3.6.0.min.js'</span>);
</code></pre>
</li>
<li><p>Which editor to integrate: There are many markdown editors available, but we need something that can work with Web Components (in-browser). I also wanted to avoid any kind of bundling of the scripts with the plugin. This is because the Outerbase plugins can have only so much size, and markdown/WYSIWYG editors are bulky. So we need one which is available from a CDN. I looked at and tried QuillJs (with QuillJs Markdown module), SimpleMDE and ToastUI Editor, and the latter one seemed relatively maintained and worked on the first try so that is the one we integrate.</p>
</li>
</ol>
<p>Using the functions listed just above, if you try to load the scripts and CSS in an Outerbase plugin you'll get the gotcha moment of this plugin; you're not allowed to load third-party CSS because of the CSP (content security policy). What do we do now? Without the stylesheets, it is pointless to integrate the editor. Maybe we bundle the CSS with the plugin (bundling the script is still a NO, due to its size)?</p>
<h3 id="heading-bun-as-a-package-manager-andamp-bundler">Bun as a package manager &amp; bundler</h3>
<p>Now we go back to reading about bun. After reading the docs it became clear that Bun doesn't support bundling the CSS at present. It simply copies the CSS files to the outdir and renames their references. There are two ways out of this:</p>
<ol>
<li><p>Create a bun plugin to handle the CSS file bundling.</p>
<ol>
<li><p>Read the CSS files from node_modules</p>
</li>
<li><p>Minify using some third-party CSS minifier (no native CSS loader) and,</p>
</li>
<li><p>Inject as text into the final bundle</p>
</li>
</ol>
</li>
<li><p>Simply get the minified CSS files from the CDN, and inject them as text into the plugin code (no bundling needed as the plugin code is very small)</p>
</li>
</ol>
<p>So we pick the easy way out here and pick the second option. Run the <code>bun init</code> command to quickly create a <code>package.json</code> file (so that we can use bun APIs). Create a new file <code>build.js</code> for handling the tooling. This is the function which does what we need. Replacements is just an object where the keys are the identifiers we want to replace with the actual CSS styles.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> bundle = <span class="hljs-keyword">async</span> (replacements) =&gt; {
  <span class="hljs-keyword">if</span> (replacements) {
    <span class="hljs-keyword">const</span> indexFile = Bun.file(<span class="hljs-string">"index.js"</span>);
    <span class="hljs-keyword">let</span> indexFileText = <span class="hljs-keyword">await</span> indexFile.text();

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> key <span class="hljs-keyword">in</span> replacements) {
      <span class="hljs-comment">// Fetch the CSS file from the CDN using the URL</span>
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(replacements[key]);
      <span class="hljs-keyword">const</span> fText = <span class="hljs-keyword">await</span> res.text();

      indexFileText = indexFileText.replace(key, fText);
    }

    createOutput(<span class="hljs-string">"out"</span>, indexFileText);
  } <span class="hljs-keyword">else</span> {
    fs.cpSync(<span class="hljs-string">"index.js"</span>, <span class="hljs-string">"out/index.js"</span>);
  }
};

<span class="hljs-keyword">const</span> createOutput = <span class="hljs-function">(<span class="hljs-params">dir, fileText</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> filePath = <span class="hljs-string">`<span class="hljs-subst">${dir}</span>/index.js`</span>;
  <span class="hljs-keyword">const</span> directoryPath = path.dirname(filePath);
  <span class="hljs-keyword">if</span> (!fs.existsSync(directoryPath)) {
    fs.mkdirSync(directoryPath, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
  }

  fs.writeFileSync(filePath, fileText);
};
</code></pre>
<p>Since we're in the CLI realm now, added a little complexity to get the file names as CLI input (using <code>commander</code>). You can check the Github repo for the code.</p>
<h3 id="heading-coding-the-plugin">Coding the plugin</h3>
<p>First of all, we load the script (remember the CSS is already injected into the plugin, in the style tag of the cell editor's shadow dom, to be precise).</p>
<pre><code class="lang-javascript">loadToastUiEditor() {
  <span class="hljs-keyword">const</span> scriptSrc =
    <span class="hljs-string">"https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"</span>;
  <span class="hljs-comment">// Optimization to not load the script again </span>
  <span class="hljs-comment">// and again, as the editor is recreated every time</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.scripts) {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> script <span class="hljs-keyword">of</span> <span class="hljs-built_in">document</span>.scripts) {
      <span class="hljs-keyword">if</span> (script.src === scriptSrc) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"script already loaded, bail out"</span>);
        <span class="hljs-keyword">return</span>;
      }
    }
  }

  <span class="hljs-keyword">const</span> el = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"script"</span>);
  el.src = scriptSrc;

  el.onload = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">this</span>.render();
  };

  el.onerror = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"failed to load the script"</span>, event);
  };

  <span class="hljs-comment">// We're adding the script to the document and not </span>
  <span class="hljs-comment">// the shadow dom, because the shadom dom is recreated</span>
  <span class="hljs-comment">// whenver the editor is opened</span>
  <span class="hljs-built_in">document</span>.head.appendChild(el);
}
</code></pre>
<p>As soon as the script is loaded we are ready to show our markdown editor</p>
<pre><code class="lang-javascript">render() {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> Editor = toastui.Editor;
    <span class="hljs-built_in">this</span>.editor = <span class="hljs-keyword">new</span> Editor({
      <span class="hljs-attr">el</span>: <span class="hljs-built_in">this</span>.shadow.querySelector(<span class="hljs-string">"#editor"</span>),
      <span class="hljs-attr">height</span>: <span class="hljs-string">"420px"</span>,
      <span class="hljs-attr">initialEditType</span>: <span class="hljs-string">"markdown"</span>,
      <span class="hljs-attr">initialValue</span>: <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">"cellvalue"</span>),
      <span class="hljs-attr">previewStyle</span>: <span class="hljs-string">"vertical"</span>,
      <span class="hljs-attr">usageStatistics</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">theme</span>: <span class="hljs-built_in">this</span>.config.theme,
      <span class="hljs-attr">events</span>: { <span class="hljs-attr">keydown</span>: <span class="hljs-built_in">this</span>.handleKeyDown },
    });

    <span class="hljs-built_in">this</span>.setEditorPosition();
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"render error"</span>, error);
  }
}
</code></pre>
<p>Many things are going on here most of which are self-explanatory, I'll briefly touch upon the important points</p>
<ol>
<li><p>We open the editor with whatever content the cell was holding</p>
<pre><code class="lang-javascript"> initialValue: <span class="hljs-built_in">this</span>.getAttribute(<span class="hljs-string">"cellvalue"</span>)
</code></pre>
</li>
<li><p>Because of the way cell plugins have been designed, they pop up near the cell to which they're attached. For our plugin, we need a centred dialog. We achieve that through plain old DOM manipulation</p>
<pre><code class="lang-javascript"> setEditorPosition() {
   <span class="hljs-keyword">const</span> agPopUpChild = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".ag-popup-child"</span>);
   <span class="hljs-keyword">const</span> container = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"container"</span>);

   <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
     agPopUpChild.style.left = <span class="hljs-string">`<span class="hljs-subst">${
       (<span class="hljs-built_in">window</span>.innerWidth - container.offsetWidth) / <span class="hljs-number">2</span>
     }</span>px`</span>;
     <span class="hljs-comment">// agPopUpChild.style.top = `${</span>
     <span class="hljs-comment">//   (window.innerHeight - container.offsetHeight - 100) / 2</span>
     <span class="hljs-comment">// }px`; // -100 offset for outerbase top bars</span>

     agPopUpChild.style.top = <span class="hljs-string">"0px"</span>; <span class="hljs-comment">// Just hardcode at 0px otherwise top border not visible</span>
   }, <span class="hljs-number">10</span>);
 }
</code></pre>
</li>
<li><p>The theme is set to the editor using <code>theme: this.config.theme</code> doesn't work without some manipulation. At the moment we do not receive the theme metadata in cell plugins. The below code finds the theme from the DOM styles</p>
<pre><code class="lang-javascript"> <span class="hljs-keyword">const</span> agPopUp = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">".ag-popup"</span>);
 <span class="hljs-keyword">const</span> colorScheme = <span class="hljs-built_in">window</span>.getComputedStyle(agPopUp)[<span class="hljs-string">"color-scheme"</span>];
 <span class="hljs-built_in">this</span>.config.theme = colorScheme === <span class="hljs-string">"normal"</span> ? <span class="hljs-string">"light"</span> : <span class="hljs-string">"dark"</span>;
</code></pre>
</li>
<li><p>If we press enter in the markdown editor (to add a new line), the cell plugin editor closes itself (maybe there is a <code>keydown</code> event listener somewhere listening for <code>Enter</code> key events). We skirt through it by stopping such event propagation</p>
<pre><code class="lang-javascript"> events: { <span class="hljs-attr">keydown</span>: <span class="hljs-built_in">this</span>.handleKeyDown }, <span class="hljs-comment">//Listen for keydown events from the md editor</span>

 handleKeyDown(_, event) {
   <span class="hljs-keyword">if</span> (event.key === <span class="hljs-string">"Enter"</span>) {
     event.stopPropagation();
   }
 }
</code></pre>
</li>
</ol>
<p>Now we're ready to enjoy our writing with the md-editor. Once we're done, we can click the save button to close the editor and update the cell's content.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> saveBtn = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"save-btn"</span>);
saveBtn.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> finalContent = <span class="hljs-built_in">this</span>.editor.getMarkdown();
  triggerEvent_$PLUGIN_ID(<span class="hljs-built_in">this</span>, {
    <span class="hljs-attr">action</span>: OuterbaseColumnEvent_$PLUGIN_ID.onStopEdit,
    <span class="hljs-attr">value</span>: finalContent,
  });
  triggerEvent_$PLUGIN_ID(<span class="hljs-built_in">this</span>, {
    <span class="hljs-attr">action</span>: OuterbaseColumnEvent_$PLUGIN_ID.updateCell,
    <span class="hljs-attr">value</span>: finalContent,
  });
});
</code></pre>
<h3 id="heading-chatgpt-the-secret-sauce-of-the-plugin">ChatGPT: The secret sauce of the plugin</h3>
<p>This plugin comes integrated with ChatGPT to help with your writing with some preconfigured actions. Using it you can change the tone of your writing, get suggestions for headlines, generate a summary for your content, and more.</p>
<p>The below method prepares the UI for showing the tone selections.</p>
<pre><code class="lang-javascript">prepareTonesSelections() {
  <span class="hljs-keyword">const</span> toneSelect = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"tone-select"</span>);
  <span class="hljs-built_in">this</span>.tones.forEach(<span class="hljs-function">(<span class="hljs-params">value</span>) =&gt;</span> {
    <span class="hljs-built_in">this</span>.addSelectOption(toneSelect, value);
  });

  <span class="hljs-keyword">const</span> toneBtn = <span class="hljs-built_in">this</span>.shadow.getElementById(<span class="hljs-string">"tone-btn"</span>);
  toneBtn.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> selectedIndex = toneSelect.selectedIndex;
    <span class="hljs-keyword">if</span> (selectedIndex) {
      <span class="hljs-keyword">const</span> selectedTone = toneSelect.options[selectedIndex].value;
      <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`Make the following text better and rewrite it in a <span class="hljs-subst">${selectedTone.toLowerCase()}</span> tone`</span>;
      <span class="hljs-built_in">this</span>.handleSelectionAction(prompt);
    }
  });
}
</code></pre>
<p>The action is handled in the below method where we go ahead only if some text is selected in the editor. We also show a "Thinking..." text as a loader and finally replace that with the result received from the API call.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> handleSelectionAction(prompt) {
  <span class="hljs-keyword">const</span> [start, end] = <span class="hljs-built_in">this</span>.editor.getSelection();
  <span class="hljs-keyword">const</span> selectedText = <span class="hljs-built_in">this</span>.editor.getSelectedText(start, end);
  <span class="hljs-keyword">if</span> (selectedText) {
    <span class="hljs-built_in">this</span>.editor.insertText(<span class="hljs-string">`<span class="hljs-subst">${selectedText}</span>\n\nThinking...`</span>);
    <span class="hljs-keyword">const</span> currLinePos = end[<span class="hljs-number">0</span>] + <span class="hljs-number">2</span>;
    <span class="hljs-built_in">this</span>.editor.setSelection(
      [currLinePos, <span class="hljs-number">1</span>],
      [currLinePos, <span class="hljs-string">"Thinking..."</span>.length + <span class="hljs-number">1</span>]
    );

    <span class="hljs-keyword">const</span> generatedText = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.talkToChatGPT(prompt, selectedText);
    <span class="hljs-keyword">if</span> (generatedText) {
      <span class="hljs-built_in">this</span>.editor.insertText(generatedText);
    }
  }
}
</code></pre>
<p>Below is the method which makes the API call to ChatGPT</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> talkToChatGPT(instruction, text) {
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.openai.com/v1/completions"</span>, {
    <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
    <span class="hljs-attr">headers</span>: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      <span class="hljs-attr">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${<span class="hljs-built_in">this</span>.config.apiKey}</span>`</span>,
    },
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
      <span class="hljs-attr">model</span>: <span class="hljs-string">"gpt-3.5-turbo-instruct"</span>,
      <span class="hljs-attr">prompt</span>: <span class="hljs-string">`<span class="hljs-subst">${instruction}</span>: <span class="hljs-subst">${text}</span>`</span>,
      <span class="hljs-attr">max_tokens</span>: <span class="hljs-number">2048</span>,
      <span class="hljs-attr">temperature</span>: <span class="hljs-number">0.3</span>,
      <span class="hljs-attr">n</span>: <span class="hljs-number">1</span>,
    }),
  });

  <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
  <span class="hljs-keyword">return</span> data.choices[<span class="hljs-number">0</span>].text.trim();
}
</code></pre>
<p>Similarly, we handle the other commands using other appropriate prompts. You can check the GitHub repo for the complete source code of the plugin.</p>
<h2 id="heading-current-limitations">Current Limitations</h2>
<ol>
<li><p>The cell plugin editors are closed automatically as soon as users click somewhere outside. It would be great to have an option to close the editor when users explicitly click on a button</p>
</li>
<li><p>The changed data does not persist even though correct events are sent to the parent DOM. One workaround is to use the Outerbase commands to make a call to the database, but due to lack of time I haven't explored that</p>
</li>
<li><p>The toolbar items that open a pop-up/dialog in the markdown editor (Heading / Link / Image etc.) do not work. The dialogs get closed as soon as you click on them. This is an open issue with the ToastUI editor where this functionality doesn't work in Shadow Dom. There is a workaround to replace these buttons with a similar button that doesn't open a dialog. Again due to lack of time, this has not been explored.</p>
</li>
</ol>
<h2 id="heading-resources">Resources</h2>
<p>The complete source code of the plugins and the templates can be found here</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/outerbase-adventures">https://github.com/ra-jeev/outerbase-adventures</a></div>
<p> </p>
<p>The demo video showing the plugins in action</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/aErH1bOy73o">https://youtu.be/aErH1bOy73o</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>When you explore a new thing many roadblocks will come. Some roadblocks will have a workaround, and some will be dead ends. Now it is up to you whether you quit on the first roadblock, or push ahead and try to find a way. This Outerbase hackathon was one such adventure for me, and I thoroughly enjoyed it.</p>
<p>Hope you liked reading the article. Do let me know your thoughts in the comments section.</p>
<p><em>Remember to keep adding the bits, soon you'll have more bytes than you'll ever need :-)</em></p>
<p>Until text time! Adios.</p>
]]></content:encoded></item><item><title><![CDATA[Creating 1Password plugin and use it to build an app with Nuxt3, Passage & Appwrite]]></title><description><![CDATA[Last year I built an app for my son (and myself). The app idea was very simple, a digital piggy bank tracker integrated with optional auto credit of pocket money. We're actively using this app even now (if interested, you can read the associated arti...]]></description><link>https://rajeev.dev/creating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite</link><guid isPermaLink="true">https://rajeev.dev/creating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite</guid><category><![CDATA[1password]]></category><category><![CDATA[1password-hackathon]]></category><category><![CDATA[Appwrite]]></category><category><![CDATA[Nuxt]]></category><category><![CDATA[BuildWith1Password]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sat, 01 Jul 2023 06:35:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/q7h8LVeUgFU/upload/ea13967cd552231b6545aab22352a803.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last year I built an app for my son (and myself). The app idea was very simple, a digital piggy bank tracker integrated with optional auto credit of pocket money. We're actively using <a target="_blank" href="https://mypiggyjar.com">this app</a> even now (if interested, you can read the associated article <a target="_blank" href="https://rajeev.dev/new-years-promise-to-my-son">here</a>). But this app had some shortcomings, the biggest one being not able to invite your family members to the app. This app rewrite is intended to fix that shortcoming.</p>
<p><em>Tl;dr this article covers a complete rewrite of an existing app using a different technology stack. The article also covers the creation of a 1Password CLI Shell Plugin.</em></p>
<h2 id="heading-introduction">Introduction</h2>
<p>To do the rewrite I wanted to use Nuxt3. The reasoning was simple, I've used Nuxt2 in the past, but haven't had the chance to look at Nuxt3 (and Vue3 for that matter). For the database and to process the important database events I decided to use Appwrite. Appwrite has inbuilt authentication but I wanted to try out <a target="_blank" href="https://passage.1password.com/">Passage by 1Password</a>, so this app uses that.</p>
<p>Appwrite provides a CLI, and <a target="_blank" href="https://1password.com/developers?utm_source=hashnode&amp;utm_medium=landing-page&amp;utm_campaign=hashnode-hackathon">1Password</a> also has a CLI but both are not integrated. So it seemed logical to first integrate the two and create the Appwrite shell plugin for the 1Password CLI.</p>
<h2 id="heading-part-1-creating-a-1password-shell-plugin">Part 1: Creating a 1Password shell plugin</h2>
<p>If you go over to the <a target="_blank" href="https://developer.1password.com/docs/cli/shell-plugins/contribute">1Password docs</a> page which mentions how you can contribute and create your own shell plugin, you'll learn that the whole thing is implemented using <code>Go</code>.</p>
<p><img src="https://media.giphy.com/media/3oEjHWzZQaCrZW2aWs/giphy.gif" alt class="image--center mx-auto" /></p>
<p>I haven't tried going anywhere with <code>Go</code>😉. But the documentation looked straightforward forward so what is the harm in trying?</p>
<h3 id="heading-setup-the-environment">Setup the environment</h3>
<p>I'm not going to rewrite what is already covered in detail in the docs link shared above. Just follow the link and install the needed dependencies. I had installed GNU Make using <a target="_blank" href="https://formulae.brew.sh/formula/make">homebrew</a> which installs it as <code>gmake</code>. So we'll need to replace make commands with <code>gmake</code> (unless you add a <code>gnubin</code> directory to the <code>PATH</code> as mentioned in the link).</p>
<h3 id="heading-choosing-a-provisioner">Choosing a Provisioner</h3>
<p>The first decision point comes when you need to choose a <code>provisioner</code>. As the docs page says, "Provisioners are in essence hooks that get executed before the executable is run by 1Password CLI, and after the executable exits in case any cleanup is needed.". So how a third-party CLI authenticates you needs to be configured here. This is not a one size fits all scenario. You need to go and read the said third-party CLI documentation to figure it out. But I had used the appwrite CLI, it needs Email &amp; Password so this reading documentation advice is not for me.</p>
<p>1Password CLI supports the following provisioners</p>
<pre><code class="lang-go"><span class="hljs-keyword">const</span> (
    APIClientCredentials = sdk.CredentialName(<span class="hljs-string">"API Client Credentials"</span>)
    APIKey               = sdk.CredentialName(<span class="hljs-string">"API Key"</span>)
    APIToken             = sdk.CredentialName(<span class="hljs-string">"API Token"</span>)
    AccessKey            = sdk.CredentialName(<span class="hljs-string">"Access Key"</span>)
    AccessToken          = sdk.CredentialName(<span class="hljs-string">"Access Token"</span>)
    AppPassword          = sdk.CredentialName(<span class="hljs-string">"App Password"</span>)
    AppToken             = sdk.CredentialName(<span class="hljs-string">"App Token"</span>)
    AuthToken            = sdk.CredentialName(<span class="hljs-string">"Auth Token"</span>)
    CLIToken             = sdk.CredentialName(<span class="hljs-string">"CLI Token"</span>)
    Credential           = sdk.CredentialName(<span class="hljs-string">"Credential"</span>)
    Credentials          = sdk.CredentialName(<span class="hljs-string">"Credentials"</span>)
    DatabaseCredentials  = sdk.CredentialName(<span class="hljs-string">"Database Credentials"</span>)
    LoginDetails         = sdk.CredentialName(<span class="hljs-string">"Login Details"</span>)
    PersonalAPIToken     = sdk.CredentialName(<span class="hljs-string">"Personal API Token"</span>)
    PersonalAccessToken  = sdk.CredentialName(<span class="hljs-string">"Personal Access Token"</span>)
    RegistryCredentials  = sdk.CredentialName(<span class="hljs-string">"Registry Credentials"</span>)
    SecretKey            = sdk.CredentialName(<span class="hljs-string">"Secret Key"</span>)
    UserLogin            = sdk.CredentialName(<span class="hljs-string">"User Login"</span>)
)
</code></pre>
<p>Appwrite CLI uses email/password for authentication so, <code>LoginDetails</code> and <code>UserLogin</code> look like the two promising choices. So I picked one of them. The next step asks for an example credential which was confusing as it didn't say whether this example is for the login or the password. Anyway, after putting some random string there we get the template code generated. And we've got a new problem</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688156660145/455ad862-10ef-45b9-959b-e7530f934982.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688156681910/525ce48a-86fc-4c15-a10b-45cff91c0a41.png" alt class="image--center mx-auto" /></p>
<p>Both of the above choices generate code with errors. These field names are not defined. And also only one <code>fieldname</code> is created.</p>
<pre><code class="lang-go">Fields: []schema.CredentialField{
  {
    Name:                fieldname.Login,
    MarkdownDescription: <span class="hljs-string">"Login used to authenticate to Appwrite."</span>,
    Secret:              <span class="hljs-literal">true</span>,
    Composition: &amp;schema.ValueComposition{
      Length: <span class="hljs-number">21</span>,
      Charset: schema.Charset{
        Lowercase: <span class="hljs-literal">true</span>,
        Digits:    <span class="hljs-literal">true</span>,
      },
    },
  },
},
</code></pre>
<p>After defining the missing fieldsname, and adding a new field (password) in the array, tried validating, building and initing the plugin (using <code>op plugin init appwrite</code>). It asked for the login and password and created an entry in the 1Password app. The moment of truth is here</p>
<p><img src="https://media.giphy.com/media/KJBgowpC3j0U8MNxkQ/giphy.gif" alt class="image--center mx-auto" /></p>
<p>Run <code>appwrite login</code>. Wait for the email and password to be supplied by the 1Password App, <strong><em>and it never happens</em></strong>.</p>
<p>This is the moment I realized something is not right. Tried looking for a config file in the system and found one at <code>~/.appwrite/prefs.json</code>. It doesn't store a password just a cookie. Essentially what the CLI does is, take the login credentials from you interactively, and then make an auth call and store the auth token in that file for future use. We could store the cookie in 1Password but then who takes care of refreshing the token?</p>
<p>Before giving up, this is where I got in touch with the 1Password team. Learnt that interactive login is not supported, and the following verbatim response "If your file supports provisioning the credentials in a file, you can use the SDK’s file provisioner when building the plugin.".</p>
<p>Interesting! File provisioner is mentioned in the docs but mostly you're going to miss reading it. Well, time to eat my words and go to appwrite CLI docs, and read it, of course. Found that there is a CI mode that works using the API Key. The only limitation is, it works with a single project at a time. And also the supported CLI commands depends on the permissions granted to the API Key.</p>
<pre><code class="lang-bash">appwrite client \
    --endpoint https://cloud.appwrite.io/v1 \
    --projectId [YOUR_PROJECT_ID] \
    --key YOUR_API_KEY
</code></pre>
<p>On running this command from the home dir of my system, it added the endpoint and the API key to the <code>prefs.json</code> mentioned earlier, but it also created a new <code>appwrite.json</code> file mentioning the project ID in the home dir (from where the command was executed). So the <code>prefs.json</code> doesn't contain the project id. If you've used appwrite then you'll know that the project folder also contains an <code>appwrite.json</code> file mentioning the project id. This hints that we only need to store the endpoint and the API key in 1Password, and then run the appwrite commands from the project directory itself (1Password supports tying up credentials to a particular dir).</p>
<p>After all this detective work, we are finally ready to implement the plugin.</p>
<h3 id="heading-implementing-the-plugin">Implementing the plugin</h3>
<p>After deleting and running the <code>gmake new-plugin</code> command again, this time I selected API Key as the credential type. Modified the generate <code>api_key.go</code> file as shown below</p>
<p><strong>Required Fields, Default Provisioner &amp; Importer</strong></p>
<pre><code class="lang-go">Fields: []schema.CredentialField{
    {
        Name:                fieldname.APIKey,
        MarkdownDescription: <span class="hljs-string">"API Key used to authenticate to Appwrite."</span>,
        Secret:              <span class="hljs-literal">true</span>,
        Composition: &amp;schema.ValueComposition{
            Length: <span class="hljs-number">256</span>,
            Charset: schema.Charset{
                Lowercase: <span class="hljs-literal">true</span>,
                Digits:    <span class="hljs-literal">true</span>,
            },
        },
    },
    {
        Name:                fieldname.Endpoint,
        MarkdownDescription: <span class="hljs-string">"Appwrite server endpoint."</span>,
        Secret:              <span class="hljs-literal">false</span>,
        Optional:            <span class="hljs-literal">false</span>,
    },
},
DefaultProvisioner: provision.TempFile(appwriteConfig, provision.AtFixedPath(ConfigPath())),
Importer: importer.TryAll(
    TryAppwriteConfigFile(),
)}
</code></pre>
<p>Note that we're creating the temp file at a fixed path <code>provision.AtFixedPath(ConfigPath()))</code>. Where ConfigPath is as shown below</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">ConfigPath</span><span class="hljs-params">()</span> <span class="hljs-title">string</span></span> {
    configDir, err := os.UserHomeDir()
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-string">"~/.appwrite/prefs.json"</span>
    }

    <span class="hljs-keyword">return</span> configDir + <span class="hljs-string">"/.appwrite/prefs.json"</span>
}
</code></pre>
<p><strong>appwriteConfig function</strong></p>
<p>This function takes the credentials from 1Password and converts that into a JSON object so that a temp config file can be created for the appwrite CLI</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">appwriteConfig</span><span class="hljs-params">(in sdk.ProvisionInput)</span> <span class="hljs-params">([]<span class="hljs-keyword">byte</span>, error)</span></span> {
    <span class="hljs-comment">// Create config object from the incoming fields</span>
    config := Config{
        APIKey:   in.ItemFields[fieldname.APIKey],
        Endpoint: in.ItemFields[fieldname.Endpoint],
    }

    <span class="hljs-comment">// Covert the content to a JSON string</span>
    contents, err := json.MarshalIndent(&amp;config, <span class="hljs-string">""</span>, <span class="hljs-string">"  "</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err
    }

    <span class="hljs-comment">// Convert the string to bytes, to be written to the temp file</span>
    <span class="hljs-keyword">return</span> []<span class="hljs-keyword">byte</span>(contents), <span class="hljs-literal">nil</span>
}

<span class="hljs-comment">// The config struct (notice the differnt json key names)</span>
<span class="hljs-keyword">type</span> Config <span class="hljs-keyword">struct</span> {
    APIKey   <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"key"`</span>
    Endpoint <span class="hljs-keyword">string</span> <span class="hljs-string">`json:"endpoint"`</span>
}
</code></pre>
<p><strong>TryAppwriteConfigFile function</strong></p>
<p>This function reads an existing prefs.json file at the config path and shows a prompt to the user that these credentials can be imported into 1Password when they run any of the appwrite commands requiring authentication.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TryAppwriteConfigFile</span><span class="hljs-params">()</span> <span class="hljs-title">sdk</span>.<span class="hljs-title">Importer</span></span> {
    <span class="hljs-keyword">return</span> importer.TryFile(ConfigPath(), <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt)</span></span> {
        <span class="hljs-keyword">var</span> config Config
        <span class="hljs-keyword">if</span> err := contents.ToJSON(&amp;config); err != <span class="hljs-literal">nil</span> {
            out.AddError(err)
            <span class="hljs-keyword">return</span>
        }

        fmt.Println(<span class="hljs-string">"Printing the imported file"</span>)
        fmt.Println(config)

        <span class="hljs-keyword">if</span> config.APIKey == <span class="hljs-string">""</span> {
            <span class="hljs-keyword">return</span>
        }

        <span class="hljs-keyword">if</span> config.Endpoint == <span class="hljs-string">""</span> {
            <span class="hljs-keyword">return</span>
        }

        out.AddCandidate(sdk.ImportCandidate{
            Fields: <span class="hljs-keyword">map</span>[sdk.FieldName]<span class="hljs-keyword">string</span>{
                fieldname.APIKey:   config.APIKey,
                fieldname.Endpoint: config.Endpoint,
            },
        })
    })
}
</code></pre>
<p>That's it. We're done. The only thing remaining is writing some tests, and configuring which appwrite commands don't require auth. The latter can be done in <code>appwrite.go</code> file as shown below</p>
<pre><code class="lang-go">NeedsAuth: needsauth.IfAll(
    needsauth.NotForHelpOrVersion(),
    needsauth.NotWithoutArgs(),
    needsauth.NotWhenContainsArgs(<span class="hljs-string">"client"</span>),
    needsauth.NotWhenContainsArgs(<span class="hljs-string">"login"</span>),
    needsauth.NotWhenContainsArgs(<span class="hljs-string">"logout"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"deploy"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"projects"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"storage"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"teams"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"users"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"account"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"avatars"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"functions"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"databases"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"health"</span>),
    needsauth.NotForExactArgs(<span class="hljs-string">"locale"</span>),
),
</code></pre>
<p>The plugin tests can be modified in <code>api_key_test.go</code> file.</p>
<pre><code class="lang-go"><span class="hljs-comment">// Test whether our file provision is working</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestAPIKeyProvisioner</span><span class="hljs-params">(t *testing.T)</span></span> {
    plugintest.TestProvisioner(t, APIKey().DefaultProvisioner, <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]plugintest.ProvisionCase{
        <span class="hljs-string">"temp file"</span>: {
            ItemFields: <span class="hljs-keyword">map</span>[sdk.FieldName]<span class="hljs-keyword">string</span>{
                fieldname.APIKey:   <span class="hljs-string">"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample"</span>,
                fieldname.Endpoint: <span class="hljs-string">"http://localhost/v1"</span>,
            },
            ExpectedOutput: sdk.ProvisionOutput{
                Files: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]sdk.OutputFile{
                    ConfigPath(): {
                        Contents: []<span class="hljs-keyword">byte</span>(plugintest.LoadFixture(t, <span class="hljs-string">"import_prefs.json"</span>)),
                    },
                },
            },
        },
    })
}

<span class="hljs-comment">// Test the importer</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">TestAPIKeyImporter</span><span class="hljs-params">(t *testing.T)</span></span> {
    plugintest.TestImporter(t, APIKey().Importer, <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]plugintest.ImportCase{
        <span class="hljs-string">"Appwrite prefs file"</span>: {
            Files: <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>{
                ConfigPath(): plugintest.LoadFixture(t, <span class="hljs-string">"import_prefs.json"</span>),
            },
            ExpectedCandidates: []sdk.ImportCandidate{
                {
                    Fields: <span class="hljs-keyword">map</span>[sdk.FieldName]<span class="hljs-keyword">string</span>{
                        fieldname.APIKey:   <span class="hljs-string">"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample"</span>,
                        fieldname.Endpoint: <span class="hljs-string">"http://localhost/v1"</span>,
                    },
                },
            },
        },
    })
}
</code></pre>
<p>To run the tests we can run <code>gmake test</code> or <code>make test</code> command.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688160973074/263bfdc5-70cd-4580-8d52-5c2045295ddb.png" alt class="image--center mx-auto" /></p>
<p>And this gives us the green light we needed.</p>
<p><img src="https://media.giphy.com/media/eIG0HfouRQJQr1wBzz/giphy.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-part-2-creating-the-app">Part 2: Creating the app</h2>
<p>We start with the basics here. Scaffold the app using the below command</p>
<pre><code class="lang-bash">npx nuxi@latest init &lt;project-name&gt;
</code></pre>
<p>Since we're using <code>Passage</code> for auth, install <code>@passageidentity/passage-elements</code>. Now here is a catch, to make use of the appwrite client SDK we need to create a session using email/password or through supported OAuth providers. Both of these are ruled out as passage auth doesn't need a password, and is also not a supported OAuth provider. I still installed the appwrite SDK as I used its locale services in my app flow.</p>
<pre><code class="lang-json">yarn add @passageidentity/passage-elements appwrite
</code></pre>
<p>We definitely need the server SDKs of both of the above. <code>@passageidentity/passage-node</code> for verifying the authenticity of incoming client requests, and <code>node-appwrite</code> for interacting with the appwrite database.</p>
<pre><code class="lang-json">yarn add node-appwrite @passageidentity/passage-node
</code></pre>
<p>For styling and readymade components, I decided to use <a target="_blank" href="https://ui.nuxtlabs.com/">NuxtLabs UI</a>. Now we're all set to start writing the app.</p>
<h3 id="heading-passage-auth">Passage Auth</h3>
<p>Passage provides readymade custom web components which handle the end-to-end auth for you. To use it in a Nuxt (or Vue) app, we need to register these components as custom elements in the Nuxt config file. If we do not do that then we'll get warnings in our browser console. Add the following inside the <code>defineNuxtConfig</code> input object in your <code>nuxt.config.ts</code> file.</p>
<pre><code class="lang-typescript">vue: {
  compilerOptions: {
    isCustomElement: <span class="hljs-function">(<span class="hljs-params">tag</span>) =&gt;</span> tag.startsWith(<span class="hljs-string">'passage-'</span>),
  },
},
</code></pre>
<p>Now I tried to use the <code>&lt;passage-auth&gt;</code> component directly in a page template after adding the necessary import</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">'@passageidentity/passage-elements/passage-auth'</span>;
</code></pre>
<p>But it doesn't work, you'll get the below error</p>
<pre><code class="lang-bash">Cannot use import statement outside a module
</code></pre>
<p>This happens because Nuxt is running in SSR mode and web components are not available server side. You can get more information on this passage documentation for <a target="_blank" href="https://docs.passage.id/frontend/examples-by-framework/next.js#:~:text=Calling%20the%20import,error%20being%20thrown.">NextJs here</a>. I tried following the same approach outlined in the link, load the component client side in the <code>onMounted</code> hook</p>
<pre><code class="lang-typescript">onMounted(<span class="hljs-function">()=&gt;</span>{
  <span class="hljs-built_in">require</span>(<span class="hljs-string">'@passageidentity/passage-elements/passage-auth'</span>);
});
</code></pre>
<p>But sadly this also is a no-go. Nuxt3 ships with <code>Vite</code> bundler by default, and using <code>require</code> is not supported in <code>Vite</code>. No issues, simply replace <code>require</code> with <code>import</code> there you might say, but we get typescript error <code>"An import declaration can only be used at the top level of a namespace or module"</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688183645206/4529834c-8f69-4acc-b186-06affe2b9394.png" alt class="image--center mx-auto" /></p>
<p>So what is the solution? To resolve this I created separate components which only contain passage elements, and load this new component only on the client side using the &lt;ClientOnly&gt; option provided by Nuxt.</p>
<p>So a SignUp component might look like this</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">passage-register</span> <span class="hljs-attr">:app-id</span>=<span class="hljs-string">"passageAppId"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>And then you use this component on a <code>sign-up</code> page as shown below</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">ClientOnly</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">UContainer</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">UCard</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"max-w-md mx-auto mt-8 text-center"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-3xl font-medium"</span>&gt;</span>Sign up<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">sign-up</span> /&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm font-medium text-gray-500 dark:text-gray-300 text-center"</span>
        &gt;</span>
          Already have an account?
          <span class="hljs-tag">&lt;<span class="hljs-name">UButton</span> <span class="hljs-attr">variant</span>=<span class="hljs-string">"link"</span> <span class="hljs-attr">:padded</span>=<span class="hljs-string">"false"</span> <span class="hljs-attr">to</span>=<span class="hljs-string">"/sign-in"</span>&gt;</span>
            Sign in
          <span class="hljs-tag">&lt;/<span class="hljs-name">UButton</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">UCard</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">UContainer</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">ClientOnly</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>After making the above adjustments it works all right.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Please note that I'm using <code>&lt;passage-register&gt;</code> and <code>&lt;passage-login&gt;</code> components instead of the <code>&lt;passage-auth&gt;</code> because I've some custom registration fields.</div>
</div>

<h3 id="heading-creating-user-andamp-family-accounts">Creating user &amp; family accounts</h3>
<p>Since we want to be able to invite family members (or create accounts for them ourselves) to the app, we need to roll out our own database schema to connect their accounts. But appwrite supports the creation of Teams and linking user accounts to them. To use this feature we will need to create appwrite users and then link those users to a team. How do we correlate the Passage users to the appwrite users?</p>
<p>Passage allows us to listen to new account creations or a fresh login, by attaching a callback (<code>onSuccess</code>) to passage elements. But there is a catch, we can't simply bind the callback to the pasasge element using the <code>"@"</code> syntax of vue. I needed to use the <code>"."</code> syntax to make it work. You can read more about this <a target="_blank" href="https://vuejs.org/guide/extras/web-components.html#passing-dom-properties">here</a>.</p>
<p>This is my final SignUp component which uses the passage callback to make an API call to a Nuxt serverless API</p>
<pre><code class="lang-typescript">&lt;script setup lang=<span class="hljs-string">"ts"</span>&gt;
<span class="hljs-keyword">import</span> <span class="hljs-string">'@passageidentity/passage-elements/passage-register'</span>;
<span class="hljs-keyword">import</span> { authResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@passageidentity/passage-elements'</span>;
<span class="hljs-keyword">const</span> { getUser } = usePassageUser();

<span class="hljs-keyword">const</span> {
  <span class="hljs-keyword">public</span>: { passageAppId },
} = useRuntimeConfig();

<span class="hljs-keyword">const</span> onRegistrationDone = <span class="hljs-keyword">async</span> (authResult: authResult) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> $fetch(<span class="hljs-string">'/api/users'</span>, {
      method: <span class="hljs-string">'post'</span>,
    });

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'got response from user create'</span>, res);
    <span class="hljs-keyword">await</span> getUser(authResult.auth_token);
    navigateTo(<span class="hljs-string">'/onboarding'</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'failed to create user in appwrite'</span>);
  }
};
&lt;/script&gt;

&lt;template&gt;
  &lt;passage-register :app-id=<span class="hljs-string">"passageAppId"</span> .onSuccess=<span class="hljs-string">"onRegistrationDone"</span> /&gt;
&lt;/template&gt;
</code></pre>
<p>And this is the <code>/api/users</code> code. We receive the request from the client, verify its authenticity using the passage's node SDK, and then create a new user in appwrite using their node SDK.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { UserObject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@passageidentity/passage-node'</span>;
<span class="hljs-keyword">import</span> { protectRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">'../usePassage'</span>;
<span class="hljs-keyword">import</span> { useAppwrite } <span class="hljs-keyword">from</span> <span class="hljs-string">'../useAppwrite'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'incoming post event for api/users/'</span>, event);

  <span class="hljs-keyword">await</span> protectRoute(event);

  <span class="hljs-keyword">const</span> user = event.context.auth.user <span class="hljs-keyword">as</span> UserObject;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`got some auth user:`</span>, user);

  <span class="hljs-keyword">const</span> { $users } = useAppwrite();

  <span class="hljs-comment">// create an appwrite user with the same ID returned by Passage</span>
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> $users.create(
    user.id,
    user.email,
    <span class="hljs-literal">undefined</span>,
    <span class="hljs-literal">undefined</span>,
    user.user_metadata?.name <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>
  );

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'res of user create'</span>, res);

  <span class="hljs-keyword">return</span> {
    status: <span class="hljs-string">'ok'</span>,
  };
});
</code></pre>
<p>And this is the <code>protectRoute</code> function to verify the authenticity of the request</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { H3Event } <span class="hljs-keyword">from</span> <span class="hljs-string">'h3'</span>;
<span class="hljs-keyword">import</span> Passage, { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'@passageidentity/passage-node'</span>;

<span class="hljs-keyword">let</span> _passage: Passage | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;

<span class="hljs-keyword">const</span> getPassage = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">if</span> (!_passage) {
    <span class="hljs-keyword">const</span> {
      passageApiKey,
      <span class="hljs-keyword">public</span>: { passageAppId },
    } = useRuntimeConfig();

    <span class="hljs-keyword">const</span> passageConfig = {
      appID: passageAppId,
      apiKey: passageApiKey,
    };

    _passage = <span class="hljs-keyword">new</span> Passage(passageConfig);
  }

  <span class="hljs-keyword">return</span> _passage;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> protectRoute = <span class="hljs-keyword">async</span> (event: H3Event) =&gt; {
  <span class="hljs-keyword">const</span> passage = getPassage();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> userId = <span class="hljs-keyword">await</span> passage.authenticateRequest(event.node.req);

    <span class="hljs-keyword">if</span> (userId) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'request authenticated'</span>, userId);

      <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> passage.user.get(userId);
      event.context.auth = { user };

      <span class="hljs-keyword">return</span>;
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'failed to authenticate request'</span>, error);
  }

  <span class="hljs-keyword">throw</span> createError({
    statusCode: <span class="hljs-number">401</span>,
    message: <span class="hljs-string">'Unauthorized'</span>,
  });
};
</code></pre>
<p><strong>Handle adding family members</strong></p>
<p>During the onboarding of a new user, the app asks them to create a family account and add family members to it. But to invite the family members they should have a passage account, right? That is the approach we've followed above, the appwrite user id is provided by Passage. To handle this, we first create Passage users using the passage-node SDK, get the user ids, create corresponding appwrite users, and then finally link these users to the created team/family.</p>
<p>The frontend function which adds the family members</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> addMembers = <span class="hljs-keyword">async</span> () =&gt; {
  loading.value = <span class="hljs-literal">true</span>;
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res: <span class="hljs-built_in">any</span> = <span class="hljs-keyword">await</span> $fetch(<span class="hljs-string">'/api/families'</span>, {
      method: <span class="hljs-string">'post'</span>,
      body: {
        <span class="hljs-keyword">type</span>: <span class="hljs-string">'ADD_MEMBERS'</span>,
        familyId: metadata?.family_id,
        members: members.value,
        onboardStep: metadata?.onboard_step,
      },
    });

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'response of add members'</span>, res);
    <span class="hljs-keyword">if</span> (res.user &amp;&amp; user.value) {
      user.value.user_metadata = res.user.user_metadata;
    }

    emit(<span class="hljs-string">'membersAdded'</span>);
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'error is adding members'</span>, error);
  }

  loading.value = <span class="hljs-literal">false</span>;
};
</code></pre>
<p>The <code>/api/families</code> code</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { UserObject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@passageidentity/passage-node'</span>;
<span class="hljs-keyword">import</span> { protectRoute, createUser, updateUser } <span class="hljs-keyword">from</span> <span class="hljs-string">'../usePassage'</span>;
<span class="hljs-keyword">import</span> { useAppwrite } <span class="hljs-keyword">from</span> <span class="hljs-string">'../useAppwrite'</span>;

<span class="hljs-keyword">const</span> addFamilyMembers = <span class="hljs-keyword">async</span> (
  userId: <span class="hljs-built_in">string</span>,
  urlOrigin: <span class="hljs-built_in">string</span>,
  data: <span class="hljs-built_in">any</span>
) =&gt; {
  <span class="hljs-keyword">if</span> (!data.familyId || !data.members || !data.members.length) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">400</span>,
      message: <span class="hljs-string">'Missing familyId or members to add'</span>,
    });
  }

  <span class="hljs-keyword">const</span> { $users, $teams } = useAppwrite();
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> userPromises = [];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> member <span class="hljs-keyword">of</span> data.members) {
      <span class="hljs-comment">// Create Passage Users</span>
      userPromises.push(
        createUser(member.email, {
          name: member.name,
          family_id: data.familyId,
          roles: member.role,
          onboard_step: <span class="hljs-string">'done'</span>,
        })
      );
    }

    <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(userPromises);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'created new users in passage: '</span>, users);

    <span class="hljs-keyword">const</span> appwriteUserPromises = [];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> users) {
      appwriteUserPromises.push(
        $users.create(
          user.id,
          user.email,
          <span class="hljs-literal">undefined</span>,
          <span class="hljs-literal">undefined</span>,
          user.user_metadata?.name <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>
        )
      );
    }

    <span class="hljs-keyword">const</span> appwriteUsers = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(appwriteUserPromises);

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'created new users in appwrite: '</span>, appwriteUsers);

    <span class="hljs-keyword">const</span> memberPromises = [];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> users) {
      memberPromises.push(
        $teams.createMembership(
          data.familyId,
          [user.user_metadata?.roles <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>],
          <span class="hljs-string">`<span class="hljs-subst">${urlOrigin}</span>/join-team`</span>,
          user.email,
          user.id,
          user.phone,
          user.user_metadata?.name <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>
        )
      );
    }

    <span class="hljs-keyword">const</span> memberships = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(memberPromises);

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'created memberships in appwrite'</span>, memberships);

    <span class="hljs-keyword">let</span> updatedUser;
    <span class="hljs-keyword">if</span> (data.onboardStep === <span class="hljs-string">'family'</span>) {
      <span class="hljs-comment">// updat passage user metadata</span>
      updatedUser = <span class="hljs-keyword">await</span> updateUser(userId, {
        onboard_step: <span class="hljs-string">'jar'</span>,
      });
    }

    <span class="hljs-keyword">return</span> {
      user: updatedUser,
    };
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'failed to add members'</span>, error);
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">500</span>,
      message: <span class="hljs-string">'Failed to add family members'</span>,
    });
  }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineEventHandler(<span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'incoming post event for api/families/'</span>);

  <span class="hljs-keyword">await</span> protectRoute(event);

  <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> readBody(event);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'body'</span>, body);

  <span class="hljs-keyword">if</span> (!body.type || ![<span class="hljs-string">'CREATE'</span>, <span class="hljs-string">'ADD_MEMBERS'</span>].includes(body.type)) {
    <span class="hljs-keyword">throw</span> createError({
      statusCode: <span class="hljs-number">400</span>,
      message: <span class="hljs-string">'Missing or unsupported event type'</span>,
    });
  }

  <span class="hljs-keyword">const</span> user = event.context.auth.user <span class="hljs-keyword">as</span> UserObject;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`got some auth user: <span class="hljs-subst">${user}</span>`</span>);

  <span class="hljs-keyword">const</span> origin = getHeader(event, <span class="hljs-string">'origin'</span>);

  <span class="hljs-keyword">let</span> data;
  <span class="hljs-keyword">if</span> (body.type === <span class="hljs-string">'CREATE'</span>) {
    data = <span class="hljs-keyword">await</span> createFamily(user.id, origin || <span class="hljs-string">''</span>, body);
  } <span class="hljs-keyword">else</span> {
    data = <span class="hljs-keyword">await</span> addFamilyMembers(user.id, origin || <span class="hljs-string">''</span>, body);
  }

  <span class="hljs-keyword">return</span> {
    status: <span class="hljs-string">'ok'</span>,
    ...data,
  };
});
</code></pre>
<p>These are the passage functions to create and update a user.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Passage, { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'@passageidentity/passage-node'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createUser = <span class="hljs-keyword">async</span> (email: <span class="hljs-built_in">string</span>, metadata: Metadata) =&gt; {
  <span class="hljs-keyword">const</span> passage = getPassage();

  <span class="hljs-keyword">let</span> userData = <span class="hljs-keyword">await</span> passage.user.create({ email, user_metadata: metadata });
  <span class="hljs-built_in">console</span>.log(userData);

  <span class="hljs-keyword">return</span> userData;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> updateUser = <span class="hljs-keyword">async</span> (userId: <span class="hljs-built_in">string</span>, data: Metadata) =&gt; {
  <span class="hljs-keyword">const</span> passage = getPassage();

  <span class="hljs-keyword">let</span> userData = <span class="hljs-keyword">await</span> passage.user.update(userId, { user_metadata: data });
  <span class="hljs-built_in">console</span>.log(userData);

  <span class="hljs-keyword">return</span> userData;
};
</code></pre>
<p>As you can see, we're storing important user metadata inside the passage user object itself. Barring the name, none of the other fields are visible to the user. We use the familyId field from this metadata to add members to the same family as the calling user.</p>
<pre><code class="lang-typescript">{
  name: member.name,
  family_id: data.familyId,
  roles: member.role,
  onboard_step: <span class="hljs-string">'done'</span>,
}
</code></pre>
<h3 id="heading-appwrite-functions">Appwrite functions</h3>
<p>The app also uses some appwrite functions to handle important database events. The shell plugin that we created in the first part gets verified for creating/deploying these functions. The database events that the app handles currently include</p>
<ol>
<li><p>Jar created event: Whenever a new jar is created and it has auto credit enabled, then we update the jar object and set its nextMoneyAt field. This is the field that is queried by the cron job for auto-crediting the configured amount to the jar.</p>
</li>
<li><p>Transaction event: On every transaction (create or update), we update the corresponding jar balance. If the transaction was done by a child, it will be pending and the jar won't be updated. Once the said transaction is approved (transaction update event) by a family member (role: member), then only the jar gets updated.</p>
</li>
<li><p>Cron job: This job runs every day at midnight and auto credits the configured auto credit amount to the eligible jars.</p>
</li>
</ol>
<h3 id="heading-app-screenshots">App Screenshots</h3>
<p>The rest of the app follows the same principle. All app data is fetched from Nuxt serverless APIs where every API call is authenticated by the passage-node SDK. Here are some of the screenshots from the app.</p>
<p>For styling the passage elements, I had to set the following CSS variables</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">passage-register</span>,
<span class="hljs-selector-tag">passage-login</span> {
  <span class="hljs-attribute">--passage-container-background-color</span>: transparent;
  <span class="hljs-attribute">--passage-body-text-color</span>: <span class="hljs-number">#ffffff</span>;
  <span class="hljs-attribute">--passage-primary-color</span>: <span class="hljs-number">#fbbf24</span>;
  <span class="hljs-attribute">--passage-onprimary-color</span>: <span class="hljs-number">#0f172a</span>;
  <span class="hljs-attribute">--passage-hover-color</span>: <span class="hljs-number">#f59e0b</span>;
  <span class="hljs-attribute">--passage-button-font-weight</span>: <span class="hljs-number">500</span>;
  <span class="hljs-attribute">--passage-container-max-width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">--passage-container-padding</span>: <span class="hljs-number">30px</span> <span class="hljs-number">0</span> <span class="hljs-number">10px</span>;
  <span class="hljs-attribute">--passage-button-width</span>: <span class="hljs-number">100%</span>;
  <span class="hljs-attribute">--passage-control-border-color</span>: <span class="hljs-number">#6b6b6b</span>;
  <span class="hljs-attribute">--passage-otp-input-background-color</span>: <span class="hljs-number">#434343</span>;
}
</code></pre>
<p><strong>Sign up screen</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187373249/76c11c70-5027-4877-a728-c513995b70e1.png" alt class="image--center mx-auto" /></p>
<p><strong>Sign in screen</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187570623/080ed958-8923-42e3-b1c5-bc03a0b9ac58.png" alt class="image--center mx-auto" /></p>
<p><strong>Sign in code</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187663367/840f6a6e-aadc-4cca-9cc7-ba4ca7230358.png" alt class="image--center mx-auto" /></p>
<p><strong>App dashboard</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187447765/e8c785ff-8264-4106-9621-21eb1fab5ebd.png" alt class="image--center mx-auto" /></p>
<p><strong>Family screen</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187899194/5f08fe85-092c-4cf7-bcf0-896afcd1d63f.png" alt class="image--center mx-auto" /></p>
<p><strong>Transactions screen</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688187973664/0fbc61de-fa7d-43e3-b18c-174d0655636f.png" alt class="image--center mx-auto" /></p>
<p><strong>User profile screen</strong></p>
<p>This screen uses the <code>&lt;passage-profile&gt;</code> component provided by the passage's client SDK. We can change our profile update operation from here.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688188104373/0d6b79ca-bf7e-4b25-aecc-aac2af48d7a8.png" alt class="image--center mx-auto" /></p>
<p>But it seems there is a bug in profile updation which the Passage team needs to look at. This app uses 3 other hidden metadata fields, and when you try to update the name it throws an error as shown below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688194278102/e9a25f5b-8511-463c-b6cb-8461791e0b32.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688194304142/180dd719-7d5b-4ac4-9f19-403bb2506ba1.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688194331869/ed3c8ec4-168d-4c52-b77a-61f9d6dcda88.png" alt class="image--center mx-auto" /></p>
<p>The error message is clear enough, but the app doesn't have any control on this component. This can be fixed by the Passage team only.</p>
<p><strong>Create jar</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688189746650/a45d7db4-dc6f-49e8-8015-15d0327f3491.png" alt class="image--center mx-auto" /></p>
<p><strong>Add family members</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688189787580/13e01a97-8906-4927-a921-6c1187ef0a21.png" alt class="image--center mx-auto" /></p>
<p><strong>Make transaction</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688189838243/7d3955f1-8cb8-45e9-b7ee-c63cee1016c2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-part-3-automating-the-env-file">Part 3: Automating the env file</h2>
<p>As this app utilizes 1Password in a big way, I decided to make use of the 1Password CLI to handle the app env file as well. Below is a simple script that reads an env file and creates a 1Password item in the specified vault and replaces field values with the respective 1Password secrets.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

<span class="hljs-comment"># Parse command-line arguments</span>
<span class="hljs-keyword">while</span> [ <span class="hljs-variable">$#</span> -gt 0 ]; <span class="hljs-keyword">do</span>
  key=<span class="hljs-string">"<span class="hljs-variable">$1</span>"</span>

  <span class="hljs-keyword">case</span> <span class="hljs-variable">$key</span> <span class="hljs-keyword">in</span>
    --vault)
      vault=<span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past argument</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past value</span>
      ;;
    --item-name)
      item_name=<span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past argument</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past value</span>
      ;;
    --env-file)
      env_file=<span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past argument</span>
      <span class="hljs-built_in">shift</span> <span class="hljs-comment"># past value</span>
      ;;
    *)
      <span class="hljs-comment"># unknown option</span>
      <span class="hljs-built_in">shift</span>
      ;;
  <span class="hljs-keyword">esac</span>
<span class="hljs-keyword">done</span>

<span class="hljs-comment"># Check if the --vault, --item-name, and --env-file parameters were provided</span>
<span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"<span class="hljs-variable">$vault</span>"</span> ] || [ -z <span class="hljs-string">"<span class="hljs-variable">$item_name</span>"</span> ] || [ -z <span class="hljs-string">"<span class="hljs-variable">$env_file</span>"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"Please provide --vault, --item-name, and --env-file parameters."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if the specified .env file exists</span>
<span class="hljs-keyword">if</span> [ ! -f <span class="hljs-string">"<span class="hljs-variable">$env_file</span>"</span> ]; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"The specified .env file does not exist."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if the 1Password CLI is installed</span>
<span class="hljs-keyword">if</span> ! <span class="hljs-built_in">command</span> -v op &gt; /dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"1Password CLI is not installed. Please install it before running this script."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Check if the item already exists in the vault</span>
<span class="hljs-keyword">if</span> op item get <span class="hljs-string">"<span class="hljs-variable">$item_name</span>"</span> --vault <span class="hljs-string">"<span class="hljs-variable">$vault</span>"</span> &gt; /dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"Item '<span class="hljs-variable">$item_name</span>' already exists in the '<span class="hljs-variable">$vault</span>' vault. Skipping creation."</span>
  <span class="hljs-built_in">exit</span> 0
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Variable to store the fields string</span>
fields=()
skipped_fields=()

<span class="hljs-comment"># Read the .env file line by line</span>
<span class="hljs-keyword">while</span> IFS= <span class="hljs-built_in">read</span> -r line || [ -n <span class="hljs-string">"<span class="hljs-variable">$line</span>"</span> ]; <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># Skip empty lines and comments</span>
  <span class="hljs-keyword">if</span> [ -z <span class="hljs-string">"<span class="hljs-variable">$line</span>"</span> ] || [ <span class="hljs-string">"<span class="hljs-variable">${line#"#"}</span>"</span> != <span class="hljs-string">"<span class="hljs-variable">$line</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">continue</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-comment"># Split the line into key and value</span>
  key=<span class="hljs-string">"<span class="hljs-variable">${line%%=*}</span>"</span>
  value=<span class="hljs-string">"<span class="hljs-variable">${line#*=}</span>"</span>

  <span class="hljs-comment"># Check if the value is already a reference to a 1Password secret</span>
  <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">${value#op://}</span>"</span> != <span class="hljs-string">"<span class="hljs-variable">$value</span>"</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Skipping creation of '<span class="hljs-variable">$key</span>' field as it is already a reference to a 1Password secret."</span>
    skipped_fields+=(<span class="hljs-variable">$line</span>)
    <span class="hljs-built_in">continue</span>
  <span class="hljs-keyword">fi</span>

  <span class="hljs-comment"># Add the key-value pair to the fields array</span>
  fields+=(<span class="hljs-string">"<span class="hljs-variable">$key</span>=<span class="hljs-variable">$value</span>"</span>)
<span class="hljs-keyword">done</span> &lt; <span class="hljs-string">"<span class="hljs-variable">$env_file</span>"</span>

<span class="hljs-comment"># Create the item in 1Password using the op create command</span>
op item create \
  --category Server \
  --title <span class="hljs-string">"<span class="hljs-variable">$item_name</span>"</span> \
  --vault <span class="hljs-string">"<span class="hljs-variable">$vault</span>"</span> \
  <span class="hljs-string">"<span class="hljs-variable">${fields[@]}</span>"</span> \
  --tags <span class="hljs-string">"<span class="hljs-variable">$item_name</span>,env"</span>

<span class="hljs-comment"># Update the .env file with the secret references</span>
<span class="hljs-keyword">for</span> ((i = 0; i &lt; <span class="hljs-variable">${#fields[@]}</span>; i++)); <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># Split the field into key and value</span>
  field=<span class="hljs-string">"<span class="hljs-variable">${fields[i]}</span>"</span>
  IFS=<span class="hljs-string">"="</span> <span class="hljs-built_in">read</span> -r key value &lt;&lt;&lt; <span class="hljs-string">"<span class="hljs-variable">$field</span>"</span>

  fields[i]=<span class="hljs-string">"<span class="hljs-variable">$key</span>=op://<span class="hljs-variable">$vault</span>/<span class="hljs-variable">$item_name</span>/<span class="hljs-variable">$key</span>"</span>
<span class="hljs-keyword">done</span>

final_fields=(<span class="hljs-string">"<span class="hljs-variable">${fields[@]}</span>"</span> <span class="hljs-string">"<span class="hljs-variable">${skipped_fields[@]}</span>"</span>)

<span class="hljs-comment"># Overwrite the .env file with the updated key-value pairs</span>
<span class="hljs-built_in">printf</span> <span class="hljs-string">"%s\n"</span> <span class="hljs-string">"<span class="hljs-variable">${final_fields[@]}</span>"</span> &gt; <span class="hljs-string">"<span class="hljs-variable">$env_file</span>"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Item '<span class="hljs-variable">$item_name</span>' created successfully in the '<span class="hljs-variable">$vault</span>' vault."</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Updated the '<span class="hljs-variable">$env_file</span>' file with the secret references."</span>
</code></pre>
<p>We need to pass the vault name, the item entry name and the env file path while executing the script. Now this env file can be uploaded to the repo along with the rest of the code.</p>
<pre><code class="lang-bash">./save-env.sh --vault AppVaults --item-name FamPro --env-file ./client/.env
</code></pre>
<pre><code class="lang-bash">APPWRITE_ENDPOINT=op://AppVaults/FamPro/APPWRITE_ENDPOINT
APPWRITE_PROJECT_ID=op://AppVaults/FamPro/APPWRITE_PROJECT_ID
APPWRITE_DATABASE_ID=op://AppVaults/FamPro/APPWRITE_DATABASE_ID
APPWRITE_JAR_COLLECTION_ID=op://AppVaults/FamPro/APPWRITE_JAR_COLLECTION_ID
APPWRITE_TRANSACTION_COLLECTION_ID=op://AppVaults/FamPro/APPWRITE_TRANSACTION_COLLECTION_ID
APPWRITE_API_KEY=op://AppVaults/FamPro/APPWRITE_API_KEY
PASSAGE_APP_ID=op://AppVaults/FamPro/PASSAGE_APP_ID
PASSAGE_API_KEY=op://AppVaults/FamPro/PASSAGE_API_KEY
</code></pre>
<p>To load the secrets back into the env file we can use the below command (with the correct file paths)</p>
<pre><code class="lang-bash">op inject -i .env_template -o .env
</code></pre>
<h2 id="heading-supported-features">Supported features</h2>
<p>The app supports the following features at the moment</p>
<ol>
<li><p>Creating a family account.</p>
</li>
<li><p>Adding members to the family with a member or child role. Only a user with a member role can add other members.</p>
</li>
<li><p>Creating multiple jars per user (member or child). Again, only users with a member role can create jars.</p>
</li>
<li><p>Making ad-hoc transactions against a jar. A user with a member role can make transactions in any of the jars in the family. A child can only view and do transactions in their jars.</p>
</li>
<li><p>Transactions done by a user with a child role remain pending. These transactions need to be approved by a user with a member role before the jar balance can be updated.</p>
</li>
</ol>
<h2 id="heading-further-enhancements">Further enhancements</h2>
<ol>
<li><p>Improve the dashboard experience</p>
</li>
<li><p>Allow deletion of member accounts</p>
</li>
<li><p>Allow modification and deletion of transactions</p>
</li>
</ol>
<h2 id="heading-app-source-code-and-link">App source code and link</h2>
<p>The full source code of the app along with the 1Password CLI script can be found here</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/FamPro">https://github.com/ra-jeev/FamPro</a></div>
<p> </p>
<p>The code for the appwrite shell plugin can be checked in <a target="_blank" href="https://github.com/1Password/shell-plugins/pull/336">this PR</a>.</p>
<p>You can play around with the app here: <a target="_blank" href="https://fam-pro.vercel.app/">https://fam-pro.vercel.app/</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Overall it was a great experience building with 1Password, Appwrite and Nuxt3. I did face some challenges in the process outlined above, but we could overcome those. 1Password offers a seamless auth experience and it would be interesting to see what features they add on top of the existing functionality. I also thank the <a target="_blank" href="https://hashnode.com/">Hashnode</a> team for organizing this hackathon.</p>
<p>I hope you enjoyed reading the article. It would mean a lot to me if you can show appreciation by sharing your feedback. If you found any mistake then please let me know in the comments.</p>
<p><em>-- Keep adding the bits, soon you'll have more bytes than you may need. :-)</em></p>
]]></content:encoded></item><item><title><![CDATA[Creating an OpenAI powered Writing Assistant for VS Code]]></title><description><![CDATA[Rabbit holes are many, and as a developer, you keep falling into one or the other during your journey. This is not necessarily a bad thing—granted that it may frustrate you—that is how you learn something extra (the 0.01 of 1.01365). This is one such...]]></description><link>https://rajeev.dev/creating-an-openai-powered-writing-assistant-vs-code-extension</link><guid isPermaLink="true">https://rajeev.dev/creating-an-openai-powered-writing-assistant-vs-code-extension</guid><category><![CDATA[openai]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[vscode extensions]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Mon, 19 Jun 2023 20:19:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1687205937099/b2749874-26ba-49da-9a07-ef174c979231.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Rabbit holes are many, and as a developer, you keep falling into one or the other during your journey. This is not necessarily a bad thing—granted that it may frustrate you—that is how you learn something extra (the 0.01 of 1.01<sup>365</sup>). This is one such story where my urge to create something new pushed me into writing a brand new VS Code extension.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>I've always wondered how a VS Code extension works and what it takes to implement one. Recently this urge got the best of me and I decided to finally create a VS Code extension. But what to build? I like using Hashnode's AI editor for rephrasing texts and other writing assistance, so I thought why not implement a similar functionality for VS Code? Since writing assistance is most useful for a markdown file (which can also work as a blog post), I created a basic writing assistant for markdown (and text) files.</p>
<p>Here is a simple GIF showcasing how the extension works.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687112508387/d8a0ae80-fa3e-4b76-ba45-89146a35ef6b.gif" alt="Write Assist AI working gif" class="image--center mx-auto" /></p>
<p>Ready to dive into how I implemented it? Let's get started.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Before we can start building the extension, we need to gather and prepare the necessary tools. In this case, the needed tools are node, git, <a target="_blank" href="https://yeoman.io/">yeoman</a> and <a target="_blank" href="https://www.npmjs.com/package/generator-code">generator-code</a>. For a newcomer like myself, <a target="_blank" href="https://code.visualstudio.com/api/get-started/your-first-extension">this basic tutorial</a> is perfect. I recommend going through it to learn the fundamentals.</p>
<p>Run <code>yo code</code> command and pick <code>New Extension (js/ts)</code> from the choices as shown below</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687169779387/103a52d9-c518-4e23-8694-fae49d972fce.png" alt="yo code command choices" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687169820685/569c6d97-1a09-4c45-8fe9-9e0e9ddc73a8.png" alt="All steps of the yo code command" class="image--center mx-auto" /></p>
<p>I haven't selected the <code>webpack</code> option yet. We can always do so later on. This is my current directory structure.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687170043976/efee7119-e8fc-4bdd-9640-9507fc7197ed.png" alt="Basic directory structure for a VS Code extension" class="image--center mx-auto" /></p>
<h2 id="heading-implementing-the-extension">Implementing the extension</h2>
<p>We're mostly interested in the <code>package.json</code> and <code>extension.ts</code> files while building the extension. The important fields in the package.json file are</p>
<ol>
<li><p><strong>activationEvents</strong>: When should our extension be activated</p>
</li>
<li><p><strong>main</strong>: The entry point of the extension code (the entry is in the <code>out</code> folder which gets generated when you debug or run the extension)</p>
</li>
<li><p><strong>contributes</strong>: What does this extension contributes to the VS Code commands and settings</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687171000838/5d4626a4-1d24-4809-9080-03883ff31b23.png" alt="package.json fields" class="image--center mx-auto" /></p>
<p>For my extension I wanted an experience similar to the Hashnode AI Editor, so adding commands to the VS Code command palette was not what I was after. What helped me here was the sample extensions directory on <a target="_blank" href="https://github.com/microsoft/vscode-extension-samples/">GitHub</a>. Their <a target="_blank" href="https://github.com/microsoft/vscode-extension-samples/tree/main/code-actions-sample">code-actions</a> sample was exactly what I had in mind (and it targets only the markdown files).</p>
<h3 id="heading-target-markdown-and-text-files">Target Markdown and Text files</h3>
<p>To target specific types of files you need to change the <code>activationEvents</code>. Since we want to work only with <code>Markdown</code> and <code>Text</code> files at the moment, this is what my <code>package.json</code> says</p>
<pre><code class="lang-json"><span class="hljs-comment">// ...</span>
<span class="hljs-string">"activationEvents"</span>: [
  <span class="hljs-string">"onLanguage:markdown"</span>,
  <span class="hljs-string">"onLanguage:plaintext"</span>
],
<span class="hljs-string">"main"</span>: <span class="hljs-string">"./out/extension.js"</span>,
<span class="hljs-string">"contributes"</span>: {},
<span class="hljs-comment">// ...</span>
</code></pre>
<p>This extension will only get activated for the languages mentioned above.</p>
<p>Also, since we don't want any command palette commands, we can remove everything under the <code>contributes</code> key.</p>
<h3 id="heading-showing-the-actions-in-the-light-bulb-menu">Showing the actions in the light bulb menu</h3>
<p>There is one function called <code>activate</code> inside the <code>extension.ts</code> file which is of interest. This is where you need to write your code. Notice that I've removed all the unnecessary boilerplate code</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// This method is called when your extension is activated</span>
<span class="hljs-comment">// Your extension is activated the very first time the </span>
<span class="hljs-comment">// command is executed</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">activate</span>(<span class="hljs-params">context: vscode.ExtensionContext</span>) </span>{}
</code></pre>
<p>There is another complementary function called <code>deactivate</code> which can be used if you need to do any kind of resource cleaning before the extension is deactivated.</p>
<p>If you run/debug the extension now, and create a new markdown file in the new VS Code window which pops up, you won't notice any difference as there is no palette command, and also there is no code in the <code>activate</code> function. Let's change that and add the following in the <code>extension.ts</code> file</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">class</span> MyCodeActionProvider <span class="hljs-keyword">implements</span> vscode.CodeActionProvider {

  provideCodeActions(
    <span class="hljs-built_in">document</span>: vscode.TextDocument,
    range: vscode.Range | vscode.Selection,
    context: vscode.CodeActionContext,
    token: vscode.CancellationToken
  ): vscode.ProviderResult&lt;(vscode.CodeAction | vscode.Command)[]&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'inside the provideCodeActions method'</span>);
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Method not implemented.'</span>);
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">activate</span>(<span class="hljs-params">context: vscode.ExtensionContext</span>) </span>{
  <span class="hljs-keyword">const</span> actionProvider = vscode.languages.registerCodeActionsProvider(
    [<span class="hljs-string">'markdown'</span>, <span class="hljs-string">'plaintext'</span>],
    <span class="hljs-keyword">new</span> MyCodeActionProvider(),
    {
      providedCodeActionKinds: [
        vscode.CodeActionKind.RefactorRewrite,
        vscode.CodeActionKind.QuickFix,
      ],
    }
  );

  context.subscriptions.push(actionProvider);
}
</code></pre>
<p>What we're doing here is informing VS Code about the kind of code actions we're providing which include <code>RefactorRewrite</code> &amp; <code>QuickFix</code>. The actual <code>"commands/code actions"</code> need to be provided by the <code>provideCodeActions</code> method of the <code>MyCodeActionProvider</code> class. If you debug the extension now you should see the below console log in your original project window's debug console.</p>
<pre><code class="lang-plaintext">inside the provideCodeActions method
</code></pre>
<p>We're making progress. Replace the <code>provideCodeActions</code> method's code with the following</p>
<pre><code class="lang-typescript">provideCodeActions(
  <span class="hljs-built_in">document</span>: vscode.TextDocument,
  range: vscode.Range | vscode.Selection,
  context: vscode.CodeActionContext,
  token: vscode.CancellationToken
): vscode.ProviderResult&lt;(vscode.CodeAction | vscode.Command)[]&gt; {
  <span class="hljs-comment">// If there is nothing selected, we won't provide any action</span>
  <span class="hljs-keyword">if</span> (range.isEmpty) {
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// supported actions and their kinds</span>
  <span class="hljs-keyword">const</span> actions = [
    {
      id: <span class="hljs-string">'rephrase'</span>,
      title: <span class="hljs-string">'Rephrase selected text'</span>,
      kind: vscode.CodeActionKind.QuickFix,
    },
    {
      id: <span class="hljs-string">'headlines'</span>,
      title: <span class="hljs-string">'Suggest headlines'</span>,
      kind: vscode.CodeActionKind.QuickFix,
    },
    {
      id: <span class="hljs-string">'professional'</span>,
      title: <span class="hljs-string">'Rewrite in professional tone'</span>,
      kind: vscode.CodeActionKind.RefactorRewrite,
    },
    {
      id: <span class="hljs-string">'casual'</span>,
      title: <span class="hljs-string">'Rewrite in casual tone'</span>,
      kind: vscode.CodeActionKind.RefactorRewrite,
    },
  ];

  <span class="hljs-keyword">const</span> cActions = [];
  <span class="hljs-comment">// prepare the code actions for the above actions</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> action <span class="hljs-keyword">of</span> actions) {
    <span class="hljs-keyword">const</span> cAction = <span class="hljs-keyword">new</span> vscode.CodeAction(action.title, action.kind);
    cAction.command = {
      command: <span class="hljs-string">`my-shiny-extension.<span class="hljs-subst">${action.id}</span>`</span>,
      title: action.title,
      <span class="hljs-built_in">arguments</span>: [action.id],
    };

    cActions.push(cAction);
  }

  <span class="hljs-keyword">return</span> cActions;
}
</code></pre>
<p>Debug/run the extension now and you should see the above actions in the bulb tooltip when you select some text in a markdown/text file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687192729783/5b8b0741-8035-46e9-a57d-39e97d9d7af1.png" alt="code action window intermediate view" class="image--center mx-auto" /></p>
<p>Of course, nothing will happen if you click on any of these actions. This is because we've only created the actions, but haven't written any code to handle them.</p>
<h3 id="heading-handling-the-code-actions">Handling the Code Actions</h3>
<p>To handle the actions we need to register these commands with the VS Code extension context. This can be done inside the <code>activate</code> function. Let's do a little bit of refactoring.</p>
<p>Move the actions out of the <code>provideCodeActions</code> method and make it a class property of <code>MyCodeActionProvider</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> actions = [
  {
    id: <span class="hljs-string">'rephrase'</span>,
    title: <span class="hljs-string">'Rephrase selected text'</span>,
    kind: vscode.CodeActionKind.QuickFix,
  },
  {
    id: <span class="hljs-string">'headlines'</span>,
    title: <span class="hljs-string">'Suggest headlines'</span>,
    kind: vscode.CodeActionKind.QuickFix,
  },
  {
    id: <span class="hljs-string">'professional'</span>,
    title: <span class="hljs-string">'Rewrite in professional tone'</span>,
    kind: vscode.CodeActionKind.RefactorRewrite,
  },
  {
    id: <span class="hljs-string">'casual'</span>,
    title: <span class="hljs-string">'Rewrite in casual tone'</span>,
    kind: vscode.CodeActionKind.RefactorRewrite,
  },
];
</code></pre>
<p>Change its reference inside the <code>provideCodeActions</code> method to</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> action <span class="hljs-keyword">of</span> MyCodeActionProvider.actions) {
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Add a new method <code>handleAction</code> which will handle the actions when a user clicks on them. The <code>actionId</code> argument will be passed by the caller. Remember we had passed <code>arguments: [action.id]</code> while returning the code actions from the <code>providecodeActions</code> method?</p>
<pre><code class="lang-typescript">handleAction(actionId: <span class="hljs-built_in">string</span>) {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`handleAction for <span class="hljs-subst">${actionId}</span>`</span>);
}
</code></pre>
<p>Now change the <code>activate</code> function as shown below</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">activate</span>(<span class="hljs-params">context: vscode.ExtensionContext</span>) </span>{
  <span class="hljs-keyword">const</span> myActionProvider = <span class="hljs-keyword">new</span> MyCodeActionProvider();
  <span class="hljs-keyword">const</span> actionProvider = vscode.languages.registerCodeActionsProvider(
    [<span class="hljs-string">'markdown'</span>, <span class="hljs-string">'plaintext'</span>],
    myActionProvider,
    {
      providedCodeActionKinds: [
        vscode.CodeActionKind.RefactorRewrite,
        vscode.CodeActionKind.QuickFix,
      ],
    }
  );

  context.subscriptions.push(actionProvider);
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> action <span class="hljs-keyword">of</span> MyCodeActionProvider.actions) {
    context.subscriptions.push(
      <span class="hljs-comment">// use the same id which we used in the command field </span>
      <span class="hljs-comment">// of the code actions</span>
      vscode.commands.registerCommand(
        <span class="hljs-string">`my-shiny-extension.<span class="hljs-subst">${action.id}</span>`</span>,
        <span class="hljs-function">(<span class="hljs-params">args</span>) =&gt;</span> myActionProvider.handleAction(args)
      )
    );
  }
}
</code></pre>
<p>For all the actions which we support, we're registering a corresponding command with the VS Code extension context. The command id that we use here must match the command id which we returned from the <code>provideCodeActions</code> method.</p>
<p>If we run/debug the extension now and click any of our actions from the light bulb menu we should see the corresponding console log in the debug console.</p>
<h3 id="heading-integrating-with-the-openai-apis">Integrating with the OpenAI APIs</h3>
<p>Now the only thing remaining is: using the OpenAI APIs to make changes to any written text. Let's get it over with.</p>
<p>Add the OpenAI library to the codebase</p>
<pre><code class="lang-bash">yarn add openai
</code></pre>
<p>Import it into the <code>extension.ts</code> file</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { OpenAIApi, Configuration } <span class="hljs-keyword">from</span> <span class="hljs-string">'openai'</span>;
</code></pre>
<p>And replace the <code>handleAction</code> method's code with the following</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> handleAction(actionId: <span class="hljs-built_in">string</span>) {
  <span class="hljs-keyword">const</span> editor = vscode.window.activeTextEditor;
  <span class="hljs-keyword">if</span> (
    !editor ||
    editor.selection.isEmpty ||
    ![<span class="hljs-string">'rephrase'</span>, <span class="hljs-string">'headlines'</span>, <span class="hljs-string">'professional'</span>, <span class="hljs-string">'casual'</span>].includes(actionId)
  ) {
    <span class="hljs-comment">// return if no active editor, or no active selection </span>
    <span class="hljs-comment">// or if unsupported actionId passed</span>
    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-comment">// Create the OpenAI Service</span>
  <span class="hljs-keyword">const</span> openAiSvc = <span class="hljs-keyword">new</span> OpenAIApi(
    <span class="hljs-keyword">new</span> Configuration({
      apiKey: <span class="hljs-string">'&lt;your_open_ai_api_key&gt;'</span>,
    })
  );

  <span class="hljs-comment">// Get the currently selected text</span>
  <span class="hljs-keyword">const</span> text = editor.document.getText(editor.selection);
  <span class="hljs-comment">// current selection range</span>
  <span class="hljs-keyword">let</span> currRange = editor.selection;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Adding a filler/loading text before making the API call</span>
    <span class="hljs-keyword">const</span> fillerText = <span class="hljs-string">'\n\nThinking...'</span>;
    editor
      .edit(<span class="hljs-function">(<span class="hljs-params">editBuilder</span>) =&gt;</span> {
        <span class="hljs-comment">// insert the filler text after the current selection end</span>
        editBuilder.insert(currRange.end, fillerText);
      })
      .then(<span class="hljs-function">(<span class="hljs-params">success</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (success) {
          <span class="hljs-comment">// Select the filler text now</span>
          editor.selection = <span class="hljs-keyword">new</span> vscode.Selection(
            editor.selection.end.line,
            <span class="hljs-number">0</span>,
            editor.selection.end.line,
            editor.selection.end.character
          );

          <span class="hljs-comment">// store this new selection range</span>
          currRange = editor.selection;
        }
      });

    <span class="hljs-comment">// Create the prompt prefix based on the action id</span>
    <span class="hljs-keyword">let</span> promptPrefix = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">switch</span> (actionId) {
      <span class="hljs-keyword">case</span> <span class="hljs-string">'rephrase'</span>:
        promptPrefix =
          <span class="hljs-string">'Rephrase the following text and make the sentences more clear and readable'</span>;
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> <span class="hljs-string">'headlines'</span>:
        promptPrefix = <span class="hljs-string">'Suggest some short headlines for the following text'</span>;
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> <span class="hljs-string">'professional'</span>:
        promptPrefix =
          <span class="hljs-string">'Make the following text better and rewrite it in a professional tone'</span>;
        <span class="hljs-keyword">break</span>;
      <span class="hljs-keyword">case</span> <span class="hljs-string">'casual'</span>:
        promptPrefix =
          <span class="hljs-string">'Make the following text better and rewrite it in a casual tone'</span>;
        <span class="hljs-keyword">break</span>;
    }

    <span class="hljs-comment">// Make the OpenAI API Call using the desired model and configs</span>
    <span class="hljs-comment">/* eslint-disable @typescript-eslint/naming-convention */</span>
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> openAiSvc.createCompletion({
      model: <span class="hljs-string">'text-davinci-003'</span>,
      prompt: <span class="hljs-string">`<span class="hljs-subst">${promptPrefix}</span>:\n\n<span class="hljs-subst">${text}</span>\n\n`</span>,
      temperature: <span class="hljs-number">0.3</span>,
      max_tokens: <span class="hljs-number">500</span>,
      frequency_penalty: <span class="hljs-number">0.0</span>,
      presence_penalty: <span class="hljs-number">0.0</span>,
      n: <span class="hljs-number">1</span>,
    });
    <span class="hljs-comment">/* eslint-enable @typescript-eslint/naming-convention */</span>

    <span class="hljs-comment">// We'd reuqested for only one result, use that</span>
    <span class="hljs-keyword">let</span> result = response.data.choices[<span class="hljs-number">0</span>].text;
    editor
      .edit(<span class="hljs-function">(<span class="hljs-params">editBuilder</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (result) {
          <span class="hljs-comment">// replace the filler text with the actual result</span>
          editBuilder.replace(
            <span class="hljs-keyword">new</span> vscode.Range(currRange.start, currRange.end),
            result.trim()
          );
        }
      })
      .then(<span class="hljs-function">(<span class="hljs-params">success</span>) =&gt;</span> {
        <span class="hljs-keyword">if</span> (success) {
          <span class="hljs-comment">// Select the resulting text (the text can be longer</span>
          <span class="hljs-comment">// and span over multiple lines, so we treat it </span>
          <span class="hljs-comment">// appropriately to make a complete selection)</span>
          editor.selection = <span class="hljs-keyword">new</span> vscode.Selection(
            currRange.start.line,
            currRange.start.character,
            currRange.end.line,
            editor.document.lineAt(currRange.end.line).text.length
          );

          <span class="hljs-keyword">return</span>;
        }
      });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(error);
  }

  <span class="hljs-comment">// In case of API error, show an error text instead</span>
  editor.edit(<span class="hljs-function">(<span class="hljs-params">editBuilder</span>) =&gt;</span> {
    editor.selection = <span class="hljs-keyword">new</span> vscode.Selection(currRange.start, currRange.end);
    editBuilder.replace(editor.selection, <span class="hljs-string">'Failed to process...'</span>);
  }):
}
</code></pre>
<p>And we're done with the code. If you run/debug the extension you should see appropriate text replacements for your text. Running it on a couple of my sentences gives me the following results</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687200980102/63f92d9c-f86d-45bb-9069-514c6128c113.png" alt="Result of running the extension" class="image--center mx-auto" /></p>
<p>As always, you can play around with the prompts and get better consistent output from the OpenAI API.</p>
<h3 id="heading-adding-extension-settings">Adding Extension Settings</h3>
<p>You may have noticed that we've added the OpenAI API key directly in the code. We should move it to VS Code settings under our extension name. To do that we need to change the <code>contributes</code> key in the <code>package.json</code> file. While we're doing that we can also move the <code>maxTokens</code> property instead of hardcoding it to <code>500</code> in the code.</p>
<pre><code class="lang-json"><span class="hljs-comment">// ..</span>
<span class="hljs-string">"contributes"</span>: {
  <span class="hljs-attr">"configuration"</span>: {
    <span class="hljs-attr">"title"</span>: <span class="hljs-string">"My Shiny Extension"</span>,
    <span class="hljs-attr">"properties"</span>: {
      <span class="hljs-attr">"myShinyExtension.openAiApiKey"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>,
        <span class="hljs-attr">"default"</span>: <span class="hljs-string">""</span>,
        <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Enter you OpenAI API Key here"</span>
      },
      <span class="hljs-attr">"myShinyExtension.maxTokens"</span>: {
        <span class="hljs-attr">"type"</span>: <span class="hljs-string">"number"</span>,
        <span class="hljs-attr">"default"</span>: <span class="hljs-number">1200</span>,
        <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Enter the maximum number of tokens to use for each OpenAI API call"</span>
      }
    }
  }
},
<span class="hljs-comment">// ..</span>
</code></pre>
<p>Now these two entries will appear under "<code>My Shiny Extension</code>" in the VS Code settings. To read the values of these entries we can use the below code (put it just above the OpenAI service creation).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> configs = vscode.workspace.getConfiguration(<span class="hljs-string">'myShinyExtension'</span>);
<span class="hljs-keyword">const</span> openAIApiKey = configs.get&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">'openAiApiKey'</span>);
<span class="hljs-keyword">const</span> maxTokens = configs.get&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-string">'maxTokens'</span>);
<span class="hljs-keyword">if</span> (!openAIApiKey) {
  vscode.window.showErrorMessage(
    <span class="hljs-string">'Missing OpenAI API Key. Please add your key in VSCode settings to use this extension.'</span>
  );

  <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>Do remember that we're creating the OpenAI service instance on each API call. This is not optimal and you should move it to the constructor and handle the error scenarios appropriately.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>As is evident from the article, developing a VS Code extension can be easy and fun. If you're observant you can recreate an existing thing (Hashnode AI editor in this case) in your way someplace else, and learn a ton along the way.</p>
<p>To publish an extension we need to complete a few more steps and optionally bundle it using <code>webpack</code> or a suitable bundler. To learn more about the process you can visit these links: 1. <a target="_blank" href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension">publishing an extension</a>, 2. <a target="_blank" href="https://code.visualstudio.com/api/working-with-extensions/bundling-extension">Bundling an extension</a></p>
<p>This was just a sneak peek of the extension I created. If interested, you can look at the complete source code of the extension <a target="_blank" href="https://github.com/ra-jeev/write-assist-ai">here</a>.</p>
<p>If you want to try out the extension (Write Assist AI), you can get it from <a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=ra-jeev.write-assist-ai">here</a>.</p>
<p>I hope you liked reading the article. If you found any mistakes in the article, please let me know in the comments. Your suggestions and feedback are most welcome.</p>
<p><em>-- Keep adding the bits, soon you'll have more bytes than you may need. :-)</em></p>
]]></content:encoded></item><item><title><![CDATA[How to Make a Gmail Bot with a persona using OpenAI GPT and MindsDB]]></title><description><![CDATA[Introduction
The AI hype refuses to die, more so after the release of Chat GPT and more recently the GPT4. I've been missing the AI action so far, so when MindsDB & Hashnode announced this hackathon and I saw the Twitter Bot implementation using the ...]]></description><link>https://rajeev.dev/how-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb</link><guid isPermaLink="true">https://rajeev.dev/how-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb</guid><category><![CDATA[Python]]></category><category><![CDATA[mindsdb]]></category><category><![CDATA[MindsDBHackathon]]></category><category><![CDATA[openai]]></category><category><![CDATA[gmail]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sun, 30 Apr 2023 11:37:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1682802482706/a4045aa3-6c49-4e0d-ab2b-89effb24538d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>The AI hype refuses to die, more so after the release of Chat GPT and more recently the GPT4. I've been missing the AI action so far, so when MindsDB &amp; Hashnode announced this hackathon and I saw the Twitter Bot implementation using the OpenAI APIs I knew that I wanted to build a bot for the hackathon.</p>
<p>But then the question arose, a bot for what purpose and for which application? Twitter was already done and dusted. I wanted my bot to be a wise and witty companion who is always available, so that helped me zero down on a Gmail bot. But the Gmail Integration was not yet implemented, and that was the second reason for choosing to build it. Practicing my Python skills, and the idea of contributing to a good project were the other important reasons.</p>
<p><strong><em>Tl;dr</em></strong> <em>this article is about creating a new application (Gmail) handler for MindsDB and then using that handler to create an email bot that replies to incoming emails interestingly and poetically.</em></p>
<h2 id="heading-setting-up-the-environment">Setting up the environment</h2>
<p>Since the first task is to develop the Gmail handler, we must set up the environment for development. But before that, I needed to know <a target="_blank" href="https://docs.mindsdb.com/contribute">how to contribute to this project</a>, and <a target="_blank" href="https://docs.mindsdb.com/contribute/install">how to install MindsDb for development</a>. During the installation I faced only one issue related to <code>libmagic</code> which was not installed on my Mac by default, so had to install it using <code>brew install libmagic.</code></p>
<p>The next step was to learn the <a target="_blank" href="https://docs.mindsdb.com/contribute/app-handlers">basics of creating an app handler</a>. This gave me a good overview of what I'm supposed to do for creating the Gmail handler.</p>
<p><em>Going through the relevant docs and following the mentioned steps is crucial if you want to contribute to any existing project.</em></p>
<h2 id="heading-running-the-existing-installation">Running the existing installation</h2>
<p>Now it was time to get my hands dirty, and the first step in that direction was to use an existing app handler. But before that, I followed the "Predict Home Rental Prices" tutorial from the "Learning Hub" in the local MindsDB web console, and everything worked fine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682843437197/b86a2975-1673-45e2-b000-df73cc21b00d.png" alt="Predict Home Rental Prices Tutorial" class="image--center mx-auto" /></p>
<p>Next was the turn of the Twitter handler. I tried creating a tweets database using the below command in the local MindsDB browser console.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> my_twitter 
<span class="hljs-keyword">WITH</span> 
    <span class="hljs-keyword">ENGINE</span> = <span class="hljs-string">'twitter'</span>,
    <span class="hljs-keyword">PARAMETERS</span> = {
      <span class="hljs-string">"bearer_token"</span>: <span class="hljs-string">"twitter bearer token"</span>,
      <span class="hljs-string">"consumer_key"</span>: <span class="hljs-string">"twitter consumer key"</span>,
      <span class="hljs-string">"consumer_secret"</span>: <span class="hljs-string">"twitter consumer key secret"</span>,
      <span class="hljs-string">"access_token"</span>: <span class="hljs-string">"twitter access token"</span>,
      <span class="hljs-string">"access_token_secret"</span>: <span class="hljs-string">"twitter access token secret"</span>
    };
</code></pre>
<p>At least it should error out saying invalid credentials, but instead, I got the below error</p>
<p><code>Can't connect to db: Handler 'twitter' can not be used</code></p>
<p>Well, that's a bummer. Why it is not working? This is where you start debugging and find out what is happening in the codebase. And how do we do that? I simply searched for the error string <code>"Can't connect to db"</code> in the codebase and found the issue to be related to the handler not getting imported. After some more investigation saw that the zsh console has this info message</p>
<p><code>Dependencies for the handler 'twitter' are not installed by default. If you want to use "twitter" please install "['tweepy']"</code></p>
<p>And there we have it. This gives us a clue that if we want to use any of the other handlers (except the basic ones) we need to install their dependencies manually.</p>
<p><code>pip install tweepy</code> and restarting MindsDB was enough to get the error I was hoping for in the first place</p>
<p><code>Can't connect to db: Error connecting to Twitter api: 401 Unauthorized Unauthorized. Check bearer_token</code></p>
<p>Now we're all set for development. As the docs said to study the Twitter handler, I simply created a copy of the twitter_handler folder and renamed it to gmail_handler. Then replaced "Twitter" with "Gmail" in <code>__init__.py</code> and <code>__about__.py</code> files along with related method name changes in the <code>gmail_handler</code> file. Verified it by executing the same Twitter create database command but replacing the engine with "gmail", and it seemed to call our gmail_handler.</p>
<h2 id="heading-implementing-the-gmail-handler">Implementing the Gmail Handler</h2>
<p>Going by the steps mentioned in <a target="_blank" href="https://docs.mindsdb.com/contribute/app-handlers">how to create an application handler</a> we need to modify the below methods. Before we can read/write emails we need to authenticate the user, so the first targets were the <code>connect</code> and the <code>check_connection</code> methods.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682845457315/29a859f0-b86d-439f-bea4-a341053208a1.png" alt="Handler methods to implement" class="image--center mx-auto" /></p>
<h3 id="heading-setting-up-a-google-project-for-gmail-apis">Setting up a Google project for Gmail APIs</h3>
<p>To use the Gmail APIs we need to set up a Google Cloud Project and a Google Account with Gmail enabled. We will also need to enable the Gmail API from the Google Cloud Console.</p>
<p>Then we need to create OAuth Client Ids for authenticating users, and possibly an Auth Consent Screen (if this is the first time we're setting up OAuth)</p>
<p>Setting up OAuth Client Id will give us a credentials file which we will need in our gmail_handler for connection. You can find more information on <a target="_blank" href="https://developers.google.com/gmail/quickstart/python">how to set up a Google project for the Gmail APIs here</a>.</p>
<h3 id="heading-initing-the-gmailhandler-class">Initing the GmailHandler class</h3>
<p>We take the connection arguments (which are passed with the CREATE DATABASE command) and store them for future use. We also register an <code>"emails"</code> table where we will store our data.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GmailHandler</span>(<span class="hljs-params">APIHandler</span>):</span>
    <span class="hljs-string">"""A class for handling connections and interactions with the Gmail API.

    Attributes:
        credentials_file (str): The path to the Google Auth Credentials file for authentication
        and interacting with the Gmail API on behalf of the uesr.

        scopes (List[str], Optional): The scopes to use when authenticating with the Gmail API.
    """</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, name=None, **kwargs</span>):</span>
        super().__init__(name)

        self.connection_args = kwargs.get(<span class="hljs-string">'connection_data'</span>, {})
        self.credentials_file = self.connection_args[<span class="hljs-string">'credentials_file'</span>]
        self.scopes = self.connection_args.get(<span class="hljs-string">'scopes'</span>, DEFAULT_SCOPES)
        self.token_file = <span class="hljs-literal">None</span>
        self.max_page_size = <span class="hljs-number">500</span>
        self.max_batch_size = <span class="hljs-number">100</span>
        self.service = <span class="hljs-literal">None</span>
        self.is_connected = <span class="hljs-literal">False</span>

        emails = EmailsTable(self)
        self._register_table(<span class="hljs-string">'emails'</span>, emails)
</code></pre>
<h3 id="heading-handling-google-authentication">Handling Google Authentication</h3>
<p>Following the link in the previous section, and the MindsDB code requirements, we need to do the following</p>
<ol>
<li><p>Replace the content of requirements.txt inside the gmail_handler folder with the following. Do remember to install these modules using the pip install command</p>
<pre><code class="lang-javascript"> google-api-python-client
 google-auth-httplib2
 google-auth-oauthlib
</code></pre>
</li>
<li><p>The <code>connect</code> method. Here we use the credentials files created in the previous section for authenticating the user.</p>
<pre><code class="lang-python"> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connect</span>(<span class="hljs-params">self</span>):</span>
     <span class="hljs-string">"""Authenticate with the Gmail API using the credentials file.

     Returns
     -------
     service: object
         The authenticated Gmail API service object.
     """</span>
     <span class="hljs-keyword">if</span> self.is_connected <span class="hljs-keyword">is</span> <span class="hljs-literal">True</span>:
         <span class="hljs-keyword">return</span> self.service

     self.service = self.create_connection()

     self.is_connected = <span class="hljs-literal">True</span>
     <span class="hljs-keyword">return</span> self.service

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_connection</span>(<span class="hljs-params">self</span>):</span>
     creds = <span class="hljs-literal">None</span>
     token_file = os.path.join(os.path.dirname(self.credentials_file), <span class="hljs-string">'token.json'</span>)

     <span class="hljs-keyword">if</span> os.path.isfile(token_file):
         creds = Credentials.from_authorized_user_file(token_file, self.scopes)

     <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> creds <span class="hljs-keyword">or</span> <span class="hljs-keyword">not</span> creds.valid:
         <span class="hljs-keyword">if</span> creds <span class="hljs-keyword">and</span> creds.expired <span class="hljs-keyword">and</span> creds.refresh_token:
             creds.refresh(Request())
         <span class="hljs-keyword">elif</span> <span class="hljs-keyword">not</span> os.path.isfile(self.credentials_file):
             <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">'Credentials must be a file path'</span>)
         <span class="hljs-keyword">else</span>:
             flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, self.scopes)
             creds = flow.run_local_server(port=<span class="hljs-number">0</span>, timeout_seconds=<span class="hljs-number">120</span>)

     <span class="hljs-comment"># Save the credentials for the next run</span>
     <span class="hljs-keyword">with</span> open(token_file, <span class="hljs-string">'w'</span>) <span class="hljs-keyword">as</span> token:
         token.write(creds.to_json())

     <span class="hljs-keyword">return</span> build(<span class="hljs-string">'gmail'</span>, <span class="hljs-string">'v1'</span>, credentials=creds)
</code></pre>
</li>
<li><p>The <code>check_connection</code> method</p>
<pre><code class="lang-python"> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">check_connection</span>(<span class="hljs-params">self</span>) -&gt; StatusResponse:</span>
     <span class="hljs-string">"""Check connection to the handler.

     Returns
     -------
     StatusResponse
         Status confirmation
     """</span>
     response = StatusResponse(<span class="hljs-literal">False</span>)

     <span class="hljs-keyword">try</span>:
         <span class="hljs-comment"># Call the Gmail API</span>
         service = self.connect()

         result = service.users().getProfile(userId=<span class="hljs-string">'me'</span>).execute()

         <span class="hljs-keyword">if</span> result <span class="hljs-keyword">and</span> result.get(<span class="hljs-string">'emailAddress'</span>, <span class="hljs-literal">None</span>) <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
             response.success = <span class="hljs-literal">True</span>
     <span class="hljs-keyword">except</span> HttpError <span class="hljs-keyword">as</span> error:
         response.error_message = <span class="hljs-string">f'Error connecting to Gmail api: <span class="hljs-subst">{error}</span>.'</span>
         log.logger.error(response.error_message)

     <span class="hljs-keyword">if</span> response.success <span class="hljs-keyword">is</span> <span class="hljs-literal">False</span> <span class="hljs-keyword">and</span> self.is_connected <span class="hljs-keyword">is</span> <span class="hljs-literal">True</span>:
         self.is_connected = <span class="hljs-literal">False</span>

     <span class="hljs-keyword">return</span> response
</code></pre>
</li>
</ol>
<p>Now we're ready to run the create database command and authenticate the user (of course we've not decided on the columns of our table but we will come to that). Simply run</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> mindsdb_gmail
<span class="hljs-keyword">WITH</span> <span class="hljs-keyword">ENGINE</span> = <span class="hljs-string">'gmail'</span>,
<span class="hljs-keyword">PARAMETERS</span> = {
  <span class="hljs-string">"credentials_file"</span>: <span class="hljs-string">"mindsdb/integrations/handlers/gmail_handler/credentials.json"</span>
};
</code></pre>
<h3 id="heading-fetching-emails-from-the-gmail-api">Fetching Emails from the Gmail API</h3>
<p>The flow for fetching emails using MindsDB is like this: you execute an <code>SQL SELECT</code> query, and the <code>select</code> method of the <code>APITable</code> class gets called. There you parse the query params and finally call the Gmail API accordingly.</p>
<ul>
<li>The <code>select</code> method of the EmailsTable</li>
</ul>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailsTable</span>(<span class="hljs-params">APITable</span>):</span>
    <span class="hljs-string">"""Implementation for the emails table for Gmail"""</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">select</span>(<span class="hljs-params">self, query: ast.Select</span>) -&gt; Response:</span>
        <span class="hljs-string">"""Pulls emails from Gmail "users.messages.list" API

        Parameters
        ----------
        query : ast.Select
           Given SQL SELECT query

        Returns
        -------
        pd.DataFrame
            Email matching the query

        Raises
        ------
        NotImplementedError
            If the query contains an unsupported operation or condition
        """</span>

        conditions = extract_comparison_conditions(query.where)

        params = {}
        <span class="hljs-keyword">for</span> op, arg1, arg2 <span class="hljs-keyword">in</span> conditions:

            <span class="hljs-keyword">if</span> op == <span class="hljs-string">'or'</span>:
                <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f'OR is not supported'</span>)

            <span class="hljs-keyword">if</span> arg1 <span class="hljs-keyword">in</span> [<span class="hljs-string">'query'</span>, <span class="hljs-string">'label_ids'</span>, <span class="hljs-string">'include_spam_trash'</span>]:
                <span class="hljs-keyword">if</span> op == <span class="hljs-string">'='</span>:
                    <span class="hljs-keyword">if</span> arg1 == <span class="hljs-string">'query'</span>:
                        params[<span class="hljs-string">'q'</span>] = arg2
                    <span class="hljs-keyword">elif</span> arg1 == <span class="hljs-string">'label_ids'</span>:
                        params[<span class="hljs-string">'labelIds'</span>] = arg2.split(<span class="hljs-string">','</span>)
                    <span class="hljs-keyword">else</span>:
                        params[<span class="hljs-string">'includeSpamTrash'</span>] = arg2
                <span class="hljs-keyword">else</span>:
                    <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f'Unknown op: <span class="hljs-subst">{op}</span>'</span>)

            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f'Unknown clause: <span class="hljs-subst">{arg1}</span>'</span>)

        <span class="hljs-keyword">if</span> query.limit <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
            params[<span class="hljs-string">'maxResults'</span>] = query.limit.value

        result = self.handler.call_gmail_api(
            method_name=<span class="hljs-string">'list_messages'</span>,
            params=params
        )

        <span class="hljs-comment"># filter targets</span>
        columns = []
        <span class="hljs-keyword">for</span> target <span class="hljs-keyword">in</span> query.targets:
            <span class="hljs-keyword">if</span> isinstance(target, ast.Star):
                columns = []
                <span class="hljs-keyword">break</span>
            <span class="hljs-keyword">elif</span> isinstance(target, ast.Identifier):
                columns.append(target.parts[<span class="hljs-number">-1</span>])
            <span class="hljs-keyword">else</span>:
                <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f"Unknown query target <span class="hljs-subst">{type(target)}</span>"</span>)

        <span class="hljs-keyword">if</span> len(columns) == <span class="hljs-number">0</span>:
            columns = self.get_columns()

        <span class="hljs-comment"># columns to lower case</span>
        columns = [name.lower() <span class="hljs-keyword">for</span> name <span class="hljs-keyword">in</span> columns]

        <span class="hljs-keyword">if</span> len(result) == <span class="hljs-number">0</span>:
            result = pd.DataFrame([], columns=columns)
        <span class="hljs-keyword">else</span>:
            <span class="hljs-comment"># add absent columns</span>
            <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> set(columns) &amp; set(result.columns) ^ set(columns):
                result[col] = <span class="hljs-literal">None</span>

            <span class="hljs-comment"># filter by columns</span>
            result = result[columns]
        <span class="hljs-keyword">return</span> result
</code></pre>
<ul>
<li>The <code>get_columns</code> method. These are the columns that our <code>"EmailsTable"</code> will have.</li>
</ul>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_columns</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-string">"""Gets all columns to be returned in pandas DataFrame responses

    Returns
    -------
    List[str]
        List of columns
    """</span>
    <span class="hljs-keyword">return</span> [
        <span class="hljs-string">'id'</span>,
        <span class="hljs-string">'message_id'</span>,
        <span class="hljs-string">'thread_id'</span>,
        <span class="hljs-string">'label_ids'</span>,
        <span class="hljs-string">'from'</span>,
        <span class="hljs-string">'to'</span>,
        <span class="hljs-string">'date'</span>,
        <span class="hljs-string">'subject'</span>,
        <span class="hljs-string">'snippet'</span>,
        <span class="hljs-string">'body'</span>,
    ]
</code></pre>
<ul>
<li>The <code>call_gmail_api</code> method of the <code>GmailHandler</code> class. The way the Gmail messages API works is, first it returns a list of the messages that match your query criteria. These messages contain just the threadId &amp; the messageId etc and not the full email. Then using the <code>"messageIds"</code> you fetch the full messages separately.</li>
</ul>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call_gmail_api</span>(<span class="hljs-params">self, method_name: str = None, params: dict = None</span>):</span>
    <span class="hljs-string">"""Call Gmail API and map the data to pandas DataFrame
    Args:
        method_name (str): method name
        params (dict): query parameters
    Returns:
        DataFrame
    """</span>
    service = self.connect()
    <span class="hljs-keyword">if</span> method_name == <span class="hljs-string">'list_messages'</span>:
        method = service.users().messages().list
    <span class="hljs-keyword">elif</span> method_name == <span class="hljs-string">'send_message'</span>:
        method = service.users().messages().send
    <span class="hljs-keyword">else</span>:
        <span class="hljs-keyword">raise</span> NotImplementedError(<span class="hljs-string">f'Unknown method_name: <span class="hljs-subst">{method_name}</span>'</span>)

    left = <span class="hljs-literal">None</span>
    count_results = <span class="hljs-literal">None</span>
    <span class="hljs-keyword">if</span> <span class="hljs-string">'maxResults'</span> <span class="hljs-keyword">in</span> params:
        count_results = params[<span class="hljs-string">'maxResults'</span>]

    params[<span class="hljs-string">'userId'</span>] = <span class="hljs-string">'me'</span>

    data = []
    limit_exec_time = time.time() + <span class="hljs-number">60</span>

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-keyword">if</span> time.time() &gt; limit_exec_time:
            <span class="hljs-keyword">raise</span> RuntimeError(<span class="hljs-string">'Handler request timeout error'</span>)

        <span class="hljs-keyword">if</span> count_results <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
            left = count_results - len(data)
            <span class="hljs-keyword">if</span> left == <span class="hljs-number">0</span>:
                <span class="hljs-keyword">break</span>
            <span class="hljs-keyword">elif</span> left &lt; <span class="hljs-number">0</span>:
                <span class="hljs-comment"># got more results that we need</span>
                data = data[:left]
                <span class="hljs-keyword">break</span>

            <span class="hljs-keyword">if</span> left &gt; self.max_page_size:
                params[<span class="hljs-string">'maxResults'</span>] = self.max_page_size
            <span class="hljs-keyword">else</span>:
                params[<span class="hljs-string">'maxResults'</span>] = left

        log.logger.debug(<span class="hljs-string">f'Calling Gmail API: <span class="hljs-subst">{method_name}</span> with params (<span class="hljs-subst">{params}</span>)'</span>)

        resp = method(**params).execute()

        <span class="hljs-keyword">if</span> <span class="hljs-string">'messages'</span> <span class="hljs-keyword">in</span> resp:
            self._handle_list_messages_response(data, resp[<span class="hljs-string">'messages'</span>])
        <span class="hljs-keyword">elif</span> isinstance(resp, dict):
            data.append(resp)

        <span class="hljs-keyword">if</span> count_results <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">and</span> <span class="hljs-string">'nextPageToken'</span> <span class="hljs-keyword">in</span> resp:
            params[<span class="hljs-string">'pageToken'</span>] = resp[<span class="hljs-string">'nextPageToken'</span>]
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">break</span>

    df = pd.DataFrame(data)

    <span class="hljs-keyword">return</span> df
</code></pre>
<ul>
<li>Inner method <code>_handle_list_messages_response</code> and other related methods</li>
</ul>
<pre><code class="lang-python"><span class="hljs-comment"># Handle the API response by downloading the full messages</span>
<span class="hljs-comment"># using a Batch Request.</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_handle_list_messages_response</span>(<span class="hljs-params">self, data, messages</span>):</span>
    total_pages = len(messages) // self.max_batch_size
    <span class="hljs-keyword">for</span> page <span class="hljs-keyword">in</span> range(total_pages):
        self._get_messages(data, messages[page * self.max_batch_size:(page + <span class="hljs-number">1</span>) * self.max_batch_size])

    <span class="hljs-comment"># Get the remaining messsages, if any</span>
    <span class="hljs-keyword">if</span> len(messages) % self.max_batch_size &gt; <span class="hljs-number">0</span>:
        self._get_messages(data, messages[total_pages * self.max_batch_size:])

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_get_messages</span>(<span class="hljs-params">self, data, messages</span>):</span>
    batch_req = self.service.new_batch_http_request(<span class="hljs-keyword">lambda</span> id, response, exception: self._parse_message(data, response, exception))
    <span class="hljs-keyword">for</span> message <span class="hljs-keyword">in</span> messages:
        batch_req.add(self.service.users().messages().get(userId=<span class="hljs-string">'me'</span>, id=message[<span class="hljs-string">'id'</span>]))

    batch_req.execute()

<span class="hljs-comment"># This method shows how to parse the full email returned </span>
<span class="hljs-comment"># by the Gmail API</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_parse_message</span>(<span class="hljs-params">self, data, message, exception</span>):</span>
    <span class="hljs-keyword">if</span> exception:
        log.logger.error(<span class="hljs-string">f'Exception in getting full email: <span class="hljs-subst">{exception}</span>'</span>)
        <span class="hljs-keyword">return</span>

    payload = message[<span class="hljs-string">'payload'</span>]
    headers = payload.get(<span class="hljs-string">"headers"</span>)
    parts = payload.get(<span class="hljs-string">"parts"</span>)

    row = {
        <span class="hljs-string">'id'</span>: message[<span class="hljs-string">'id'</span>],
        <span class="hljs-string">'thread_id'</span>: message[<span class="hljs-string">'threadId'</span>],
        <span class="hljs-string">'label_ids'</span>: message.get(<span class="hljs-string">'labelIds'</span>, []),
        <span class="hljs-string">'snippet'</span>: message.get(<span class="hljs-string">'snippet'</span>, <span class="hljs-string">''</span>),
    }

    <span class="hljs-keyword">if</span> headers:
        <span class="hljs-keyword">for</span> header <span class="hljs-keyword">in</span> headers:
            key = header[<span class="hljs-string">'name'</span>].lower()
            value = header[<span class="hljs-string">'value'</span>]

            <span class="hljs-keyword">if</span> key <span class="hljs-keyword">in</span> [<span class="hljs-string">'from'</span>, <span class="hljs-string">'to'</span>, <span class="hljs-string">'subject'</span>, <span class="hljs-string">'date'</span>]:
                row[key] = value
            <span class="hljs-keyword">elif</span> key == <span class="hljs-string">'message-id'</span>:
                row[<span class="hljs-string">'message_id'</span>] = value

    row[<span class="hljs-string">'body'</span>] = self._parse_parts(parts)

    data.append(row)

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

    body = <span class="hljs-string">''</span>
    <span class="hljs-keyword">for</span> part <span class="hljs-keyword">in</span> parts:
        <span class="hljs-keyword">if</span> part[<span class="hljs-string">'mimeType'</span>] == <span class="hljs-string">'text/plain'</span>:
            part_body = part.get(<span class="hljs-string">'body'</span>, {}).get(<span class="hljs-string">'data'</span>, <span class="hljs-string">''</span>)
            body += urlsafe_b64decode(part_body).decode(<span class="hljs-string">'utf-8'</span>)
        <span class="hljs-keyword">elif</span> part[<span class="hljs-string">'mimeType'</span>] == <span class="hljs-string">'multipart/alternative'</span> <span class="hljs-keyword">or</span> <span class="hljs-string">'parts'</span> <span class="hljs-keyword">in</span> part:
            <span class="hljs-comment"># Recursively iterate over nested parts to find the plain text body</span>
            body += self._parse_parts(part[<span class="hljs-string">'parts'</span>])
        <span class="hljs-keyword">else</span>:
            log.logger.debug(<span class="hljs-string">f"Unhandled mimeType: <span class="hljs-subst">{part[<span class="hljs-string">'mimeType'</span>]}</span>"</span>)

    <span class="hljs-keyword">return</span> body
</code></pre>
<p>The above is sufficient to fetch and store the emails of the authenticated user in the database. We can run an SQSL SELECT query like below to fetch the emails. The query parameter supports all the <a target="_blank" href="https://support.google.com/mail/answer/7190">filter options</a> that are available in the Gmail API</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> *
<span class="hljs-keyword">FROM</span> mindsdb_gmail.emails
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">query</span> = <span class="hljs-string">'from:test@example.com OR search_text OR from:test@example1.com'</span>
<span class="hljs-keyword">AND</span> label_ids = <span class="hljs-string">"INBOX,UNREAD"</span> 
<span class="hljs-keyword">LIMIT</span> <span class="hljs-number">20</span>;
</code></pre>
<h3 id="heading-sending-emails-using-the-gmail-api">Sending Emails using the Gmail API</h3>
<p>For sending emails through the Gmail API and MindsDB we need to use the <code>SQL INSERT</code> query. This in turn calls the insert method of the <code>EmailsTable</code> class, we created earlier.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insert</span>(<span class="hljs-params">self, query: ast.Insert</span>):</span>
    <span class="hljs-string">"""Sends emails using the Gmail "users.messages.send" API

    Parameters
    ----------
    query : ast.Insert
        Given SQL INSERT query

    Raises
    ------
    ValueError
        If the query contains an unsupported condition
    """</span>
    columns = [col.name <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> query.columns]

    <span class="hljs-keyword">if</span> self.handler.connection_args.get(<span class="hljs-string">'credentials_file'</span>, <span class="hljs-literal">None</span>) <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">raise</span> ValueError(
            <span class="hljs-string">"Need the Google Auth Credentials file in order to write an email"</span>
        )

    supported_columns = {<span class="hljs-string">"message_id"</span>, <span class="hljs-string">"thread_id"</span>, <span class="hljs-string">"to_email"</span>, <span class="hljs-string">"subject"</span>, <span class="hljs-string">"body"</span>}
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> set(columns).issubset(supported_columns):
        unsupported_columns = set(columns).difference(supported_columns)
        <span class="hljs-keyword">raise</span> ValueError(
            <span class="hljs-string">"Unsupported columns for create email: "</span>
            + <span class="hljs-string">", "</span>.join(unsupported_columns)
        )

    <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> query.values:
        params = dict(zip(columns, row))

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-string">'to_email'</span> <span class="hljs-keyword">in</span> params:
            <span class="hljs-keyword">raise</span> ValueError(<span class="hljs-string">'"to_email" parameter is required to send an email'</span>)

        message = EmailMessage()
        message[<span class="hljs-string">'To'</span>] = params[<span class="hljs-string">'to_email'</span>]
        message[<span class="hljs-string">'Subject'</span>] = params[<span class="hljs-string">'subject'</span>] <span class="hljs-keyword">if</span> <span class="hljs-string">'subject'</span> <span class="hljs-keyword">in</span> params <span class="hljs-keyword">else</span> <span class="hljs-string">''</span>

        content = params[<span class="hljs-string">'body'</span>] <span class="hljs-keyword">if</span> <span class="hljs-string">'body'</span> <span class="hljs-keyword">in</span> params <span class="hljs-keyword">else</span> <span class="hljs-string">''</span>
        message.set_content(content)

        <span class="hljs-comment"># If threadId is present then add References and In-Reply-To headers</span>
        <span class="hljs-comment"># so that proper threading can happen</span>
        <span class="hljs-keyword">if</span> <span class="hljs-string">'thread_id'</span> <span class="hljs-keyword">in</span> params <span class="hljs-keyword">and</span> <span class="hljs-string">'message_id'</span> <span class="hljs-keyword">in</span> params:
            message[<span class="hljs-string">'In-Reply-To'</span>] = params[<span class="hljs-string">'message_id'</span>]
            message[<span class="hljs-string">'References'</span>] = params[<span class="hljs-string">'message_id'</span>]

        encoded_message = urlsafe_b64encode(message.as_bytes()).decode()

        message = {
            <span class="hljs-string">'raw'</span>: encoded_message
        }

        <span class="hljs-keyword">if</span> <span class="hljs-string">'thread_id'</span> <span class="hljs-keyword">in</span> params:
            message[<span class="hljs-string">'threadId'</span>] = params[<span class="hljs-string">'thread_id'</span>]

        self.handler.call_gmail_api(<span class="hljs-string">'send_message'</span>, {<span class="hljs-string">'body'</span>: message})
</code></pre>
<p>This method calls the same <code>call_gmail_api</code> we saw earlier to send an email. We can use an <code>SQL INSERT</code> query to send an email. The <code>thread_id</code> and <code>message_id</code> parameter values are only required if we're replying to an incoming email and want that our reply should form a thread with the original email. (The <code>"subject"</code> should exactly match the original subject line for it to work)</p>
<pre><code class="lang-sql"><span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> mindsdb_gmail.emails (thread_id, message_id, to_email, subject, <span class="hljs-keyword">body</span>)
<span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'187cbdd861350934d'</span>, <span class="hljs-string">'8e54ccfd-abd0-756b-a12e-f7bc95ebc75b@Spark'</span>, <span class="hljs-string">'test@example2.com'</span>, <span class="hljs-string">'Trying out MindsDB'</span>,
        <span class="hljs-string">'This seems awesome. You must try it out whenever you can.'</span>)
</code></pre>
<h2 id="heading-creating-the-gmail-bot">Creating the Gmail Bot</h2>
<p>Now that we're unblocked and can fetch/send emails easily using our shiny new GmailHandler, we're ready to work on our Gmail bot.</p>
<h3 id="heading-obtaining-an-openai-api-key">Obtaining an OpenAI API key</h3>
<p>Since we're developing this locally we do not have the luxury of using the inbuilt API key provided by MindsDB Cloud. We need to create an account on OpenAI and create an API key.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682850492409/b83632b7-fc9a-42f4-a746-5f41eb5214c4.png" alt="Creating an API Key for OpenAI API" class="image--center mx-auto" /></p>
<h3 id="heading-training-the-model-using-mindsdb">Training the Model using MindsDB</h3>
<p>I do not have access to GPT4 APIs, so I'm using the <code>gpt-3.5-turbo</code> model itself. We're just telling GPT to respond to the email with a proper salutation and signature and to keep the email tone casual. This is done using the prompt_template parameter.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">MODEL</span> mindsdb.gpt_model
PREDICT response
<span class="hljs-keyword">USING</span>
<span class="hljs-keyword">engine</span> = <span class="hljs-string">'openai'</span>,
max_tokens = <span class="hljs-number">500</span>,
api_key = <span class="hljs-string">'&lt;your_api_key&gt;'</span>, 
model_name = <span class="hljs-string">'gpt-3.5-turbo'</span>,
prompt_template = <span class="hljs-string">'From input message: {{input_text}}\
by from_user: {{from_email}}\
In less than 500 characters, write an email response to {{from_email}} in the following format:\
Start with proper salutation and respond with a short message in a casual tone, and sign the email with my name mindsdb'</span>;
</code></pre>
<p>Once the training is complete we're ready to see our bot in action. Run the following command</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> response
<span class="hljs-keyword">FROM</span> mindsdb.gpt_model_email
<span class="hljs-keyword">WHERE</span> from_email = <span class="hljs-string">"alice@example.com"</span> 
<span class="hljs-keyword">AND</span> input_text = <span class="hljs-string">"Hi there, I'm bored. Give me a puzzle to solve"</span>;
</code></pre>
<p>And we get the following response, seems quite all right, isn't it?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682851072277/6f2a16bf-5bb5-4a5b-99fa-5e7aba3db750.png" alt="model email response in casual tone" class="image--center mx-auto" /></p>
<p>On asking for a new puzzle it says the following</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682851254263/7a64f4f9-9b02-446a-946c-c049362b0ae3.png" alt="casual second response by GPT" class="image--center mx-auto" /></p>
<h3 id="heading-giving-the-bot-a-persona">Giving the bot a persona</h3>
<p>Since our initial experiments seem to work fine, we can get a bit adventurous now. Let's give our bot a combined persona of Master Yoda from the Star Wars movies, and Edgar Allan Poe, the famous poet. We do this by changing the prompt_template in our earlier command</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">MODEL</span> mindsdb.gpt_model_yodapoe
PREDICT response
<span class="hljs-keyword">USING</span>
<span class="hljs-keyword">engine</span> = <span class="hljs-string">'openai'</span>,
max_tokens = <span class="hljs-number">800</span>,
api_key = <span class="hljs-string">'&lt;your_api_key&gt;'</span>, 
model_name = <span class="hljs-string">'gpt-3.5-turbo'</span>, <span class="hljs-comment">-- you can also use 'text-davinci-003' or 'gpt-3.5-turbo'</span>
prompt_template = <span class="hljs-string">'From input message: {{input_text}}\
by from_user: {{from_email}}\
In less than 500 characters, write an email response to {{from_email}} in the following format:\
&lt;respond with a 4 line poem as if you were Edgar Allan Poe but you are also a wise elder like Master Yoda from the Star Wars movies. The wordings should be like Master Yoda but the format should be like Poe. Do not mention that you are Master Yoda or Edgar Allan Poe. Sign it with a made up quote similar to what Voltaire, Nietzsche etc would say. Do not explain or say anything else about the quote.&gt;'</span>;
</code></pre>
<p>Train the model, and then run the same SELECT queries by changing the model name</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> response
<span class="hljs-keyword">FROM</span> mindsdb.gpt_model_yodapoe
<span class="hljs-keyword">WHERE</span> from_email = <span class="hljs-string">"alice@example.com"</span> 
<span class="hljs-keyword">AND</span> input_text = <span class="hljs-string">"Hi there, I'm bored. Give me a puzzle to solve"</span>;
</code></pre>
<p>This is what I get</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682851797607/9e09c975-adb7-4081-a610-50f1ca4c2ca3.png" alt="persona response for a puzzle " class="image--center mx-auto" /></p>
<p>On changing the input text to the following</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> response
<span class="hljs-keyword">FROM</span> mindsdb.gpt_model_yodapoe
<span class="hljs-keyword">WHERE</span> from_email = <span class="hljs-string">"alice@example.com"</span> 
<span class="hljs-keyword">AND</span> input_text = <span class="hljs-string">"Hi there, What's in a hackathon?"</span>;
</code></pre>
<p>we get the following result</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682852028537/b78bcd53-3d16-4aab-8b70-7e2b7cfe2c37.png" alt="what is in a hackathon response" class="image--center mx-auto" /></p>
<p>Overall this seems to be working fine. We can always fine-tune the persona based on our taste. Now we can connect this response generation with the actual email sending and we may as well create a scheduled job to read emails at regular intervals and reply to them based on some predefined criteria.</p>
<h2 id="heading-outcomes">Outcomes</h2>
<p>When I started working on this feature, I had the following goals and their respective outcomes at the end</p>
<ol>
<li><p>Create the Gmail handler: This in my opinion is done. There may be some bugs that I'll need to solve in due course</p>
</li>
<li><p>Contribute to the MindsDB project: I've already <a target="_blank" href="https://github.com/mindsdb/mindsdb/pull/5889">opened a PR</a> for my changes and now I'm hoping that it gets merged into the codebase.</p>
</li>
<li><p>Create a Gmail Bot: We've all the ingredients in place, we just need to deploy it somewhere so that it is always available. I did try deploying the source on a droplet but I'm stuck with an error for which I've raised a <a target="_blank" href="https://github.com/mindsdb/mindsdb/issues/5892">GitHub issue</a>.</p>
</li>
<li><p>Practice my Python skills: I think I've made good progress on this while working on the feature. Python is not my area of expertise, so I'm quite pumped that I was able to create a working integration in a short span of 7-8 days.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Overall it was quite fun and interesting to create a Gmail Bot and the needed Gmail Handler for MindsDB. During the process, I got to see the inner workings of the MindsDB codebase. I learnt to use the Gmail APIs and how emails are structured behind the scenes and how to parse it. It also allowed me to use the MindsDB integration with OpenAI, and now I too can say AI is eating the world :-).</p>
<p>Hope you liked reading the article. If you've noticed any error or issue anywhere please do let me know in the comments.</p>
<p><em>-- Keep adding the bits, soon you'll have more bytes than you may need.</em></p>
]]></content:encoded></item><item><title><![CDATA[Mastering Keyboard Navigation with Roving tabindex in Grids]]></title><description><![CDATA[Did you ever try to navigate a webpage using just your keyboard? Do you know how it works under the hood? Do you know what is "tabindex", and what is its role in keyboard navigation? In this article, we will cover all these burning questions and also...]]></description><link>https://rajeev.dev/mastering-keyboard-navigation-with-roving-tabindex-in-grids</link><guid isPermaLink="true">https://rajeev.dev/mastering-keyboard-navigation-with-roving-tabindex-in-grids</guid><category><![CDATA[HTML5]]></category><category><![CDATA[React]]></category><category><![CDATA[Accessibility]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Thu, 06 Apr 2023 18:04:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/ZS3OfU40CQU/upload/394038a94a9e8ba3eeb96ef292f9aced.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Did you ever try to navigate a webpage using just your keyboard? Do you know how it works under the hood? Do you know what is <code>"tabindex",</code> and what is its role in keyboard navigation? In this article, we will cover all these burning questions and also learn to implement custom keyboard navigation for a grid using the roving tabindex technique.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>Recently I was looking to implement keyboard navigation for my daily puzzle game <a target="_blank" href="https://playgoldroad.com">GoldRoad</a>. I had a vague idea about adding key event listeners and using these key presses but I wanted to learn if there is something more to it. And I wasn't disappointed. This article tells the story of the rabbit hole I fell into.</p>
<p>To navigate a website using the keyboard, we typically use the <code>Tab</code> key (If you're on the Safari browser, and haven't changed any settings then you need to use the <code>option + Tab</code> keys). We keep pressing the <code>Tab</code> key and we're taken to different elements of the webpage.</p>
<p>If you're on a laptop or desktop right now, and if you haven't tried it already, you can experience it yourself by pressing the <code>Tab</code> key a couple of times. The elements that get focussed and in what order are decided by their <code>"tabindex"</code> attribute.</p>
<h3 id="heading-what-is-tabindex">What is tabindex?</h3>
<p>As the name suggests, <code>tabindex</code> is the index of tabbing and it is a global HTML attribute. It decides which elements on a page get focussed and in what order when keyboard navigation is done using the <code>Tab</code> key. It can take integer values: a lower positive value implies that the element will be reached (and focussed) ahead of others (including elements having <code>tabindex</code> of 0). Any negative value (usually <code>-1</code> is used) means that the element can't be reached by pressing the tab key alone.</p>
<p>Some interactive HTML elements like buttons, inputs, selects, anchor tags etc get a default <code>tabindex</code> value of 0, and these are the elements that usually get focussed when we do keyboard navigation.</p>
<p>Run the below CodeSandBox to see it in action. The <code>h2</code> element has a <code>tabindex</code> of <code>0</code>, and since it is present before the grid of buttons, it gets focused first when you press the <code>Tab</code> key, and then the buttons get focused one after another in the order they were added to the DOM, and so on. Notice that the button with tabindex -1 doesn't get focussed.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/basic-tab-index-demo-1s3ec9?fontsize=14&amp;hidenavigation=1&amp;theme=dark&amp;view=preview?runonclick=1">https://codesandbox.io/embed/basic-tab-index-demo-1s3ec9?fontsize=14&amp;hidenavigation=1&amp;theme=dark&amp;view=preview?runonclick=1</a></div>
<p> </p>
<h3 id="heading-shortcomings-of-the-tab-key">Shortcomings of the Tab key</h3>
<p>If you tried navigating the previous grid, you'll notice the below problems</p>
<ol>
<li><p>To get to the 9th button, 10 key presses are needed (one for the <code>h2</code> element, and then 9 more for the 9 buttons). And how do you go back and forth between these buttons? We can keep pressing the <code>Tab</code> key but that is not an optimal experience.</p>
</li>
<li><p>What if we wanted to start from the middle of the grid? We could give the middle button a positive <code>tabindex</code> value, but even though positive integers are valid values we should avoid using values greater than 0 (this is because it messes up the keyboard navigation for people using assistive technologies).</p>
</li>
<li><p>Similar to issue 1, if we've other focusable elements below the grid (e.g. the bottom <code>p</code> tag), then it will take a lot of key presses to reach there. How do we solve this?</p>
</li>
</ol>
<h2 id="heading-grid-navigation-using-the-arrow-keys">Grid Navigation using the Arrow Keys</h2>
<p>We can improve our grid navigation by using the arrow keys. This will solve the 1st issue, and maybe the 3rd issue partially, but to solve it effectively we need to consider the grid as a single entity in terms of the tab stops. So the first tab key takes us to the h2 element, the second one takes us to the grid, and the third tab press takes us to the paragraph element. And to navigate inside the grid we use the arrow keys with the help of the roving tabindex technique.</p>
<h3 id="heading-what-is-roving-tabindex">What is Roving tabindex?</h3>
<p>To consider the grid as a single entity we need to assign a <code>tabindex</code> of -1 to all the inner buttons and give a <code>tabindex="0"</code> to that one button where the focus should land. Then we use <code>EventListeners</code> for tracking the key presses, and we keep roving this <code>tabindex="0"</code> and the corresponding focus to the appropriate button. At a time there will be only one button having a 0 tabindex.</p>
<p>This technique of managing focus inside a component using the tabindex attribute is called the roving tabindex technique. This also allows us to come back to the same element of the grid from where we tab away from it (this is a requirement for better accessibility).</p>
<h2 id="heading-roving-tabindex-implementation">Roving tabindex implementation</h2>
<p>Enough talk. Let's implement the "roving tabindex" technique for the below grid. You can try navigating this grid with your arrow keys after pressing the <code>Tab</code> key once.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://codesandbox.io/embed/navigate-a-grid-using-keyboard-with-roving-tabindex-technique-v5ucrj?fontsize=14&amp;hidenavigation=1&amp;theme=dark&amp;view=preview&amp;runonclick=1">https://codesandbox.io/embed/navigate-a-grid-using-keyboard-with-roving-tabindex-technique-v5ucrj?fontsize=14&amp;hidenavigation=1&amp;theme=dark&amp;view=preview&amp;runonclick=1</a></div>
<p> </p>
<h3 id="heading-create-a-grid-of-buttons">Create a Grid of Buttons</h3>
<p>Create a new react project with the create-react-app command or use whichever CRA alternative you prefer. Then create a new file called <code>GridNavigator.js</code> inside your <code>src</code> folder. Add the following code to the file.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> GridNavigator = <span class="hljs-function">(<span class="hljs-params">{ rows, cols, start }</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> currentIdx = useRef(start);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      {Array.from(Array(rows), (_, row) =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">row-</span>${<span class="hljs-attr">row</span>}`}&gt;</span>
          {Array.from(Array(cols), (_, col) =&gt; {
            const idx = `${row}${col}`;

            return (
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">col-</span>${<span class="hljs-attr">idx</span>}`}
                <span class="hljs-attr">tabIndex</span>=<span class="hljs-string">{currentIdx.current</span> === <span class="hljs-string">idx</span> ? "<span class="hljs-attr">0</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">-1</span>"}
              &gt;</span>
                {idx}
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            );
          })}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>This component takes the number of rows &amp; columns from its parent and creates a corresponding grid of buttons. It also sets the <code>tabindex</code> of each of the buttons to -1 (except the button having its <code>idx === start</code>).</p>
<p>To test this <code>GridNavigator</code> you can call it inside your <code>App.js</code> file as shown below</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { GridNavigator } <span class="hljs-keyword">from</span> <span class="hljs-string">'./GridNavigator'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'./App.css'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">GridNavigator</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">{5}</span> <span class="hljs-attr">cols</span>=<span class="hljs-string">{5}</span> <span class="hljs-attr">start</span>=<span class="hljs-string">'24'</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h3 id="heading-setting-the-accessibility-attributes">Setting the Accessibility Attributes</h3>
<p>It is a good practice to assign proper roles to each grid item for better accessibility. And you should also assign proper ARIA attributes to describe the grid to the users who take the help of assistive technologies. Below are some of the attributes we'll be using</p>
<ul>
<li><p><code>role</code>: Since our grid is made up of interactive elements we'll be using <code>role="grid"</code> for the outermost div, <code>role="row"</code> for each row, and <code>role="gridcell"</code> for the buttons. Other possible replacements for the <code>grid</code> role is the <code>table</code> or the <code>treegrid</code> roles.</p>
</li>
<li><p><code>aria-label</code>: This can be used to give the grid a proper caption.</p>
</li>
<li><p><code>aria-rowcount</code> &amp; <code>aria-colcount</code>: For mentioning the rows &amp; columns count of the grid. These attributes need to be used on the outermost div having the <code>role</code>\="<code>grid".</code></p>
</li>
<li><p><code>aria-rowindex</code> &amp; <code>aria-colindex</code>: On each button having the role of <code>gridcell</code>, we should mention its row &amp; column index.</p>
</li>
</ul>
<p>For more details about various aria attributes related to the grid role, you can <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/grid_role">visit this MDN link</a>.</p>
<p>Make minor adjustments to the code from the last section as shown below</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//... rest of the code</span>
<span class="hljs-keyword">return</span> (
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> 
    <span class="hljs-attr">role</span>=<span class="hljs-string">"grid"</span>
    <span class="hljs-attr">aria-label</span>=<span class="hljs-string">'A grid of buttons'</span>
    <span class="hljs-attr">aria-rowcount</span>=<span class="hljs-string">{rows}</span> 
    <span class="hljs-attr">aria-colcount</span>=<span class="hljs-string">{cols}</span>
  &gt;</span>
    {Array.from(Array(rows), (_, row) =&gt; (
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">row-</span>${<span class="hljs-attr">row</span>}`} <span class="hljs-attr">role</span>=<span class="hljs-string">"row"</span>&gt;</span>
        {Array.from(Array(cols), (_, col) =&gt; {
          const idx = `${row}${col}`;

          return (
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">col-</span>${<span class="hljs-attr">idx</span>}`}
              <span class="hljs-attr">tabIndex</span>=<span class="hljs-string">{currentIdx.current</span> === <span class="hljs-string">idx</span> ? "<span class="hljs-attr">0</span>" <span class="hljs-attr">:</span> "<span class="hljs-attr">-1</span>"}
              <span class="hljs-attr">role</span>=<span class="hljs-string">"gridcell"</span>
              <span class="hljs-attr">aria-rowindex</span>=<span class="hljs-string">{row}</span>
              <span class="hljs-attr">aria-colindex</span>=<span class="hljs-string">{col}</span>
            &gt;</span>
              {idx}
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          );
        })}
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    ))}
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
);
<span class="hljs-comment">//... rest of the code</span>
</code></pre>
<h3 id="heading-adding-the-key-event-listener">Adding the key event listener</h3>
<p>Add a key-down event listener on the outermost div. Apart from the event listener, we will give every button a ref so that we can use it to focus the appropriate button on arrow key presses.</p>
<p>Make the following changes to the <code>GridNavigator</code> component</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// import createRef</span>
<span class="hljs-keyword">import</span> { useRef, createRef } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
</code></pre>
<p>Inside the <code>GridNavigator</code> function, create a <code>ref</code> to store the references of all the grid buttons</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> btnRefs = useRef({});
</code></pre>
<p>Add the key event listener on the div with <code>role="grid"</code>. Also, create and add a ref to all the buttons</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>
      <span class="hljs-attr">role</span>=<span class="hljs-string">'grid'</span>
      <span class="hljs-attr">aria-label</span>=<span class="hljs-string">'A grid of buttons'</span>
      <span class="hljs-attr">aria-rowcount</span>=<span class="hljs-string">{rows}</span>
      <span class="hljs-attr">aria-colcount</span>=<span class="hljs-string">{cols}</span>
      <span class="hljs-attr">onKeyDown</span>=<span class="hljs-string">{onKeyDown}</span>
    &gt;</span>
      {Array.from(Array(rows), (_, row) =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">row-</span>${<span class="hljs-attr">row</span>}`} <span class="hljs-attr">role</span>=<span class="hljs-string">'row'</span>&gt;</span>
          {Array.from(Array(cols), (_, col) =&gt; {
            const idx = `${row}${col}`;
            // If we don't have a ref for this button, create it
            if (!btnRefs.current[idx]) {
              btnRefs.current[idx] = createRef();
            }

            return (
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">col-</span>${<span class="hljs-attr">idx</span>}`}
                <span class="hljs-attr">ref</span>=<span class="hljs-string">{btnRefs.current[idx]}</span>
                <span class="hljs-attr">tabIndex</span>=<span class="hljs-string">{currentIdx.current</span> === <span class="hljs-string">idx</span> ? '<span class="hljs-attr">0</span>' <span class="hljs-attr">:</span> '<span class="hljs-attr">-1</span>'}
                <span class="hljs-attr">role</span>=<span class="hljs-string">'gridcell'</span>
                <span class="hljs-attr">aria-rowindex</span>=<span class="hljs-string">{row}</span>
                <span class="hljs-attr">aria-colindex</span>=<span class="hljs-string">{col}</span>
              &gt;</span>
                {idx}
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            );
          })}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
</code></pre>
<p>Add the <code>onKeyDown</code> function along with the helper functions</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Parses the row &amp; col value of the currently selected button</span>
<span class="hljs-keyword">const</span> parseRowCol = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> row = <span class="hljs-built_in">parseInt</span>(currentIdx.current[<span class="hljs-number">0</span>], <span class="hljs-number">10</span>);
  <span class="hljs-keyword">const</span> col = <span class="hljs-built_in">parseInt</span>(currentIdx.current[<span class="hljs-number">1</span>], <span class="hljs-number">10</span>);
  <span class="hljs-keyword">return</span> { row, col };
};

<span class="hljs-comment">// Moves the focus &amp; saves the id of the newly focused button</span>
<span class="hljs-keyword">const</span> handleFocus = <span class="hljs-function">(<span class="hljs-params">row, col</span>) =&gt;</span> {
  currentIdx.current = <span class="hljs-string">`<span class="hljs-subst">${row}</span><span class="hljs-subst">${col}</span>`</span>;
  <span class="hljs-keyword">const</span> btnRef = btnRefs.current[currentIdx.current];
  btnRef.current.focus();
};

<span class="hljs-comment">// Handles keyboard events</span>
<span class="hljs-keyword">const</span> onKeyDown = <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { row, col } = parseRowCol();

  <span class="hljs-keyword">switch</span> (event.key) {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowUp'</span>:
      <span class="hljs-keyword">if</span> (row &gt; <span class="hljs-number">0</span>) {
        handleFocus(row - <span class="hljs-number">1</span>, col);
      }

      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowDown'</span>:
      <span class="hljs-keyword">if</span> (row &lt; rows - <span class="hljs-number">1</span>) {
        handleFocus(row + <span class="hljs-number">1</span>, col);
      }

      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowLeft'</span>:
      <span class="hljs-comment">// If we're on the leftmost col then move to the extreme right</span>
      <span class="hljs-comment">// col of the previous row, provided it's not the first row</span>
      <span class="hljs-keyword">if</span> (col &gt; <span class="hljs-number">0</span>) {
        handleFocus(row, col - <span class="hljs-number">1</span>);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (row &gt; <span class="hljs-number">0</span>) {
        handleFocus(row - <span class="hljs-number">1</span>, cols - <span class="hljs-number">1</span>);
      }

      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'ArrowRight'</span>:
      <span class="hljs-comment">// If we're on the rightmost col then move to the first </span>
      <span class="hljs-comment">// col of the next row, provided it's not the last row</span>
      <span class="hljs-keyword">if</span> (col &lt; cols - <span class="hljs-number">1</span>) {
        handleFocus(row, col + <span class="hljs-number">1</span>);
      } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (row &lt; rows - <span class="hljs-number">1</span>) {
        handleFocus(row + <span class="hljs-number">1</span>, <span class="hljs-number">0</span>);
      }

      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">default</span>:
      <span class="hljs-keyword">return</span>;
  }
};
</code></pre>
<p>Now try navigating the grid using your keyboard. To start the navigation you need to press the <code>Tab</code> key which will put the focus on the button having <code>idx === start</code>. Afterwards, you can use your arrow keys to navigate the grid. To come out of the grid press the <code>Tab</code> key once more. If you try to focus the grid once more, your focus should land on the exact button where you left it.</p>
<p>This implementation solves all the problems we set out to solve. But some of the end users may not know that they can use the arrow keys to navigate the grid, so we mustn't rely on keyboard navigation alone.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<ul>
<li><p>Keyboard navigation is crucial for web accessibility and improves the user experience for those who depend on it.</p>
</li>
<li><p>The roving tabindex method can enhance keyboard navigation for complicated interactive elements such as grids.</p>
</li>
<li><p>Combining tabindex with correct accessibility attributes such as roles and ARIA attributes ensures that web applications are accessible to a broader audience, including those with disabilities.</p>
</li>
</ul>
<p>Hope you enjoyed reading the article. If you found any mistake in the article please let me know in the comments so that I can fix it.</p>
<p>Cheers :-)</p>
]]></content:encoded></item><item><title><![CDATA[Boost Your API Performance with Firebase CDN: A Guide to API Caching]]></title><description><![CDATA[If you're using or thinking of using Firebase Cloud Functions or Cloud Run for your website/app backend, I highly recommend looking into caching your API responses. Used appropriately it can improve the speed and performance of your website/app. This...]]></description><link>https://rajeev.dev/guide-to-api-caching-with-firebase-cdn</link><guid isPermaLink="true">https://rajeev.dev/guide-to-api-caching-with-firebase-cdn</guid><category><![CDATA[2Articles1Week]]></category><category><![CDATA[APIs]]></category><category><![CDATA[cache]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Fri, 17 Mar 2023 05:10:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678857275569/bba787bc-b4c2-4af2-a49c-b6fdb2f187ba.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're using or thinking of using <code>Firebase Cloud Functions</code> or <code>Cloud Run</code> for your website/app backend, I highly recommend looking into caching your API responses. Used appropriately it can improve the speed and performance of your website/app. This article will give you a simple overview of what CDN and caching are, and how you can easily configure them in Firebase.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>Before going further, let's review the two terms, Caching and CDN, which are essential for a basic understanding of the topic.</p>
<h3 id="heading-what-is-caching">What is Caching?</h3>
<p><strong>Caching</strong> is the process of storing copies of some of your data in temporary storage locations for faster retrieval. These temporary storage locations can be anywhere in between and including the original content storage (e.g. a database) and the client (e.g. your browser).</p>
<h3 id="heading-what-is-a-cdn">What is a CDN?</h3>
<p>A <strong>CDN</strong> or <strong>Content Delivery Network</strong> is a network of geographically distributed servers that are used to serve content/data to end users faster.</p>
<p>These servers sit between us, the client, and the origin server, and can keep a cache of our content for the configured time. Also, since they are distributed across the world, the data is served from the edge that is closest to us. And because of this proximity, the round trip delay is much lower compared to if the request had to be served from the origin server (or simply the origin).</p>
<p><strong>But how does the CDN get the data in the first place?</strong></p>
<p>Initially, the CDN cache doesn't have any data, we call it a <strong><em>cold cache</em></strong>. When a request is made, it is routed to the edge closest to you. The edge location checks its cache, which is empty, gets the data from the origin and returns it to you, simultaneously storing it in its cache (this is called warming the cache) for future requests.</p>
<p><strong>So one request is all it takes for the CDN to get the data?</strong></p>
<p>Yes and no! The edge location closest to you gets the data when you make the first request. Now, any subsequent "<strong><em>similar</em></strong>" request to the same edge, either by you or someone else, is served from the cache. Since this cache has the relevant data for the request, it is called a <strong><em>warm</em></strong> or <strong><em>hot cache</em></strong>.</p>
<p>If a request is made to some other edge location by someone else then that server might not have any data stored yet, so the same process of data fetching and storing gets repeated there.</p>
<p>As you might have guessed, in this article we will be looking at storing our API responses at the CDN for faster retrieval.</p>
<h2 id="heading-configuring-api-caching-with-firebase-cdn">Configuring API Caching with Firebase CDN</h2>
<p>So how do we configure the firebase CDN? If you look at the firebase documentation, you won't find any separate subsection called CDN. Firebase CDN is covered under firebase hosting, you can read about it <a target="_blank" href="https://firebase.google.com/docs/hosting#:~:text=deploy%20web%20apps%20and%20serve%20both%20static%20and%20dynamic%20content%20to%20a%20global%20CDN%20(content%20delivery%20network)">in the introduction here</a>.</p>
<p>To use the CDN for API caching we need to connect our firebase function(s)/cloud run to firebase hosting. But does that mean we need to host our website on firebase hosting? Not necessarily.</p>
<h3 id="heading-setup-a-firebase-project">Setup a Firebase Project</h3>
<p>If you don't have a firebase project already, create one using either the command line or the firebase console.</p>
<p>To show the difference between a direct firebase function call and one through the CDN we'll be creating a simple project. I simply executed the <code>firebase init</code> command inside an empty directory and followed the prompts (selected the default choice for each). In this project, I've enabled only <code>hosting</code> and <code>functions</code>.</p>
<p>This is my current project directory. Everything has been generated by the init command</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678896983266/a9f96ac7-3f02-4cd0-9d38-1f4413114335.png" alt="current project directory structure" class="image--center mx-auto" /></p>
<p>Next, head to the <code>index.js</code> file within the <code>functions</code> folder. I've created two simple functions inside it as shown below. We'll be using one of the functions directly and the other one through the CDN. On function calls, we just respond with preconfigured messages</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> functions = <span class="hljs-built_in">require</span>(<span class="hljs-string">'firebase-functions'</span>);

<span class="hljs-built_in">exports</span>.helloWorld = functions.https.onRequest(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  functions.logger.info(<span class="hljs-string">`helloWorld! Hostname: <span class="hljs-subst">${req.hostname}</span>`</span>);
  res.send({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Hello World!'</span> });
});

<span class="hljs-built_in">exports</span>.wonderfulWorld = functions.https.onRequest(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  functions.logger.info(<span class="hljs-string">`wonderfulWorld! Hostname: <span class="hljs-subst">${req.hostname}</span>`</span>);
  res.send({ <span class="hljs-attr">message</span>: <span class="hljs-string">'What a Wonderful World!'</span> });
});
</code></pre>
<p>If we try to call these functions from our <code>index.html</code> file we'll get <code>CORS</code> errors. So before deploying the functions let's install the <code>cors</code> package using the <code>"npm i cors"</code> command and wrap the two functions' bodies inside it.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> functions = <span class="hljs-built_in">require</span>(<span class="hljs-string">'firebase-functions'</span>);
<span class="hljs-comment">// Require cors. For testing we're allowing it for all origins</span>
<span class="hljs-keyword">const</span> cors = <span class="hljs-built_in">require</span>(<span class="hljs-string">'cors'</span>)({<span class="hljs-attr">origin</span>: <span class="hljs-literal">true</span>})

<span class="hljs-built_in">exports</span>.helloWorld = functions.https.onRequest(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  functions.logger.info(<span class="hljs-string">`helloWorld! Hostname: <span class="hljs-subst">${req.hostname}</span>`</span>);
  cors(req, res, <span class="hljs-function">() =&gt;</span> {
    res.send({ <span class="hljs-attr">message</span>: <span class="hljs-string">'Hello World!'</span> });
  })
});

<span class="hljs-built_in">exports</span>.wonderfulWorld = functions.https.onRequest(<span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  functions.logger.info(<span class="hljs-string">`wonderfulWorld! Hostname: <span class="hljs-subst">${req.hostname}</span>`</span>);
  cors(req, res, <span class="hljs-function">() =&gt;</span> {
    res.send({ <span class="hljs-attr">message</span>: <span class="hljs-string">'What a Wonderful World!'</span> });
  })
});
</code></pre>
<p>Now we're ready to test these functions. Deploy the functions using the firebase deploy command</p>
<pre><code class="lang-bash">firebase deploy --only <span class="hljs-built_in">functions</span>
</code></pre>
<p>Now head over to the <code>index.html</code> file inside the <code>public</code> folder and replace its content with the below code. I've just modified the file generated by firebase and added the two function calls in it.</p>
<p><em>Don't forget to replace</em> <code>&lt;project_id&gt;</code> <em>with your actual project id. You may also need to change the function region if you deployed to a region other than</em> <strong><em>us-central1</em></strong></p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Welcome to Firebase Hosting<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">media</span>=<span class="hljs-string">"screen"</span>&gt;</span><span class="css">
      <span class="hljs-selector-tag">body</span> {
        <span class="hljs-attribute">background</span>: <span class="hljs-number">#eceff1</span>;
        <span class="hljs-attribute">color</span>: <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.87</span>);
        <span class="hljs-attribute">font-family</span>: Roboto, Helvetica, Arial, sans-serif;
        <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">0</span>;
      }
      <span class="hljs-selector-id">#message</span> {
        <span class="hljs-attribute">background</span>: white;
        <span class="hljs-attribute">max-width</span>: <span class="hljs-number">360px</span>;
        <span class="hljs-attribute">margin</span>: <span class="hljs-number">100px</span> auto <span class="hljs-number">16px</span>;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">32px</span> <span class="hljs-number">24px</span>;
        <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">3px</span>;
      }
      <span class="hljs-selector-id">#message</span> <span class="hljs-selector-tag">h1</span> {
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">32px</span>;
        <span class="hljs-attribute">color</span>: <span class="hljs-number">#ffa100</span>;
        <span class="hljs-attribute">font-weight</span>: bold;
        <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> <span class="hljs-number">0</span> <span class="hljs-number">16px</span>;
      }
      <span class="hljs-selector-id">#message</span> <span class="hljs-selector-tag">p</span> {
        <span class="hljs-attribute">line-height</span>: <span class="hljs-number">140%</span>;
        <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
      }
      <span class="hljs-selector-id">#message</span> <span class="hljs-selector-tag">a</span> {
        <span class="hljs-attribute">display</span>: block;
        <span class="hljs-attribute">text-align</span>: center;
        <span class="hljs-attribute">background</span>: <span class="hljs-number">#039be5</span>;
        <span class="hljs-attribute">text-transform</span>: uppercase;
        <span class="hljs-attribute">text-decoration</span>: none;
        <span class="hljs-attribute">color</span>: white;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">16px</span>;
        <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
        <span class="hljs-attribute">cursor</span>: pointer;
      }
      <span class="hljs-selector-id">#message</span> <span class="hljs-selector-tag">a</span><span class="hljs-selector-pseudo">:hover</span> {
        <span class="hljs-attribute">background</span>: <span class="hljs-number">#028bd5</span>;
      }
      <span class="hljs-selector-id">#message</span>,
      <span class="hljs-selector-id">#message</span> <span class="hljs-selector-tag">a</span> {
        <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">1px</span> <span class="hljs-number">3px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.12</span>), <span class="hljs-number">0</span> <span class="hljs-number">1px</span> <span class="hljs-number">2px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.24</span>);
      }
      <span class="hljs-selector-id">#cdn</span>,
      <span class="hljs-selector-id">#direct</span> {
        <span class="hljs-attribute">background</span>: <span class="hljs-number">#e1e1e1</span>;
        <span class="hljs-attribute">padding</span>: <span class="hljs-number">4px</span> <span class="hljs-number">8px</span>;
        <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
        <span class="hljs-attribute">color</span>: black;
      }
      <span class="hljs-keyword">@media</span> (<span class="hljs-attribute">max-width:</span> <span class="hljs-number">600px</span>) {
        <span class="hljs-selector-tag">body</span>,
        <span class="hljs-selector-id">#message</span> {
          <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">0</span>;
          <span class="hljs-attribute">background</span>: white;
          <span class="hljs-attribute">box-shadow</span>: none;
        }
        <span class="hljs-selector-tag">body</span> {
          <span class="hljs-attribute">border-top</span>: <span class="hljs-number">16px</span> solid <span class="hljs-number">#ffa100</span>;
        }
      }
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"message"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Welcome<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Click on the button below to make the API Calls...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">onclick</span>=<span class="hljs-string">"makeCalls()"</span>&gt;</span>Test API Calls<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Response through CDN<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pre</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"cdn"</span>&gt;</span>Waiting for the click<span class="hljs-symbol">&amp;hellip;</span><span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Response through function<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">pre</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"direct"</span>&gt;</span>Waiting for the click<span class="hljs-symbol">&amp;hellip;</span><span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
      <span class="hljs-keyword">const</span> directEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'direct'</span>);
      <span class="hljs-keyword">const</span> cdnEl = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'cdn'</span>);

      <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">makeCalls</span>(<span class="hljs-params"></span>) </span>{
        directEl.textContent = <span class="hljs-string">'Loading...'</span>;
        cdnEl.textContent = <span class="hljs-string">'Loading...'</span>;

        <span class="hljs-keyword">const</span> promises = [
          fetch(
            <span class="hljs-string">'https://us-central1-&lt;project_id&gt;.cloudfunctions.net/helloWorld'</span>
          ),
          fetch(
            <span class="hljs-string">'https://us-central1-&lt;project_id&gt;.cloudfunctions.net/wonderfulWorld'</span>
          ),
        ];

        <span class="hljs-keyword">const</span> [directCall, cdnCall] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.allSettled(promises);
        <span class="hljs-keyword">if</span> (directCall.status === <span class="hljs-string">'fulfilled'</span> &amp;&amp; directCall.value.ok) {
          <span class="hljs-keyword">const</span> directCallData = <span class="hljs-keyword">await</span> directCall.value.json();
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'directCallData'</span>, directCallData);

          directEl.textContent = <span class="hljs-built_in">JSON</span>.stringify(directCallData, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>);
        }

        <span class="hljs-keyword">if</span> (cdnCall.status === <span class="hljs-string">'fulfilled'</span> &amp;&amp; cdnCall.value.ok) {
          <span class="hljs-keyword">const</span> cdnCallData = <span class="hljs-keyword">await</span> cdnCall.value.json();
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'cdnCallData'</span>, cdnCallData);

          cdnEl.textContent = <span class="hljs-built_in">JSON</span>.stringify(cdnCallData, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>);
        }
      }
    </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>If you load this html file in your browser and click the button you should be able to see the configured messages. But the first output label is misleading, as we haven't configured any CDN yet. Let's change that in the next section.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678905100657/733b743d-7c70-4be1-9c95-1fdb7a0238ab.png" alt="api call output" class="image--center mx-auto" /></p>
<p>Below is a screenshot of my Chrome dev console's network tab. As you can see both requests are taking similar times at this point</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678905518507/45f97f97-ad10-444d-92aa-a499ef7cfe64.png" alt="api network calls in the browser dev console" class="image--center mx-auto" /></p>
<h3 id="heading-connecting-firebase-function-andamp-cdn">Connecting Firebase function &amp; CDN</h3>
<p>To connect one of the functions to the CDN, let's head over to the <code>"firebase.json"</code> file at the root of the project, and make the below changes</p>
<pre><code class="lang-json"><span class="hljs-comment">// Modify the hosting attribute of your "firebase.json"</span>
<span class="hljs-string">"hosting"</span>: {
  <span class="hljs-comment">//...other hosting settings</span>

  <span class="hljs-comment">// Add the "rewrites" attribute within "hosting"</span>
  <span class="hljs-attr">"rewrites"</span>: [
    {
      <span class="hljs-attr">"source"</span>: <span class="hljs-string">"/api/wonderful"</span>, <span class="hljs-comment">// Your api route</span>
      <span class="hljs-attr">"function"</span>: <span class="hljs-string">"wonderfulWorld"</span>, <span class="hljs-comment">// Your function name</span>
      <span class="hljs-attr">"region"</span>: <span class="hljs-string">"us-central1"</span> <span class="hljs-comment">// The region where the function is deployed</span>
    }
  ]
}
</code></pre>
<p>If you want to use Google Cloud Run instead of firebase cloud functions, you can connect the two using the below rewrite</p>
<pre><code class="lang-json"><span class="hljs-string">"hosting"</span>: {
  <span class="hljs-attr">"rewrites"</span>: [
    {
      <span class="hljs-attr">"source"</span>: <span class="hljs-string">"/api/wonderful"</span>,
      <span class="hljs-attr">"run"</span>: {
        <span class="hljs-attr">"serviceId"</span>: <span class="hljs-string">"&lt;cloud_run_service_id&gt;"</span>,
        <span class="hljs-attr">"region"</span>: <span class="hljs-string">"us-central1"</span> <span class="hljs-comment">// The region where cloud run is deployed</span>
      }
    }
  ]
}
</code></pre>
<p>Next, head over to the <code>index.html</code> file and replace the <code>"wonderfulWorld"</code> function URL with the below URL. Replace <code>&lt;project_id&gt;</code> with your actual project id.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> promises = [
  fetch(
    <span class="hljs-string">'https://us-central1-&lt;project_id&gt;.cloudfunctions.net/helloWorld'</span>
  ),
  fetch(<span class="hljs-string">'https://&lt;project_id&gt;.web.app/api/wonderful'</span>),
];
</code></pre>
<p>After making this change we need to deploy our changes to firebase hosting. We can simply execute the <code>"firebase deploy" or "firebase deploy --only hosting"</code> command to do it.</p>
<p>Now, if we reload our local <code>index.html</code> file, or, head over to the URL <code>https://&lt;project_id&gt;.web.app</code> and click on the button a couple of times, we should see results similar to as shown below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678939750463/30d41b52-2850-4bae-ba3b-37d649d6775c.png" alt="API calls, direct and through the CDN" class="image--center mx-auto" /></p>
<p>Do note that the response size for the <code>wonderful API call</code> has increased from the earlier value of <code>13 Bytes</code>. Also, as you can see, the first calls for both functions take significantly more time which is because of the cold start of the cloud function. Afterwards, the direct function call is consistently faster compared to the <code>wonderful</code> call routed through the CDN. This is because we've added one extra step to the trip and haven't added any cache yet.</p>
<p>You can click on these calls to see their details (specifically the Response headers)</p>
<p><strong>The helloWorld call</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678941092807/d9ff0a32-ab48-4570-b6dc-a354d899f704.png" alt="helloWorld function call" class="image--center mx-auto" /></p>
<p><strong>The wonderful API call</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678941159068/766cf201-f347-4300-b93d-87b84538db71.png" alt="wonderful api call" class="image--center mx-auto" /></p>
<p>As you can see, there are many extra headers present in this call compared to the previous one. Some of the interesting headers we can notice</p>
<ul>
<li><p><code>x-served-by</code>: it mentions a cache (of course it is empty at this point)</p>
</li>
<li><p><code>vary</code>: This contains more header names as compared to the direct function call</p>
</li>
<li><p><code>x-cache</code>: With value <code>MISS</code>, which is correct because there is nothing in the cache so it missed serving from the cache</p>
</li>
<li><p><code>x-cache-hits</code>: With value <code>0</code>. If it serves from the cache then this value would be a positive integer</p>
</li>
<li><p><code>cache-control</code>: With a value <code>private</code> for both calls. This is the header we'll be modifying for enabling cache. <code>private</code> means that the data is private and can only be stored in a private cache, say your browser.</p>
</li>
</ul>
<h3 id="heading-configuring-cache-control-header">Configuring cache-control header</h3>
<p>Head over to the <code>index.js</code> file within the <code>functions</code> folder, and add the following line to the two functions just before the <code>"res.send"</code> line.</p>
<pre><code class="lang-javascript">res.setHeader(<span class="hljs-string">'cache-control'</span>, <span class="hljs-string">'public, max-age=30, s-maxage=90'</span>);
</code></pre>
<p>Deploy your functions changes by executing <code>"firebase deploy --only functions".</code></p>
<p>What we're doing here is:</p>
<ul>
<li><p>Setting the <code>cache-control</code> header to <code>public</code>. This allows the CDN to cache the data</p>
</li>
<li><p>Setting <code>max-age</code> to <code>30</code>. This means that the browser can store this response, and it will remain fresh and valid for 30 seconds from the time it was generated.</p>
</li>
<li><p>Setting <code>s-maxage</code> to <code>90</code>. This directive is similar to the max-age directive but applies only to the shared caches (the CDNs and proxies in between the origin and the client). So we're storing the response for a longer duration at the CDN. This may or may not be the case always and it can be removed if not needed.</p>
</li>
</ul>
<p>So essentially what we've done is allow caching at the client as well as the intermediate steps in the journey. And also configured the time for which we can go on without asking the origin for fresh data.</p>
<p>Below is what I see in the network tab of my browser's dev console after making some requests</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678948499160/be32d6dc-fff3-452a-9dbb-986076e7408a.png" alt="API calls with cache control header" class="image--center mx-auto" /></p>
<p>As you can see, the first requests for both functions are taking their usual timings. But the second requests which were made after ~10 seconds take only <code>4ms</code>. And we also see that it has been served from the disk cache. That means the responses were cached locally and no new network request was made.</p>
<p>Below are the response headers for the first two <code>wonderful</code> calls. Notice the <code>x-cache</code> header with a value of <code>MISS</code>. If you check your dev console, you'll also see that for the second call, no request headers are present. This is because no network request was made for it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678949164922/82e111b3-8913-42c0-bb3f-cee6e633fe3e.png" alt="wonderful api call 1" class="image--center mx-auto" /></p>
<p>What is interesting is the third batch of requests. The helloWorld function call takes its usual <code>~400-500ms</code> range (because a fresh request was made to the firebase function) but the <code>wonderful</code> call takes only <code>69ms</code>. Further clicking on the <code>wonderful</code> request gives me the below details</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678949057411/2209a84f-f39a-4ce7-b2b2-d7d3e57ab543.png" alt="wonderful api call 3" class="image--center mx-auto" /></p>
<p>We see that the <code>x-cache</code> has a value of <code>HIT</code> now and <code>x-cache-hits</code> is <code>1</code>. This request was served from the cache, and our firebase function was not called. We can also verify this by checking the function logs in the Google Cloud Console.</p>
<p>What we've achieved here is:</p>
<ol>
<li><p>significantly lesser response time</p>
</li>
<li><p>a lesser number of firebase functions invocations</p>
</li>
<li><p>potentially lesser number of database calls (generally you would get the data from a database and not just return a static value which we're doing currently)</p>
</li>
</ol>
<h2 id="heading-when-and-where-to-cache">When and where to cache</h2>
<p>When deciding to use a cache, the first question you should ask yourself is, "When and where (locally, CDN etc.) to do it"? There is no silver bullet here, and every case needs to be analyzed based on its pros and cons. A wrong decision may return stale and irrelevant data to your users.</p>
<p>In general, if the same data is needed for a lot of users then it is a good candidate to be cached at the CDN. For example, weather data, some meta information, a blog post etc. The time for which to store the data depends on the case at hand.</p>
<p>User profile data doesn't change that frequently, but it is useful for only one person so it can be stored locally (using <code>private</code> for <code>cache-control</code>) for a short duration. And so on.</p>
<h2 id="heading-things-to-keep-in-mind-with-firebase-cdn">Things to Keep in Mind with Firebase CDN</h2>
<p>If you're convinced about using a cache for your API, below are some things that you should keep in mind</p>
<ul>
<li><p>In general, a firebase function can be configured for up to 9 minutes of request timeout. But when you connect your functions to firebase hosting, the request timeout value for these functions gets capped at 60 seconds.</p>
</li>
<li><p>Only GET &amp; HEAD requests can be cached at the CDN</p>
</li>
<li><p>For Firebase the Cache keys' generation depends on the following factors. If any of these factors are different a different key gets generated and hence cache hit or miss happens accordingly</p>
<ul>
<li><p>The hostname (<code>&lt;project_id&gt;.web.app</code> in the example project of this article)</p>
</li>
<li><p>The path (<code>/api/wonderful</code>)</p>
</li>
<li><p>The query string (we didn't use any query string)</p>
</li>
<li><p>The content of the request headers specified in the <code>Vary</code> header (by default Firebase CDN uses Origin, cookie, need-authorization, x-fh-requested-host and accept-encoding as can be seen from the screenshots above). Only the <strong>__session</strong> cookie (if present) is made part of the cache key.</p>
</li>
</ul>
</li>
<li><p>Sometimes you need to remove the API data cached at the CDN. This can be done by redeploying to firebase hosting (using <code>firebase deploy --only hosting</code>)</p>
</li>
<li><p>Instead of setting the <code>cache-control</code> header individually within each function, we can add it to <code>firebase.json</code> itself. Though for more control over individual requests caching, adding the header within the function is better.</p>
</li>
</ul>
<h2 id="heading-limitations">Limitations</h2>
<p>While Firebase CDN is good for general use cases, if you want more from your CDN then you should look for a proper CDN service like Cloudflare, Google and so on. Firebase CDN doesn't provide DDoS protection, Rate Limiting etc out of the box. Also, data transfer out of hosting is free till 360MB/day, beyond that you get charged <a target="_blank" href="https://firebase.google.com/pricing">$0.15/GB</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679030406258/7e81492b-30fb-4de2-810d-69ec7822154a.png" alt="firebase hosting pricing" class="image--center mx-auto" /></p>
<h2 id="heading-real-live-caching-example">Real Live Caching Example</h2>
<p>For one of my recent projects, I applied the Firebase CDN cache for one of the endpoints. The project is a daily puzzle game based on React SPA, called <a target="_blank" href="https://playgoldroad.com">GoldRoad</a> where every player gets the same puzzle which gets refreshed at midnight GMT.</p>
<p>This is how I've configured the cache. The request gets stored at the CDN for the maximum time possible (including a buffer of 5 mins).</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> cacheTime = <span class="hljs-number">300</span>; <span class="hljs-comment">// 5 mins local cache time</span>
<span class="hljs-keyword">let</span> serverCacheTime;

<span class="hljs-comment">// 1. game is the current puzzle object</span>
<span class="hljs-comment">// 2. nextGameAt is the dateTime when the new puzzle will be available</span>
<span class="hljs-keyword">const</span> nextGameAtInMs = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(game.nextGameAt).getTime();

<span class="hljs-comment">// Server Cache time, minus the local cache time (some buffer)</span>
serverCacheTime = <span class="hljs-built_in">parseInt</span>((nextGameAtInMs - <span class="hljs-built_in">Date</span>.now()) / <span class="hljs-number">1000</span>) - cacheTime;
<span class="hljs-keyword">if</span> (serverCacheTime &lt; <span class="hljs-number">0</span>) {
  serverCacheTime = <span class="hljs-number">0</span>;
}

<span class="hljs-keyword">if</span> (serverCacheTime &lt; cacheTime) {
  cacheTime = serverCacheTime;
}

response.set(<span class="hljs-string">'Cache-Control'</span>, <span class="hljs-string">`public, max-age=<span class="hljs-subst">${cacheTime}</span>, s-maxage=<span class="hljs-subst">${serverCacheTime}</span>`</span>
);
</code></pre>
<p>Many other directives can be used in the cache-control header. You should read more about these directives for advanced use cases.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, using the firebase CDN can greatly enhance your API performance by caching frequently accessed data closer to the end user, thus reducing latency and improving response times. It also reduces firebase functions invocations, as well as database calls, thus reducing cost.</p>
<p>But before embarking on this journey, do analyze the pros and cons of adding a cache for your use case.</p>
<p>Hope you enjoyed reading the article. If you found any mistake in the article please let me know in the comments.</p>
<p>Cheers :-)</p>
<h2 id="heading-further-reading">Further reading</h2>
<p>We've only looked at a couple of directives for the cache-control header, for a <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control">complete overview please visit the MDN site</a></p>
<p>For understanding caching in detail you can visit the following links</p>
<p><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching">HTTP caching on MDN</a></p>
<p><a target="_blank" href="https://web.dev/http-cache/">HTTP Cache on web.dev</a></p>
]]></content:encoded></item><item><title><![CDATA[Debug or not, there is no middle ground]]></title><description><![CDATA[Introduction
"What is debugging?" is a typical question to start an article on the topic, but this is not that article. Instead what I want you to do here is, take a look at the cover image, and process it. Now, try to answer, why the person in the i...]]></description><link>https://rajeev.dev/debug-or-not-there-is-no-middle-ground</link><guid isPermaLink="true">https://rajeev.dev/debug-or-not-there-is-no-middle-ground</guid><category><![CDATA[DebuggingFeb]]></category><category><![CDATA[2Articles1Week]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sat, 11 Mar 2023 23:17:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678554609137/f6e1c8d5-4d27-4223-945f-102475efcf07.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>"What is debugging?" is a typical question to start an article on the topic, but this is not that article. Instead what I want you to do here is, take a look at the cover image, and process it. Now, try to answer, why the person in the image is doing whatever he is doing. There can be multiple correct answers to this question, but if you answered along the lines of "to keep the crops healthy for a better yield" then you're not far from the zen of software development.</p>
<p>Debugging is not a chore, even though it feels like one at times, but is an important pillar on which any healthy and yielding software is built. You cannot love programming and not love (okay, "love" may be a strong word here, any value on the positive axis should do) debugging at the same time. This is akin to "Heisenberg's Certainty Principle" (is there one?) for Software development. The sooner you start loving the debugging process, the closer you're to achieving zen.</p>
<h2 id="heading-why-and-what-to-debug">Why, and what, to debug?</h2>
<p>Before we get to the "how", we need to understand the "why", and the "what". I'm sure by now you've some inklings on why we need to debug. Because we want our software to be healthy and useful for the end user. And we achieve that by making it error-free while giving the best experience.</p>
<p>The "what" is the next step of the "why". Whatever hinders our ability to give the best experience to the end user needs to be debugged and corrected. Some of these hindrances can be because of faulty code, some because of the UI/UX, some others may be because of the network, and so on.</p>
<p>The "what" may have left you confused. There is no clear answer in the previous paragraph. And you're right because it depends on the situation you're in. In my previous life, I used to work on Video Telephony (think Zoom calls for phones, but long back and with different standards and protocols). Now there was a phone already in the market which used to work fine with other similar phones. Then there was this phone in the testing phase which we were working on. And when you make a video call between these two, no video used to come on either of the devices. Of course, the video call between the two of our devices was fine as well. Now, whose fault is it? The only truth here is that the call should work, and we should debug every entity in between and including the two devices but starting with ours.</p>
<h2 id="heading-how-to-debug">How to debug?</h2>
<p>There are many techniques and approaches to debugging. What you need to understand is that the what, and the how, go hand in hand. Depending on the issue you're facing, one way of debugging might be better suited for the job at hand. And sometimes you may need to apply more than one technique to find out the root cause. I'm not going to go into the definitions and detailed explanations of these techniques, you can read about them with a simple Google Search (or maybe ask ChatGPT?).</p>
<p>The starting point of debugging is not using a debugger or adding some debug messages. Rather it is having at least a basic understanding of how something works. If you have that understanding, you can use any appropriate technique to debug. Let's go back to the video call example. On a high level, the way it works is:</p>
<ol>
<li><p>The calling device sends signals/messages to the server that it wants to do a video call with the other device</p>
</li>
<li><p>The server informs the other device about the incoming call</p>
</li>
<li><p>The two devices do further signalling to arrive at a common medium of communication (the audio &amp; the video codecs)</p>
</li>
<li><p>Finally, the media flows between the devices (with or without the server in the loop)</p>
</li>
</ol>
<p>Now we can start eliminating the suspects based on the information we've at hand. Since the call gets established we can rule out the signalling issues. Next, we check whether the other device's transmitted video packets reach our device and whether our video packets leave the device or not. After verifying all these using network packet sniffers, the conclusion was that there is something wrong with the video packets themselves.</p>
<p>So after talking to my senior, I dumped these incoming/outgoing packets into separate files (like the debug messages), and tried playing these with a video player. Finally, the issue was traced to the other device not sending (and expecting) initial video config header bytes (if I remember correctly, mere 12-16 bytes). And this I could figure out only because I had the required knowledge of the config header standard. The final fix was us adding a check; if the call is with that device, don't send or expect the config bytes and hardcode it. Even though we were doing everything correctly we had to add the fix, because the other device was already live in the market and could not be changed.</p>
<p>So the point I'm trying to make here is that unless you have the needed knowledge it can be very challenging to fix an issue. That doesn't mean that you cannot gain this knowledge in parallel while fixing the issue, in most cases that is how you do it. So try to gain knowledge about how exactly your software works. This may require you to venture beyond your assigned work, but that is how you improve.</p>
<p>Also, many times it is beneficial to discuss the issue with your peers and seniors, as they may provide a clue based on their experiences. And the other point is, sometimes you need to add the fix at your end even if everything is correct there :-). So don't resent it.</p>
<h2 id="heading-what-to-take-home-from-debugging">What to take home from debugging?</h2>
<p>The first thing you should do after any debugging session is, reflect on it. Think about why the issue happened, and how it was fixed. Now that you have that extra knowledge what you could have done better if you had this knowledge before the issue? In my case, I could have checked for the config header bytes in the packet sniffer logs instead of dumping them into files and then finding out.</p>
<p>The second thing is, actually apply whatever you learnt in your previous experiences. Later on, with a different device and codec a similar issue occurred where the initial video was blurry. From the packets' analysis itself, I could figure out that the issue was with the config header.</p>
<p>Every debugging session should make you a smarter and better developer.</p>
<h2 id="heading-dont-rule-out-anything">Don't rule out anything</h2>
<p>During one of my recent debugging sessions, I relearned the fact that one should not rule out anything while fixing an issue.</p>
<h3 id="heading-the-recent-issue">The recent issue</h3>
<p>The issue was the "copy to clipboard" not working on the DuckDuckGo Android browser. The issue was reported by one of the players of my puzzle game <a target="_blank" href="https://playgoldroad.com">GoldRoad</a>. I use the Web Share API for sharing the user's daily stats, and as a fallback for sharing Clipboard API is used for copying the result to the clipboard.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> shareStats = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = <span class="hljs-string">'The text to share'</span>;

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.share) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.share({
        text,
      });
    } <span class="hljs-keyword">catch</span> (error) {}
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.clipboard.writeText(text);
  }
};
</code></pre>
<p>If you look at the compatibility chart for the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API">Clipboard API</a> you'll see that it has wide availability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678573529269/a54fbf29-f60c-4958-8aac-10f3d7bbf348.png" alt="Clipboard compatibility" class="image--center mx-auto" /></p>
<p>Also, the <code>"clipboard-write"</code> permission of the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API"><strong>Permissions API</strong></a> is granted automatically to pages when they are in the active tab. So that should allow me to use the <code>writeText</code> method of the Clipboard API on any browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678573696850/28396ea9-008b-4f7f-93a7-d8f0902754cc.png" alt="writeText compatibility" class="image--center mx-auto" /></p>
<p>But it didn't work despite such reassurances. Now how do you debug the issue considering it needs to be done on Android (the DuckDuckGo Mac client works fine)? Since console logs were of no use, I used the normal p / div tags to print the debugging messages in the browser window itself.</p>
<p>The debugging revealed the following</p>
<ol>
<li><p><code>"Navigator.clipboard.writeText"</code> throws <code>NotAllowedError</code> with a message saying <code>Write permission denied</code></p>
</li>
<li><p>Since write permission was denied, so maybe somehow we could query and ask for the needed permission using the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API"><strong>Permissions API</strong></a>? Alas! <code>"Navigator.permissions"</code> is not available on DuckDuckGo</p>
</li>
</ol>
<h3 id="heading-the-fix">The Fix</h3>
<p>The workaround was to use the <code>document.execCommand</code>. After some searching on Google, <a target="_blank" href="https://web.dev/async-clipboard/#:~:text=be%20triggered%20using-,document.execCommand(%27copy%27),-and%20document.execCommand">found this link</a> with a code snippet</p>
<pre><code class="lang-javascript">button.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> input = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'input'</span>);
  input.style.display = <span class="hljs-string">'none'</span>;
  <span class="hljs-built_in">document</span>.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  <span class="hljs-keyword">const</span> result = <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'copy'</span>);
  <span class="hljs-keyword">if</span> (result === <span class="hljs-string">'unsuccessful'</span>) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to copy text.'</span>);
  }
  input.remove();
});
</code></pre>
<p>But this also didn't work in my case. Further trials and errors showed that we can't do <code>input.style.display = 'none';</code> Because if the element is not visible then it won't work for DuckDuckGo. So the final working code is as below</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> shareStats = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = <span class="hljs-string">`The text to share\nWith multiple lines`</span>;

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.share) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.share({
        text,
      });
    } <span class="hljs-keyword">catch</span> (error) {}

    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">await</span> copyToClipboard(text);
};

<span class="hljs-keyword">const</span> copyToClipboard = <span class="hljs-keyword">async</span> (text) =&gt; {
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.clipboard) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.clipboard.writeText(text);
      <span class="hljs-keyword">return</span>;
    } <span class="hljs-keyword">catch</span> (error) {}
  }

  <span class="hljs-keyword">const</span> textarea = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'textarea'</span>);
  textarea.style.position = <span class="hljs-string">'fixed'</span>;
  textarea.style.width = <span class="hljs-string">'1px'</span>;
  textarea.style.height = <span class="hljs-string">'1px'</span>;
  textarea.style.padding = <span class="hljs-number">0</span>;
  textarea.style.border = <span class="hljs-string">'none'</span>;
  textarea.style.outline = <span class="hljs-string">'none'</span>;
  textarea.style.boxShadow = <span class="hljs-string">'none'</span>;
  textarea.style.background = <span class="hljs-string">'transparent'</span>;

  <span class="hljs-built_in">document</span>.body.appendChild(textarea);

  textarea.textContent = text;
  textarea.focus();
  textarea.select();

  <span class="hljs-keyword">const</span> result = <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'copy'</span>);
  textarea.remove();
  <span class="hljs-keyword">if</span> (!result) {
    <span class="hljs-comment">// Show some error message to the user</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Show a success message to the user mentioning the text is copied to their clipboard</span>
  }
};
</code></pre>
<p>Learnings?</p>
<ol>
<li><p>Don't rule out anything, even if everything says that it will work</p>
</li>
<li><p>Code copied from the internet may not work in its entirety and all situations</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To summarize, debugging plays a crucial role in software development and requires a good grasp of the software's know-how. There are diverse methods and strategies for debugging, and we must remain open-minded while resolving any issue. Each debugging session presents an opportunity for us to enhance our skills and knowledge, so we should welcome it with open arms.</p>
<p>If you liked reading the article, do drop a 👋 in the comments section.</p>
<p>Keep adding the bits, only they make a BYTE. :-)</p>
]]></content:encoded></item><item><title><![CDATA[Create a personal expense tracker using MongoDB Atlas App Services & Triggers]]></title><description><![CDATA[Introduction
Did you know that Atlas App Services is a fully managed cloud service offering from MongoDB that we can use as a backend for our apps? This article provides a step-by-step guide on how to use MongoDB Atlas App Services and its various tr...]]></description><link>https://rajeev.dev/react-app-with-mongodb-atlas-app-services</link><guid isPermaLink="true">https://rajeev.dev/react-app-with-mongodb-atlas-app-services</guid><category><![CDATA[MongoDB]]></category><category><![CDATA[React]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[atlas app services ]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Sat, 04 Mar 2023 15:20:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/DVbf1r8tmUM/upload/a6593770b0659f9271a7324d22729ffe.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Did you know that Atlas App Services is a fully managed cloud service offering from MongoDB that we can use as a backend for our apps? This article provides a step-by-step guide on how to use MongoDB Atlas App Services and its various triggers for creating an app. The app is a basic personal expense tracker, and we will be using the React library to code it from scratch.</p>
<h2 id="heading-what-is-a-trigger">What is a trigger?</h2>
<p>Before moving ahead, let's answer this question first. What is a trigger? I'm sure all of us are familiar with the most common usage of this word, but it is much more than that. "A trigger is a cause or an event that precedes or starts an action". It is like "if-this-then-that" but at a different layer and with a broader scope. Triggers play a crucial role in the smooth functioning of any serverless application development, we must have a good grasp of them.</p>
<p>So which triggers and actions we're talking about here considering application development in general and our app in particular? Well, some of the examples can be</p>
<ol>
<li><p>If someone signs up for our app, then we can send them a welcome email, and/or create a user entry in our database.</p>
</li>
<li><p>If a user does something inside the app, say creates a new transaction then we can update their balance for quick retrieval</p>
</li>
<li><p>If there is an action that happens on a regular schedule, then maybe we can automate it instead of updating it manually, and so on...</p>
</li>
</ol>
<p>There can be many more examples, but these 3 are sufficient to understand the triggers which App services offer. The first one is called the "Auth Trigger" as we're doing something because of the auth event. The second one is a database trigger, as when a new entry is added to the database we do something else asynchronously. And the last one is a scheduled trigger because it happens on a regular schedule. Now let's dive into the app we're going to build.</p>
<h2 id="heading-the-frontend">The Frontend</h2>
<p>As we're building a personal expense tracker, at the bare minimum it needs to have the following features</p>
<ol>
<li><p>Ability to create an account so that we can associate the transactions with a particular user. To keep it simple we'll be using anonymous login to achieve it</p>
</li>
<li><p>Ability to add manual entries for any credit or debit</p>
</li>
<li><p>A basic dashboard where we can get a holistic view of our finances for the month, and also see the individual transactions</p>
</li>
<li><p>On change of month reset the credits/debits and possibly do other related chores</p>
</li>
</ol>
<p>Now that the scope of the work is defined, let's start building it</p>
<h3 id="heading-setting-up-the-react-app">Setting up the React App</h3>
<p>Let's quickly set up a React project using the following set of commands in your terminal window. <em>I'm using "yarn" as my package manager, you can use commands specific to your preferred package manager.</em></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create the project dir &amp; client subdir and immediately cd into it.</span>
<span class="hljs-comment"># "mkdir -p" creates the non existant parent dir.</span>
mkdir -p my-expenses-tracker/client &amp;&amp; <span class="hljs-built_in">cd</span> <span class="hljs-variable">$_</span>

<span class="hljs-comment"># Create a React app in the current dir (client)</span>
yarn create react-app .

<span class="hljs-comment"># Run the app and start the dev server</span>
yarn start
</code></pre>
<p>Open another terminal window, navigate to the client folder and run the following commands.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add react-router-dom and react-icons to the project</span>
yarn add react-router-dom react-icons

<span class="hljs-comment"># Create routes &amp; components folders inside src dir</span>
mkdir src/routes src/components

<span class="hljs-comment"># Create the initial pages &amp; components</span>
touch src/routes/Dashboard.js src/routes/AddExpense.js src/components/Navbar.js

<span class="hljs-comment"># Open the project in VS Code</span>
<span class="hljs-built_in">cd</span> .. &amp;&amp; code .
</code></pre>
<h3 id="heading-creating-the-routes">Creating the routes</h3>
<p>We'll be adding two routes to the app: <strong>1.</strong> the dashboard page, and <strong>2.</strong> the new transaction page. Replace the content of the <code>App.js</code> file with the following:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { BrowserRouter, Routes, Route, Outlet } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;

<span class="hljs-keyword">import</span> { Dashboard } <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/Dashboard'</span>;
<span class="hljs-keyword">import</span> { NewTransaction } <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/NewTransaction'</span>;
<span class="hljs-keyword">import</span> { Navbar } <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/Navbar'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./App.css'</span>;

<span class="hljs-keyword">const</span> Layout = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'app'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Navbar</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Outlet</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">BrowserRouter</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">'/'</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Layout</span> /&gt;</span>}&gt;
          <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">index</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Dashboard</span> /&gt;</span>} /&gt;
          <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">'/new'</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">NewTransaction</span> /&gt;</span>} /&gt;
        <span class="hljs-tag">&lt;/<span class="hljs-name">Route</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">BrowserRouter</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h3 id="heading-the-navbar-component">The Navbar component</h3>
<p>Add the following code into the <code>Navbar.js</code> file that we created earlier. You can get the image assets, as well as the CSS files (<code>index.css</code> &amp; <code>App.css</code>) from <a target="_blank" href="https://github.com/ra-jeev/expense-buddy">the Github repo</a>.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { NavLink } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> { FaPlusCircle } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-icons/fa'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Navbar = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'navbar '</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">NavLink</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'logo nav-link'</span> <span class="hljs-attr">to</span>=<span class="hljs-string">'/'</span>&gt;</span>
        Expense Buddy
      <span class="hljs-tag">&lt;/<span class="hljs-name">NavLink</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">NavLink</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'nav-link'</span> <span class="hljs-attr">to</span>=<span class="hljs-string">'/new'</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">FaPlusCircle</span> /&gt;</span> Add New
      <span class="hljs-tag">&lt;/<span class="hljs-name">NavLink</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
  );
};
</code></pre>
<h3 id="heading-dashboard-page">Dashboard Page</h3>
<p>Add the following code to the <code>Dashboard.js</code> file. We'll come back to this file and modify it to add the backend interaction later on.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;
<span class="hljs-keyword">import</span> { FaPlusCircle } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-icons/fa'</span>;

<span class="hljs-keyword">import</span> { formatDateTime, formatCurrency } <span class="hljs-keyword">from</span> <span class="hljs-string">'../utils'</span>;
<span class="hljs-keyword">import</span> AddImage <span class="hljs-keyword">from</span> <span class="hljs-string">'../assets/images/add-notes.svg'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Dashboard = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [user, setUser] = useState();
  <span class="hljs-keyword">const</span> [transactions, setTransactions] = useState([]);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);

  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'container'</span>&gt;</span>
      {loading ? (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'loader'</span>&gt;</span>Loading...<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ) : transactions.length ? (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'dashboard'</span>&gt;</span>
          {user &amp;&amp; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'card summary-card'</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>This month<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>Current Balance<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details-value'</span>&gt;</span>
                  {formatCurrency(user.balance)}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'card-row'</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details money-in'</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details-label'</span>&gt;</span>Total money in<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details-value'</span>&gt;</span>
                    {formatCurrency(user.currMonth.in)}
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details money-out'</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details-label'</span>&gt;</span>Total money out<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'details-value'</span>&gt;</span>
                    {formatCurrency(user.currMonth.out)}
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          )}

          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'transactions-title'</span>&gt;</span>Transactions<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

          {transactions.map((transaction) =&gt; {
            return (
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
                <span class="hljs-attr">key</span>=<span class="hljs-string">{transaction._id}</span>
                <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">card</span> <span class="hljs-attr">transaction-card</span> ${
                  <span class="hljs-attr">transaction.type</span> === <span class="hljs-string">'IN'</span>
                    ? '<span class="hljs-attr">transaction-in</span>'
                    <span class="hljs-attr">:</span> '<span class="hljs-attr">transaction-out</span>'
                }`}
              &gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>{transaction.comment}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'transaction-date'</span>&gt;</span>
                    {formatDateTime(transaction.createdAt)}
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'transaction-value'</span>&gt;</span>
                  {formatCurrency(transaction.amount)}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            );
          })}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      ) : (
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'no-data'</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">'no-data-img'</span>
            <span class="hljs-attr">src</span>=<span class="hljs-string">{AddImage}</span>
            <span class="hljs-attr">alt</span>=<span class="hljs-string">'No transactions found, add one'</span>
          /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'no-data-text'</span>&gt;</span>No transactions found<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">type</span>=<span class="hljs-string">'button'</span>
            <span class="hljs-attr">className</span>=<span class="hljs-string">'btn btn-primary'</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> navigate('/new')}
          &gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">FaPlusCircle</span> /&gt;</span> Add Transaction
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      )}
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>Without anything to show, the page looks like the below screenshot</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677939077152/2a4e7af6-2aa0-4a7d-bd2d-b2a0ec7318b0.png" alt="dashboard page without any data" class="image--center mx-auto" /></p>
<h3 id="heading-newtransaction-page">NewTransaction Page</h3>
<p>Here we create a form to submit a new transaction to the database. Right now it is just the barebones file, we'll add the backend interaction later on</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-router-dom'</span>;

<span class="hljs-keyword">const</span> INITIAL_STATE = {
  <span class="hljs-attr">comment</span>: <span class="hljs-string">''</span>,
  <span class="hljs-attr">amount</span>: <span class="hljs-string">''</span>,
  <span class="hljs-attr">type</span>: <span class="hljs-string">''</span>,
};

<span class="hljs-keyword">const</span> TRANSACTION_TYPES = {
  <span class="hljs-attr">SELECT</span>: <span class="hljs-string">'Select a type'</span>,
  <span class="hljs-attr">IN</span>: <span class="hljs-string">'Add'</span>,
  <span class="hljs-attr">OUT</span>: <span class="hljs-string">'Deduct'</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> NewTransaction = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> [formState, setFormState] = useState(INITIAL_STATE);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [message, setMessage] = useState(<span class="hljs-literal">null</span>);

  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-keyword">const</span> setInput = <span class="hljs-function">(<span class="hljs-params">key, value</span>) =&gt;</span> {
    setFormState({ ...formState, [key]: value });
  };

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (message) {
      <span class="hljs-keyword">const</span> timerId = <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (message.type === <span class="hljs-string">'success'</span>) {
          navigate(<span class="hljs-string">'/'</span>, { <span class="hljs-attr">replace</span>: <span class="hljs-literal">true</span> });
        }
        setMessage(<span class="hljs-literal">null</span>);
      }, <span class="hljs-number">2000</span>);

      <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">clearTimeout</span>(timerId);
    }
  }, [message, navigate]);

  <span class="hljs-keyword">const</span> onSubmit = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">const</span> amount = <span class="hljs-built_in">parseFloat</span>(formState.amount);
    <span class="hljs-keyword">const</span> comment = formState.comment.trim();

    <span class="hljs-keyword">if</span> (!amount || !comment || !formState.type) {
      alert(<span class="hljs-string">'Please fill in all fields'</span>);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'final transaction data'</span>, {
        ...formState,
        <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
      });
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'failed to save the transaction'</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'container'</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'card transaction-form'</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Add transaction details<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{onSubmit}</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'form-group'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">'name'</span>&gt;</span>Transaction amount<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">'number'</span>
              <span class="hljs-attr">name</span>=<span class="hljs-string">'amount'</span>
              <span class="hljs-attr">id</span>=<span class="hljs-string">'amount'</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Enter the amount'</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{formState.amount}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInput('amount', e.target.value)}
            /&gt;
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'form-group'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">'comment'</span>&gt;</span>Transaction comment<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">'text'</span>
              <span class="hljs-attr">name</span>=<span class="hljs-string">'comment'</span>
              <span class="hljs-attr">id</span>=<span class="hljs-string">'comment'</span>
              <span class="hljs-attr">placeholder</span>=<span class="hljs-string">'Transaction comment'</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{formState.comment}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInput('comment', e.target.value)}
            /&gt;
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'form-group'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">'transaction-type'</span>&gt;</span>Transaction type<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">select</span>
              <span class="hljs-attr">name</span>=<span class="hljs-string">'transaction-type'</span>
              <span class="hljs-attr">id</span>=<span class="hljs-string">'transaction-type'</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{formState.type}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInput('type', e.target.value)}
            &gt;
              {Object.keys(TRANSACTION_TYPES).map((type) =&gt; {
                return (
                  <span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">type-</span>${<span class="hljs-attr">type</span>}`} <span class="hljs-attr">value</span>=<span class="hljs-string">{type}</span>&gt;</span>
                    {TRANSACTION_TYPES[type]}
                  <span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
                );
              })}
            <span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

          {message &amp;&amp; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{</span>`<span class="hljs-attr">message-</span>${<span class="hljs-attr">message.type</span>}`}&gt;</span>{message.text}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          )}

          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">'card-row'</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">'btn btn-outlined'</span>
              <span class="hljs-attr">disabled</span>=<span class="hljs-string">{loading}</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">'button'</span>
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setFormState(INITIAL_STATE)}
            &gt;
              Cancel
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span> 
              <span class="hljs-attr">className</span>=<span class="hljs-string">'btn btn-primary'</span>
              <span class="hljs-attr">disabled</span>=<span class="hljs-string">{loading}</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">'submit'</span>
            &gt;</span>
              Save
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
};
</code></pre>
<p>This is how the page looks</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677939012443/ba5aed05-55c2-4ff9-bd46-646b58efb566.png" alt="new transaction page" class="image--center mx-auto" /></p>
<h3 id="heading-utility-functions">Utility functions</h3>
<p>Create a new folder called <code>utils</code> in the <code>src</code> directory, and add an index.js file inside it. Then add the following utility functions for formatting dates and currency in it. You can change the currency code to your desired currency, or even make it configurable per user.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> dateTimeFormatter;
<span class="hljs-keyword">let</span> currencyFormatter;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> formatDateTime = <span class="hljs-function">(<span class="hljs-params">dateString</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!dateString) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>;
  }

  <span class="hljs-keyword">if</span> (!dateTimeFormatter) {
    dateTimeFormatter = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Intl</span>.DateTimeFormat(<span class="hljs-string">'en-US'</span>, {
      <span class="hljs-attr">year</span>: <span class="hljs-string">'numeric'</span>,
      <span class="hljs-attr">month</span>: <span class="hljs-string">'short'</span>,
      <span class="hljs-attr">day</span>: <span class="hljs-string">'numeric'</span>,
      <span class="hljs-attr">hour</span>: <span class="hljs-string">'numeric'</span>,
      <span class="hljs-attr">minute</span>: <span class="hljs-string">'numeric'</span>,
    });
  }

  <span class="hljs-keyword">return</span> dateTimeFormatter.format(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(dateString));
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> formatCurrency = <span class="hljs-function">(<span class="hljs-params">amount</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (!currencyFormatter) {
    currencyFormatter = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Intl</span>.NumberFormat(<span class="hljs-string">'en-US'</span>, {
      <span class="hljs-attr">style</span>: <span class="hljs-string">'currency'</span>,
      <span class="hljs-attr">currency</span>: <span class="hljs-string">'INR'</span>,
      <span class="hljs-attr">maximumFractionDigits</span>: <span class="hljs-number">2</span>,
    });
  }

  <span class="hljs-keyword">return</span> currencyFormatter.format(amount);
};
</code></pre>
<p>This is my current project folder structure after making the above changes. I've removed the <code>logo.svg</code> file from the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677828821723/44a18f14-8630-41d8-b3a8-36fd0012eeeb.png" alt="current project folder structure" class="image--center mx-auto" /></p>
<h2 id="heading-the-backend">The Backend</h2>
<p>First of all, we'll create a new cluster on MongoDB Atlas. For our current purposes, an M0 FREE shared cluster is fine. If this is your first time interacting with MongoDB Atlas, you can use <a target="_blank" href="https://www.mongodb.com/docs/guides/atlas/account/">this excellent guide</a> to get started (you can follow till the <code>"Configure a Network Connection"</code> section).</p>
<h3 id="heading-database-and-collections">Database and collections</h3>
<p>Now let's create a database, and add <code>transactions</code> collection to it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677831290155/7e169c3d-8934-4578-8cae-123fb1c2dd88.png" alt="Create database page" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677831327953/b53585b0-9725-4a7f-9f6d-ab8dd775d498.png" alt="create database and collection" class="image--center mx-auto" /></p>
<p>Add one more collection called <code>"users"</code> to the database</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677831395211/609d9579-44df-45a1-8533-2b7bdcf0e33a.png" alt="create users collection" class="image--center mx-auto" /></p>
<h3 id="heading-atlas-app-services">Atlas App Services</h3>
<p>Head over to the <code>App Services</code> tab and create a new Atlas App Services application. Click next in the <code>Start with an app template</code> screen (while <code>"Build your own App"</code> is selected, the default).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677833276786/ffc73221-92d5-42b7-b237-beeb76e7c798.png" alt="Create Atlas App Services app" class="image--center mx-auto" /></p>
<p>Give a name to your application, and click on the <code>"Create App Service"</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677833416332/34a74faf-fe76-4945-b9a9-b9ffbbab55f9.png" alt="configure Atlas App Services App" class="image--center mx-auto" /></p>
<h3 id="heading-authentication">Authentication</h3>
<p>Click on Authentication from the left sidebar and enable anonymous auth and save the draft.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677833730018/15cc1f98-8c90-4b77-9dfb-c7b3919bde54.png" alt="enable anonymous auth" class="image--center mx-auto" /></p>
<p>After making any changes in the App Services application, we need to deploy the changes for it to take effect. Click on <code>REVIEW DRAFT &amp; DEPLOY</code> button and deploy the change<code>.</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677835651069/1e16a637-f948-4f87-b7bd-a710a034b83f.png" alt="deploy changes" class="image--center mx-auto" /></p>
<h3 id="heading-creating-the-auth-trigger">Creating the Auth Trigger</h3>
<p>Now we're ready to create our first trigger. Whenever a user signs up for our application (even if anonymously), we will create a corresponding document in the <code>"users"</code> collection in the database.</p>
<p>Click on triggers from the left sidebar, select <code>Authentication Triggers</code> from the dropdown menu on the top left, and click on <code>Add an Authentication Trigger</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677836251625/43444c56-9d30-4e5a-8821-bcafc8855869.png" alt="create auth trigger" class="image--center mx-auto" /></p>
<p>For <code>Action Type,</code> choose <code>Create</code>, from <code>Providers</code> dropdown pick <code>Anonymous</code>, and select <code>function</code> as the <code>Event Type</code>. Doing this allows us to automatically trigger a function whenever a new user signs up for our application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677838425154/4c61e0ff-29c5-499c-a60f-8476a7665515.png" alt="Configure auth trigger" class="image--center mx-auto" /></p>
<p>Add the following code in the code panel on the same screen. Don't forget to replace the <code>&lt;db_name&gt;</code> with your database name. What we do here is, from the incoming auth event get the auth user id, and create an entry in the <code>"users"</code> collection with some default values.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">authEvent</span>) </span>{
  <span class="hljs-keyword">const</span> { user, time } = authEvent;

  <span class="hljs-keyword">const</span> mongoDb = context.services.get(<span class="hljs-string">'mongodb-atlas'</span>).db(<span class="hljs-string">'&lt;db_name&gt;'</span>);
  <span class="hljs-keyword">const</span> usersCollection = mongoDb.collection(<span class="hljs-string">'users'</span>);

  <span class="hljs-keyword">const</span> userData = {
    <span class="hljs-attr">_id</span>: BSON.ObjectId(user.id),
    <span class="hljs-attr">balance</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">currMonth</span>: {
      <span class="hljs-attr">in</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">out</span>: <span class="hljs-number">0</span>,
    },
    <span class="hljs-attr">createdAt</span>: time, 
    <span class="hljs-attr">updatedAt</span>: time,
  };

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> usersCollection.insertOne(userData);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'result of user insert op: '</span>, <span class="hljs-built_in">JSON</span>.stringify(res));
};
</code></pre>
<p>Afterwards, deploy your changes for them to take effect. Should you need to make any changes to the function code, you can do so by clicking the <code>Functions</code> menu item from the left sidebar and then click on your function name.</p>
<h3 id="heading-testing-the-auth-trigger">Testing the Auth Trigger</h3>
<p>Now we're ready to test the Auth trigger we created in the last section. Before doing that we'll pull the App Services application to our local machine. It is not mandatory to do so, but having the functions' code on the local machine, makes it easier to make changes. Let's install the <code>"realm-cli"</code> and configure it <a target="_blank" href="https://www.mongodb.com/docs/atlas/app-services/cli/">using this guide</a>.</p>
<p>Now pull the application code by firing up a terminal, navigate to the project's root directory and run the following command.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># This will pull the application to the backend </span>
<span class="hljs-comment"># folder (the folder will be created automatically)</span>
<span class="hljs-comment"># Also, don't forget to use your app_id</span>
realm-cli pull --<span class="hljs-built_in">local</span> backend/ --remote &lt;app_id&gt;
</code></pre>
<p>Now go to the <code>client</code> folder, and install the <code>realm-web</code> SDK.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># From project root run the following</span>
<span class="hljs-built_in">cd</span> client &amp;&amp; yarn add realm-web

<span class="hljs-comment"># Make a new file for handling realm auth etc</span>
touch src/RealmApp.js
</code></pre>
<p>Add the following code to the <code>RealmApp.js</code> file</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { createContext, useContext, useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> Realm <span class="hljs-keyword">from</span> <span class="hljs-string">'realm-web'</span>;

<span class="hljs-keyword">const</span> RealmContext = createContext(<span class="hljs-literal">null</span>);

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RealmAppProvider</span>(<span class="hljs-params">{ appId, children }</span>) </span>{
  <span class="hljs-keyword">const</span> [realmApp, setRealmApp] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [appDB, setAppDB] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [realmUser, setRealmUser] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    setRealmApp(Realm.getApp(appId));
  }, [appId]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> init = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">if</span> (!realmApp.currentUser) {
        <span class="hljs-keyword">await</span> realmApp.logIn(Realm.Credentials.anonymous());
      }

      setRealmUser(realmApp.currentUser);
      setAppDB(
        realmApp.currentUser
          .mongoClient(process.env.REACT_APP_MONGO_SVC_NAME)
          .db(process.env.REACT_APP_MONGO_DB_NAME)
      );
    };

    <span class="hljs-keyword">if</span> (realmApp) {
      init();
    }
  }, [realmApp]);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">RealmContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">realmUser</span>, <span class="hljs-attr">appDB</span> }}&gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">RealmContext.Provider</span>&gt;</span></span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useRealmApp</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> app = useContext(RealmContext);
  <span class="hljs-keyword">if</span> (!app) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(
      <span class="hljs-string">`No Realm App found. Did you call useRealmApp() inside of a &lt;RealmAppProvider /&gt;.`</span>
    );
  }

  <span class="hljs-keyword">return</span> app;
}
</code></pre>
<p>Modify the <code>App.js</code> file and add the following changes to it</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Add the RealmApp import</span>
<span class="hljs-keyword">import</span> { RealmAppProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'./RealmApp'</span>;

<span class="hljs-comment">// Wrap the BrowserRouter inside the RealmAppProvider</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">RealmAppProvider</span> <span class="hljs-attr">appId</span>=<span class="hljs-string">{process.env.REACT_APP_REALM_APP_ID}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">BrowserRouter</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">'/'</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Layout</span> /&gt;</span>}&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">index</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Dashboard</span> /&gt;</span>} /&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">'/new'</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">NewTransaction</span> /&gt;</span>} /&gt;
          <span class="hljs-tag">&lt;/<span class="hljs-name">Route</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">BrowserRouter</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">RealmAppProvider</span>&gt;</span></span>
  );
}
</code></pre>
<p>Create an env file named <code>.env</code> at the root of the react project and add the following keys and their respective values</p>
<pre><code class="lang-ini"><span class="hljs-attr">REACT_APP_REALM_APP_ID</span>=&lt;realm_app_id&gt;
<span class="hljs-attr">REACT_APP_MONGO_SVC_NAME</span>=mongodb-atlas
<span class="hljs-attr">REACT_APP_MONGO_DB_NAME</span>=&lt;db_name&gt;
</code></pre>
<p>At this point, the project structure looks like the following</p>
<p><strong>The client folder</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677863134116/fb9ab78d-a253-46fe-8f9e-f33571dbcf7d.png" alt="client folder structure" class="image--center mx-auto" /></p>
<p><strong>The backend folder</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677863204436/825fcf67-d0ab-40f8-b7ea-4b713a46bd28.png" alt="the backend folder structure" class="image--center mx-auto" /></p>
<p>Restart the dev server for the env values to take effect. As soon as the application loads in your browser, an anonymous user should have been created in the realm app. You can check it by going to your <code>App Services</code> dashboard, and clicking <code>App Users</code> from the left sidebar menu. To view the function logs, we can click on <code>"Logs"</code> from the same sidebar menu.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677862672180/696996f4-f283-429b-8bee-d8f059241eff.png" alt="app services users" class="image--center mx-auto" /></p>
<p>Also, if you go the <code>"Data Services"</code> tab, and browse your database's users collection, you should see an entry there. This was created by the Auth Trigger we had created earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677862737974/45558c99-8c74-4a1b-b29f-faa1731f95f5.png" alt="user in the users collection of the database" class="image--center mx-auto" /></p>
<p>Congratulations are in order. You've made it this far, you were able to create a trigger, and make it work successfully :-).</p>
<h3 id="heading-database-interactions-from-the-client">Database interactions from the client</h3>
<p>For interacting with our database through the Realm SDK, we need to define the data access rules first. Without that we won't be able to read or write to the database. You can verify this by doing the following changes to our <code>Dashboard.js</code> file.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Add the following import</span>
<span class="hljs-keyword">import</span> { useRealmApp } <span class="hljs-keyword">from</span> <span class="hljs-string">'../RealmApp'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Dashboard = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-comment">// ...</span>

  <span class="hljs-comment">// Add the following before the return statement</span>
  <span class="hljs-keyword">const</span> { appDB } = useRealmApp();

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> getUser = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> appDB.collection(<span class="hljs-string">'users'</span>).find({});
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'got some user'</span>, res);
    };

    <span class="hljs-keyword">if</span> (appDB) {
      getUser();
    }
  }, [appDB]);

  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>After saving the code if you try to get the user from the database you'll get the following error message</p>
<pre><code class="lang-javascript">no rule exists <span class="hljs-keyword">for</span> namespace <span class="hljs-string">'&lt;your_db_name&gt;.users'</span>
</code></pre>
<p>Then how could the user entry creation in the database get through earlier, you might ask? Well, the backend triggers are run as the system, so it has the required privileges. You can verify whether the triggers run as <code>"system"</code> or not by adding the following console log to the auth trigger function code.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">'context.user.type:'</span>, context.user.type)
</code></pre>
<p>Also, the rules we're talking about here are for the Realm App (used by the React App) to access the database on our behalf. Click on <code>Rules</code> from the sidebar under <code>DATA ACCESS</code> menu section. Then click on the <code>"users"</code> collection in the middle panel, and click on <code>Skip (start from scratch)</code> at the bottom of the rightmost panel.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677870093356/1ccec609-e70c-4cac-a283-5671df525beb.png" alt="add data access rules" class="image--center mx-auto" /></p>
<p>Give this rule a proper name, say ReadWriteOwn, and click on <code>Advanced Document Filters</code>, and add the following <code>JSON</code> expression to both the read and the write text boxes. Select <code>Read and write all fields</code> from the dropdown menu at the bottom, save the draft, and then deploy your changes.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"_id"</span>: {
        <span class="hljs-attr">"%stringToOid"</span>: <span class="hljs-string">"%%user.id"</span>
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677871339743/1c72645a-6d5c-4377-a41c-2bb1b0bbaf86.png" alt="Add read / write checks for authroization" class="image--center mx-auto" /></p>
<p>What we're doing above is: matching the incoming userId (from the HTTP request that the Realm SDK makes) against the <code>_id</code>(which is the document owner's user id ) of the document. If both are the same, the user making the request will be authorized to read/write, else the access will be denied. This is necessary if we don't want any unauthorized access to our data, which is true in this case. Also, <code>"%stringToOid": "%%user.id"</code> converts the incoming user id (which is a <code>string</code>) to the mongo <code>ObjectId</code> so that we can compare it against <code>_id</code> (which is an <code>ObjectId</code>).</p>
<p>Now if you reload the dashboard page, you can verify that the user data is getting returned successfully from the database through the console log we've added there.</p>
<h3 id="heading-add-transactions">Add Transactions</h3>
<p>We're ready to create transactions now. Let's make the same data access rules for the <code>"transactions"</code> collection with two minor changes.</p>
<ol>
<li><p>Users should be able to create new transactions on their own, so we need to allow inserting new documents into the collection (as opposed to the <code>"users"</code> collection where the insert happens only once, and that too from the auth trigger). So we need to check/select the <code>"insert"</code> option (just above the <code>Advanced Document Filters</code>).</p>
</li>
<li><p>We'll save the transaction's owner id in a new field <code>owner_id</code> (which will be a <code>string</code>), so we don't need to convert the incoming userId to an ObjectId. Use the following for <code>"Advance Document Filters"</code> read &amp; write text boxes.</p>
</li>
</ol>
<pre><code class="lang-json">{
  <span class="hljs-attr">"owner_id"</span>: <span class="hljs-string">"%%user.id"</span>
}
</code></pre>
<p>Save the draft and deploy your changes. Head over to the <code>NewTransaction.js</code> file and add the following changes.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Add the following import statement</span>
<span class="hljs-keyword">import</span> { useRealmApp } <span class="hljs-keyword">from</span> <span class="hljs-string">'../RealmApp'</span>;

<span class="hljs-comment">// Call useRealmApp inside the function component</span>
<span class="hljs-keyword">const</span> { appDB, realmUser } = useRealmApp();

<span class="hljs-comment">// Replace the onSubmit function with the following</span>
<span class="hljs-keyword">const</span> onSubmit = <span class="hljs-keyword">async</span> (e) =&gt; {
    e.preventDefault();

    <span class="hljs-keyword">const</span> amount = <span class="hljs-built_in">parseFloat</span>(formState.amount);
    <span class="hljs-keyword">const</span> comment = formState.comment.trim();

    <span class="hljs-keyword">if</span> (!amount || !comment || !formState.type) {
      alert(<span class="hljs-string">'Please fill in all fields'</span>);
      <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> finalData = {
        amount,
        comment,
        <span class="hljs-attr">type</span>: formState.type,
        <span class="hljs-attr">owner_id</span>: realmUser.id,
        <span class="hljs-attr">createdAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
      };

      setLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> appDB.collection(<span class="hljs-string">'transactions'</span>).insertOne(finalData);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'result of insert op'</span>, res);

      setFormState(INITIAL_STATE);
      setMessage({ <span class="hljs-attr">type</span>: <span class="hljs-string">'success'</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">'Successfully saved the transaction.'</span> });
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'failed to save the transaction'</span>);
      setMessage({ <span class="hljs-attr">type</span>: <span class="hljs-string">'error'</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">'Failed to save the transaction.'</span> });
    }

    setLoading(<span class="hljs-literal">false</span>);
  };
</code></pre>
<p>Now try creating a transaction, you should be able to see the created transaction in the database. But the main balance in the user document won't change as we haven't written any trigger for that. Let's remedy that and create our second trigger in the next section.</p>
<h3 id="heading-creating-a-database-trigger">Creating a Database Trigger</h3>
<p>What we want to do here is: whenever a new transaction is created by the user, we update the main balance as well as the in/out values for the current month. Remember the user document had the below structure</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> userData = {
    <span class="hljs-attr">_id</span>: BSON.ObjectId(user.id),
    <span class="hljs-attr">balance</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">currMonth</span>: {
      <span class="hljs-attr">in</span>: <span class="hljs-number">0</span>,
      <span class="hljs-attr">out</span>: <span class="hljs-number">0</span>,
    },
    <span class="hljs-attr">createdAt</span>: time, 
    <span class="hljs-attr">updatedAt</span>: time,
};
</code></pre>
<p>Head over to the App Services Triggers section and create a new database trigger.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677905186053/a6adb894-366d-448a-b28b-c57b6e2ae744.png" alt="creating a database trigger" class="image--center mx-auto" /></p>
<p>Select your cluster and database from the dropdowns, and choose transactions as the collection name. Again select function as the event type and create a new function by giving it an appropriate name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677905381935/b2ee859d-9149-4e97-af37-d4ab4a2a08fd.png" alt="configuring database trigger" class="image--center mx-auto" /></p>
<p>Add the following code to the code panel for the function. What the code essentially does is: get the inserted document, extract the <code>owner_id</code> from it, and then update the corresponding user document. We use the <code>$inc</code> pipeline of mongoDB to increment (or decrement in case of deduction by making the value negative) the respective fields.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">changeEvent</span>) </span>{
    <span class="hljs-keyword">const</span> doc = changeEvent.fullDocument;

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'incoming doc:'</span>, <span class="hljs-built_in">JSON</span>.stringify(doc))

    <span class="hljs-keyword">const</span> filter = { <span class="hljs-attr">_id</span>: BSON.ObjectId(doc.owner_id) };
    <span class="hljs-keyword">const</span> update = {
      <span class="hljs-attr">$set</span>: { <span class="hljs-attr">updatedAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() },
      <span class="hljs-attr">$inc</span>: {},
    };

    <span class="hljs-keyword">if</span> (doc.type === <span class="hljs-string">'IN'</span>) {
      update.$inc.balance = doc.amount;
      update.$inc[<span class="hljs-string">'currMonth.in'</span>] = doc.amount;
    } <span class="hljs-keyword">else</span> {
      update.$inc.balance = -doc.amount;
      update.$inc[<span class="hljs-string">'currMonth.out'</span>] = doc.amount;
    }

    <span class="hljs-comment">// Replace the DB name with your db name</span>
    <span class="hljs-keyword">const</span> usersCollection = context.services
      .get(<span class="hljs-string">'mongodb-atlas'</span>)
      .db(<span class="hljs-string">'&lt;db_name&gt;'</span>)
      .collection(<span class="hljs-string">'users'</span>);

    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> usersCollection.updateOne(filter, update);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'update op res:'</span>, <span class="hljs-built_in">JSON</span>.stringify(res));
};
</code></pre>
<p>Now go to the <code>Dashboard.js</code> file and make the following changes to the component code</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// import BSON from 'realm-web</span>
<span class="hljs-keyword">import</span> { BSON } <span class="hljs-keyword">from</span> <span class="hljs-string">'realm-web'</span>;

<span class="hljs-comment">// Destructure realmUser also from useRealmApp</span>
<span class="hljs-keyword">const</span> { realmUser, appDB } = useRealmApp();

<span class="hljs-comment">// Update the useEffect to the following</span>
useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> getUser = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> appDB
        .collection(<span class="hljs-string">'users'</span>)
        .findOne({ <span class="hljs-attr">_id</span>: <span class="hljs-keyword">new</span> BSON.ObjectId(realmUser.id) });
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'got some user'</span>, res);
      setUser(res);
    };

    <span class="hljs-keyword">const</span> getTransactions = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> appDB.collection(<span class="hljs-string">'transactions'</span>).find({});
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'got transactions res'</span>, res);
      setTransactions(res);
      setLoading(<span class="hljs-literal">false</span>);
    };

    <span class="hljs-keyword">if</span> (appDB) {
      getUser();
      getTransactions();
    }
}, [appDB, realmUser]);
</code></pre>
<p>We're using the realm user id to get the user data now. Also, we've added the code to fetch the user's transactions. After saving the code, make a couple of transactions to see if everything is working properly (it is better to delete the transactions before the database trigger was created for the Math to add up). You should get a screen like the below screenshot</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677912282919/e82a67a0-4ca2-45d3-bd88-4bc910c3cd9f.png" alt="dashboard with transactions" class="image--center mx-auto" /></p>
<p>If you observe, you'll see that the transactions are in the order in which they were added to the database. Also, we only want to show transactions for the current month in the dashboard. You can verify this by adding an entry for the last month directly to the database, and then on dashboard refresh that entry also shows up (do note that this also changes the main balances as there is no date/month guard for the insert trigger).</p>
<p>To rectify the above issues, make the following changes to the realm call</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
date.setDate(<span class="hljs-number">1</span>);
date.setHours(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);

<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> appDB.collection(<span class="hljs-string">'transactions'</span>).find(
    {
        <span class="hljs-attr">createdAt</span>: { <span class="hljs-attr">$gte</span>: date, <span class="hljs-attr">$lte</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() },
    },
    {
        <span class="hljs-attr">sort</span>: {
            <span class="hljs-attr">createdAt</span>: <span class="hljs-number">-1</span>,
        },
    }
);
</code></pre>
<p>We've added the descending sort order for the matched entries. Also, we're only asking for the documents added on or after day 1 of the current month using the <code>$gte</code> (greater than or equal to) pipeline. I've added the upper bound till the current time (<code>$lte</code> pipeline, less than or equal to) also, though it is not needed. After making these changes you should get the transaction entries in the correct order, and only for the current month.</p>
<p>Congratulations on creating and making the second type of trigger work. <strong>👏</strong></p>
<h3 id="heading-handling-month-changes">Handling Month Changes</h3>
<p>Now the only thing remaining is: what happens when the month changes? Since we only want to show the inflows and outflows for the current month, we need to reset them to 0 on the month change, and this should happen automatically. The way to do this is a <code>Scheduled Trigger</code> (also known as a <code>CRON</code> job).</p>
<p>Let's go to the app services dashboard one final time, and click on <code>Triggers</code> in the left sidebar. Then click on "<code>Add a Trigger"</code> button, and select <code>Scheduled</code> as the <code>"Trigger Type"</code>. Change the <code>"Schedule Type"</code> to <code>Advanced</code> and use <code>0 0 1 * *</code> as the <code>CRON schedule</code>. You can see the dates with times when the next event will occur. Please note that these times are as per the UTC timezone. You can make appropriate changes in hours &amp; minutes if you want to use other timezones.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1677917334763/04d0ea05-af88-42c7-9ccc-aa67f075ea6c.png" alt="creating a scheduled trigger" class="image--center mx-auto" /></p>
<p>Finally select <code>Function</code> as the event type, and add the following code in the code text box. We just fetch the users who've done any transaction during the last month (the in/out field(s) would be non-zero), and set them to 0.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> usersCollection = context.services
    .get(<span class="hljs-string">'mongodb-atlas'</span>)
    .db(<span class="hljs-string">'&lt;db_name&gt;'</span>)
    .collection(<span class="hljs-string">'users'</span>);

  <span class="hljs-comment">// Use the $or pipeline to fetch only those users </span>
  <span class="hljs-comment">// who've any transaction last month </span>
  <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> usersCollection.find({
    <span class="hljs-string">"$or"</span>: [
      { <span class="hljs-string">"currMonth.in"</span>: { <span class="hljs-string">"$gt"</span>: <span class="hljs-number">0</span> } },
      { <span class="hljs-string">"currMonth.out"</span>: { <span class="hljs-string">"$gt"</span>: <span class="hljs-number">0</span> } }
    ]
  }).toArray();

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`find op users length: <span class="hljs-subst">${users.length}</span>`</span>);
  <span class="hljs-keyword">const</span> bulkOps = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> users) {
    bulkOps.push({
      <span class="hljs-attr">updateOne</span>: {
        <span class="hljs-attr">filter</span>: { <span class="hljs-attr">_id</span>: user._id },
        <span class="hljs-attr">update</span>: {
          <span class="hljs-attr">$set</span>: {
            <span class="hljs-attr">updatedAt</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(),
            <span class="hljs-string">'currMonth.in'</span>: <span class="hljs-number">0</span>,
            <span class="hljs-string">'currMonth.out'</span>: <span class="hljs-number">0</span>,
          },
        },
      },
    });
  }

  <span class="hljs-keyword">if</span> (bulkOps.length) {
    <span class="hljs-keyword">await</span> usersCollection.bulkWrite(bulkOps);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'after the bulk write ops'</span>);
  }
};
</code></pre>
<p>And we're done. Every month on the first day at midnight UTC, we'll make the in &amp; out for every user 0.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations on completing this basic tutorial on using MongoDB Atlas App Services and its triggers, and creating a simple react expense tracker app with it. But don't stop here as you can improve the app further. Below are some of the shortcomings of the app which we just built</p>
<ol>
<li><p>Our app is prone to the javascript floating point math precision issues. You can use the Decimal BSON type provided by MongoDB to handle it in a better way. See <a target="_blank" href="https://www.mongodb.com/docs/manual/tutorial/model-monetary-data/">this excellent guide</a> on this issue.</p>
</li>
<li><p>Our ScheduledJob looks for and updates all the users who've made any transaction in the last month. For smaller apps this is fine, but for apps with a large number of users we can't do this from one function. Atlas App functions have a runtime limitation of 180 seconds which may not be enough to do everything</p>
</li>
<li><p>Right now the scheduled trigger fires at midnight UTC, ideally it should fire in each of the user's timezone, etc.</p>
</li>
</ol>
<p>I hope you work on solving some of these problems. If you've any questions, don't hesitate to leave a comment.</p>
<p>Thanks a lot for following along with this tutorial. I hope you found it useful and were able to gain something from it. Please check out the <a target="_blank" href="https://github.com/ra-jeev/expense-buddy">final code on GitHub</a> for your reference.</p>
<p>Keep adding the bits, only they make a BYTE. :-)</p>
]]></content:encoded></item><item><title><![CDATA[How to create a simple context menu action on Macbooks]]></title><description><![CDATA[Introduction
The backstory for this rabbit hole is not that deep. When you start your writing journey soon you get to know about some sort of text length restrictions. Sometimes it is for SEO because you want more eyeballs on your articles, organical...]]></description><link>https://rajeev.dev/how-to-create-context-menu-actions-on-macbooks</link><guid isPermaLink="true">https://rajeev.dev/how-to-create-context-menu-actions-on-macbooks</guid><category><![CDATA[automation]]></category><category><![CDATA[shell]]></category><category><![CDATA[macOS]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[General Advice]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Thu, 05 Jan 2023 18:57:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/CiUR8zISX60/upload/ea73b3d2bb4ff61e8c5589cf846153b2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>The backstory for this rabbit hole is not that deep. When you start your writing journey soon you get to know about some sort of text length restrictions. Sometimes it is for SEO because you want more eyeballs on your articles, organically. Or, maybe you're writing your Ads or Website copy. Or you're just the sort of person who needs to know the exact characters count at all times.</p>
<p>Whatever the case may be, this restriction mindset soon becomes a habit. You start selecting random texts and try to find out their length. Or, maybe not! This is just me, being my weird self.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/ZtSGStwmFYQjMKRO2H/giphy.gif">https://media.giphy.com/media/ZtSGStwmFYQjMKRO2H/giphy.gif</a></div>
<p> </p>
<p>Until now I used to copy-paste the text into my VS Code, which is always open by the way, and find out its length by selecting it. But I was looking for a better &amp; omnipresent solution (at least on my laptop). And that is how I learnt about the Automator App. You must believe me when I say this, before yesterday I hadn't heard of Automator App, even though I've had a MacBook for quite some time.</p>
<h2 id="heading-the-automator-app">The Automator App</h2>
<p>As the name implies, it allows you to create automated workflows and actions etc for your MacBook. When you launch it, using either the <code>Spotlight Search</code> or from <code>Launchpad -&gt; Other -&gt; Automator</code>, you'll see something like the below (please note that my laptop is on <code>macOS Monterey v12.6.2</code>)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672931767998/37ede9cf-82b8-4a80-a97f-a13942171758.png" alt="Automator app screen" class="image--center mx-auto" /></p>
<p>We can create a whole lot of things from here. For now, I've only explored the <code>Quick Action</code> type as that is sufficient for creating a context menu action.</p>
<h2 id="heading-creating-the-text-length-action">Creating the <code>Text Length</code> Action</h2>
<p>After selecting the <code>Quick Action</code> option you should get the below screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672932832513/97bb3037-d1c5-4b76-a022-44cacbd723da.png" alt="Create quick action screen" class="image--center mx-auto" /></p>
<p>The leftmost panel is the list of categories for which actions are available. The second panel lists the available actions, and the rightmost panel is where you create the action workflow.</p>
<p>If you look at the top of the rightmost panel, it says <code>"Workflow receives current"</code> and <code>"Automatic (Text)"</code> is already selected from the dropdown. This is exactly what we need to create our current action. If you want to create a different action then you can look at other available choices.</p>
<h3 id="heading-run-shell-script-action"><code>Run Shell Script</code> action</h3>
<p>As our workflow is already receiving the text selection, now we somehow need to calculate its length. If you go through the available actions, you'll some familiar names like <code>Run JavaScript</code> &amp; <code>Run Shell Script</code> (forgive me for not mentioning <code>Run AppleScript</code> as I didn't have the desire to explore <code>AppleScript</code>). You can also type in the search box above the second panel (the middle panel)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672933649371/12e2ac30-bff7-40a3-807a-0d7104a2ae27.png" alt="Different script options in Automator" class="image--center mx-auto" /></p>
<p>Drag and drop the Run Shell Script action to the right side panel. So this script is the second step of our workflow, the first being the automatic text selection input (notice the connection shown between these steps).</p>
<p>Now change the <code>Pass Input</code> option on the top right of the shell script to <code>"as arguments"</code> instead of the current <code>"to stdin"</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672934215838/2455d2f2-5510-4bdb-8d18-a6e7dd85595f.png" alt="shell script input as arguments" class="image--center mx-auto" /></p>
<p>Now we're ready to work on the incoming arguments (provided by the previous step). Note that you can change the type of shell from the left top <code>Shell</code> option (mine is showing <code>/bin/zsh</code>). Our script can receive multiple input arguments in some other workflows, and that's why you see the for loop which comes up automatically. For text inputs, we will receive only one input argument. So we can change the script to <code>echo ${#1} characters</code>. Essentially we're taking the first argument and finding out its length using the # operator, and then adding the "characters" suffix.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672946226482/af5afe05-5215-45bc-a523-d348fb7cae46.png" alt="script for the shell script" class="image--center mx-auto" /></p>
<p>And that is it. Our action needs only this much scripting.</p>
<h3 id="heading-testing-the-action">Testing the action</h3>
<p>Before finalizing and saving our newly created action we need to test it first. To test it while we're still in the Automator we can make use of <code>Get Specified Text</code> action. Search for "text" in the search box, and drag the <code>Get Specified Text</code> action just above our shell script. Write something in the textarea (I've written "This is me.", how original).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672936339247/34b13a20-35d0-4f89-be8c-5927c72ab586.png" alt="Adding the &quot;Get Specified Text&quot; action" class="image--center mx-auto" /></p>
<p>We're ready to test our script now. Click on the run option from the top right part of the Automator, it shows that everything is a success in the logs at the bottom of the screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672936547830/176d7f18-a71b-4ed5-81ca-a436c2c35be7.png" alt="running the test in the automator" class="image--center mx-auto" /></p>
<p>It's great that everything is green, but what was the length of "This is me."? Well, we can see the output of our shell script by clicking on the results at the bottom of the script.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672936712813/a28cbd5a-0213-46b2-8d9a-3ce44c194a37.png" alt="result of the script" class="image--center mx-auto" /></p>
<p>It seems to be giving the correct results. You can test it again by changing the input text to something else. For newlines, it adds one to the count for every newline character.</p>
<h3 id="heading-the-speak-text-action">The <code>Speak Text</code> action</h3>
<p>But where will we see the output in real situations? We need one more action to get the output of the shell script delivered to us. There are a couple of ways to do this, one being a <code>dialogbox</code> giving us the output, but the one I liked and used is the <code>"Speak Text".</code></p>
<p>When you searched for "text" in the search box, then you may have seen "Speak Text" also as one of the filtered results. Drag it to just below the Shell Script. So the output of the shell script will be the input to the <code>Speak Text</code> action.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672937511413/76b4f48c-e5e1-4375-b556-b7a61cf68250.png" alt="Adding the speak text action" class="image--center mx-auto" /></p>
<p>Run the same test one more time, and this time you should hear "11 characters" (remember my dialogue is still "This is me.", maybe it is time to change my script writer) from the speaker of your MacBook, or your headphones if they are connected to it.</p>
<h3 id="heading-saving-the-custom-action">Saving the custom action</h3>
<p>We don't need the <code>"Get Specified Text"</code> action anymore, delete it by clicking the <code>"x"</code> button on its top right. Click on <code>File -&gt; Save</code> from the top menu bar, give your action a name (I am using "Text Length") and save it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672938275543/cd51fd3f-f145-43fe-ad18-49a23c602fa0.png" alt="saving the action" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672938283994/664371cd-8057-4934-8d0c-9d9d36087290.png" alt="giving a name to the action" class="image--center mx-auto" /></p>
<h2 id="heading-text-length-in-action"><code>Text Length</code> in action</h2>
<p>After saving the action it will be available from <code>the mouse right-click -&gt; Services -&gt; Text Length.</code> In some applications where the right-click context menu is already customized by the application, say VS Code, you can get it from the <code>mac menu bar -&gt; &lt;Application_Name&gt; -&gt; Services -&gt; Text Length.</code> My created actions were saved in <code>~/Library/Services,</code> if you ever want to edit or delete them.</p>
<p>Below are some screen recordings of our custom action, in action. No prizes for figuring out the test subject of the first video :-).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/J2xE_NetwUs">https://youtu.be/J2xE_NetwUs</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/qudohPicHo4">https://youtu.be/qudohPicHo4</a></div>
<p> </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/DEs-pqtEYV8">https://youtu.be/DEs-pqtEYV8</a></div>
<p> </p>
<h3 id="heading-using-run-javascript-action-in-place-of-shell-script">Using <code>Run JavaScript</code> action in place of <code>Shell Script</code></h3>
<p>Tried to use Javascript instead of the shell script. Came to know that there are certain extra benefits provided to JavaScript (as well as AppleScript) actions. We don't need to use the <code>Speak Text</code> action if we use these actions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672943140017/3c0422ff-45f0-47ca-bb9a-99131de226ea.png" alt="using the run javascript action" class="image--center mx-auto" /></p>
<p>As you can see, I've removed both, the <code>Run Shell Script</code> &amp; <code>Speak Text</code> actions, and have instead added the <code>Run JavaScript</code> action. Do remember to select <code>text</code> from the dropdown menu for <code>Workflow receives current</code> setting at the top.</p>
<p>This is the code inside the code block</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">run</span>(<span class="hljs-params">input, parameters</span>) </span>{
    <span class="hljs-keyword">const</span> len = input[<span class="hljs-number">0</span>].length
    app = Application.currentApplication()
    app.includeStandardAdditions = <span class="hljs-literal">true</span>
    app.say(len + <span class="hljs-string">' characters'</span>)
    <span class="hljs-keyword">return</span>;
}
</code></pre>
<p><code>input</code> is the input arguments (which is a list, as there can be multiple arguments). So we are grabbing the first item from the list and getting its length. Then we use those extra bells and whistles to speak the text in the next 3 lines. Actual speaking is done by <code>app.say(len + ' characters')</code>, the first 2 lines are just enabling that.</p>
<p>Also, the <code>parameters</code> above is not your typical function parameters, it is some sort of meta-information of the function. When I tried printing it, got the following result (with some different function code which is visible below)</p>
<pre><code class="lang-javascript">{|temporary items path|:<span class="hljs-string">"/var/folders/js/7l1q79ds78d8rc3sjs53_xp80000gn/T/6764B971-C5BA-484C-BCBA-1F88B3715B83/1/com.apple.Automator.RunJavaScript"</span>, <span class="hljs-attr">source</span>:<span class="hljs-string">"function run(input, parameters) {

    // Your script goes here
    console.log('input is:', input);
    console.log(parameters);

    return parameters;
}"</span>, <span class="hljs-attr">ignoresInput</span>:<span class="hljs-literal">false</span>}
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>There are so many things around you waiting to be explored by you. You just need the drive, or the itch to go look for it. The above article is the perfect example of scratching your itch. Now that we've got the basic introduction to Automation on a MacBook, I'm sure we can create some good, time-saving workflows for ourselves.</p>
<p>Hope you enjoyed reading the article. Thanks for reading till the end. Do share your comments in the comment section.</p>
<p>Until next time. :-)</p>
]]></content:encoded></item><item><title><![CDATA[Handling programmatic "Copy to Clipboard" on DuckDuckGo Android browser]]></title><description><![CDATA[The background
This is going to be a short article. Recently I launched an in-browser daily puzzle game (GoldRoad). Later on, I added the ability to share your stats for the day on social media using the Web Share API. This is how it looks on my Andr...]]></description><link>https://rajeev.dev/programmatic-copy-to-clipboard-duckduckgo-android</link><guid isPermaLink="true">https://rajeev.dev/programmatic-copy-to-clipboard-duckduckgo-android</guid><category><![CDATA[duckduckgo]]></category><category><![CDATA[#CLIPBOARD]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Wed, 04 Jan 2023 14:42:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/96955551ec39f5fa3904edb052a0e688.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-the-background">The background</h2>
<p>This is going to be a short article. Recently I launched an in-browser daily puzzle game (<a target="_blank" href="https://goldroad.web.app">GoldRoad</a>). Later on, I added the ability to share your stats for the day on social media using the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API">Web Share API</a>. This is how it looks on my Android phone</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td></td></tr>
</thead>
<tbody>
<tr>
<td></td></tr>
</tbody>
</table>
</div><p>But sharing menu may not be available everywhere, considering this is a web app which can be opened on laptops as well. So I also added a fallback to copy the stats to the device's clipboard, feeling proud &amp; clever at the same time having thought of fallbacks and all.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/K3vVkohWnJVO8rZ5Su/giphy.gif">https://media.giphy.com/media/K3vVkohWnJVO8rZ5Su/giphy.gif</a></div>
<p> </p>
<p>And then one of my friends who plays the game religiously informs me that this share button doesn't do anything for him. Knowing him, I asked which browser are you using, and he says DuckDuckGo on Android. And believe me, all my smugness is gone now :-)</p>
<h2 id="heading-the-original-code">The original code</h2>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> shareStats = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = <span class="hljs-string">'The text to share'</span>;

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.share) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.share({
        text,
      });
    } <span class="hljs-keyword">catch</span> (error) {}
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.clipboard.writeText(text);
  }
};
</code></pre>
<p>I had used the `writeText` method of the Clipboard API thinking it has wide availability (as can be seen <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API">here</a> and in the below image) so it should work in almost any browser, and also because "The <code>"clipboard-write"</code> permission of the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API">Permissions API</a> is granted automatically to pages when they are in the active tab". There was <a target="_blank" href="https://web.dev/async-clipboard/#:~:text=the%20Clipboard%20API%20is%20only%20supported%20for%20pages%20served%20over%20HTTPS">one more caveat</a> I found, "the Clipboard API is only supported for pages served over HTTPS" (which is true anyway in my case).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672832602496/c3ab6836-ba2d-4eb6-8b4b-d407befc18c0.png" alt="clipboard api support on major browsers" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1672832467842/8ac8f490-a99b-495f-a947-e3e98cb94188.png" alt="writeText support on major browsers" class="image--center mx-auto" /></p>
<h2 id="heading-the-issues-with-duckduckgo-ddg">The issues with DuckDuckGo (DDG)</h2>
<p>After doing some debugging my investigations revealed the following</p>
<ol>
<li><p><code>Navigator.share</code> is not available on DDG</p>
</li>
<li><p><code>Navigator.clipboard.writeText</code> throws <code>NotAllowedError</code> with a message saying <code>Write permission denied</code></p>
</li>
<li><p>Since write permission was denied, so maybe somehow we could query and ask for the needed permission using the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API">Permissions API</a>? Alas! <code>Navigator.permissions</code> is not available on DDG</p>
</li>
<li><p>Tried adding the clipboardWrite permission to the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions"><code>manifest.json</code></a> file (though that is applicable for a web extension only), of course, it didn't work</p>
</li>
</ol>
<h2 id="heading-workaround-with-execcommand">Workaround with <code>execCommand</code></h2>
<p>Since all my trials with valid methods failed to yield results, I had to fall back to the <a target="_blank" href="https://web.dev/async-clipboard/#:~:text=be%20triggered%20using-,document.execCommand(%27copy%27),-and%20document.execCommand">document.execCommand</a>. As shown in the linked article, you could use the below code to copy text to the device clipboard.</p>
<pre><code class="lang-javascript">button.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> input = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'input'</span>);
  input.style.display = <span class="hljs-string">'none'</span>;
  <span class="hljs-built_in">document</span>.body.appendChild(input);
  input.value = text;
  input.focus();
  input.select();
  <span class="hljs-keyword">const</span> result = <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'copy'</span>);
  <span class="hljs-keyword">if</span> (result === <span class="hljs-string">'unsuccessful'</span>) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to copy text.'</span>);
  }
  input.remove();
});
</code></pre>
<p>But there is a twist here as well, if you set the <code>input</code> element's <code>display</code> to <code>none</code> as shown above, then execCommand returns true but doesn't copy anything to the clipboard in the case of DDG (haven't tried it with other browsers, as for everyone else Clipboard API is working fine, at least for the latest versions where I tested)</p>
<h2 id="heading-final-code">Final Code</h2>
<p>So without further ado, here is my final code which is working fine on DDG (sometimes I do see the phone's keyboard popping up for a split second, but that is fine I think).</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> shareStats = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> text = <span class="hljs-string">`The text to share\nWith multiple lines`</span>;

  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.share) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.share({
        text,
      });
    } <span class="hljs-keyword">catch</span> (error) {}

    <span class="hljs-keyword">return</span>;
  }

  <span class="hljs-keyword">await</span> copyToClipboard(text);
};

<span class="hljs-keyword">const</span> copyToClipboard = <span class="hljs-keyword">async</span> (text) =&gt; {
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.navigator.clipboard) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">window</span>.navigator.clipboard.writeText(text);
      <span class="hljs-keyword">return</span>;
    } <span class="hljs-keyword">catch</span> (error) {}
  }

  <span class="hljs-keyword">const</span> textarea = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'textarea'</span>);
  textarea.style.position = <span class="hljs-string">'fixed'</span>;
  textarea.style.width = <span class="hljs-string">'1px'</span>;
  textarea.style.height = <span class="hljs-string">'1px'</span>;
  textarea.style.padding = <span class="hljs-number">0</span>;
  textarea.style.border = <span class="hljs-string">'none'</span>;
  textarea.style.outline = <span class="hljs-string">'none'</span>;
  textarea.style.boxShadow = <span class="hljs-string">'none'</span>;
  textarea.style.background = <span class="hljs-string">'transparent'</span>;

  <span class="hljs-built_in">document</span>.body.appendChild(textarea);

  textarea.textContent = text;
  textarea.focus();
  textarea.select();

  <span class="hljs-keyword">const</span> result = <span class="hljs-built_in">document</span>.execCommand(<span class="hljs-string">'copy'</span>);
  textarea.remove();
  <span class="hljs-keyword">if</span> (!result) {
    <span class="hljs-comment">// Show some error message to the user</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Show a success message to the user mentioning the text is copied to their clipboard</span>
  }
};
</code></pre>
<p>The only thing to note above is the use of a <code>textarea</code> instead of an <code>input</code>, because if our text is multiline then the <code>input</code> will eat away all our newlines. Also, since we're not hiding the <code>textarea</code>, we are adding some styles to make it inconsequential.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p><code>execCommand</code> seems to be working at this point, but it has been made obsolete and may go away in future versions of all browsers. Just to be on the safer side, it is <strong>better to surround the execCommand call with a try-catch block</strong>. Hopefully in the future DDG will allow us to copy text using the Clipboard API itself.</p>
<p>Do let me know in the comments section if you spot an error, or if the explanation is wrong anywhere.</p>
<p>Thanks for reading :-)</p>
]]></content:encoded></item><item><title><![CDATA[Creating a Daily Puzzle Game with React, MongoDB & Google Cloud Run]]></title><description><![CDATA[About GoldRoad
GoldRoad is a daily puzzle game in a browser. Your gold is to find the best possible path between two given coins. The best possible path is the one which allows the user to collect the maximum gold. I created this game as an entry for...]]></description><link>https://rajeev.dev/creating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run</link><guid isPermaLink="true">https://rajeev.dev/creating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run</guid><category><![CDATA[MongoDB]]></category><category><![CDATA[React]]></category><category><![CDATA[#cloudrun]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Mon, 12 Dec 2022 20:20:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1670837537758/0ROzF2p1D.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-about-goldroad">About GoldRoad</h2>
<p>GoldRoad is a daily puzzle game in a browser. Your gold is to find the best possible path between two given coins. The best possible path is the one which allows the user to collect the maximum gold. I created this game as an entry for the MongoDb hackathon on <a target="_blank" href="https://dev.to/ra_jeeves/creating-a-puzzle-game-with-reactjs-mongodb-atlas-gcp-cloud-run-1995">dev.to</a></p>
<h2 id="heading-the-backstory">The backstory</h2>
<p>Some months ago I had come across <a target="_blank" href="https://twitter.com/sumul/status/1545430273113866240">one Twitter post</a> announcing a new daily puzzle game. I gave it a try and liked the game so much that I implemented the same using Python and <a target="_blank" href="https://rajeev.dev/series/python-turtle-puzzle-game">published blogs</a> documenting the process.</p>
<p>From then only, I wanted to create a similar, but different game. Recently I was watching a video explaining the greedy algorithm for finding the best path, and that instantly gelled with the Figure game in my mind. So here it is, after some back-and-forth with different approaches and customizations.</p>
<h2 id="heading-app-link-andamp-screenshots">App Link &amp; Screenshots</h2>
<p>You can play the <a target="_blank" href="https://goldroad.web.app">game here</a></p>
<h3 id="heading-the-game-page">The Game Page</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670875484124/trf0sNxo1.png" alt="The game page screenshot" class="image--center mx-auto" /></p>
<h3 id="heading-the-about-page">The About Page</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670875521033/0c9Q1_0eE.png" alt="The about page screenshot" class="image--center mx-auto" /></p>
<h2 id="heading-why-use-mongodb-atlas-andamp-app-services">Why use MongoDB Atlas &amp; App Services?</h2>
<p>I came to know about the hackathon quite late. So it would have been easier to stick to my comfort zone, and use VueJs together with Firebase (of course Firestore/RealtimeDb wasn't an option considering you're supposed to use MongoDB). But what is the point of doing that? The spirit of a hackathon is to use the given technology as much as possible (at least that is what I believe), and so that is what I did.</p>
<h2 id="heading-the-implementation">The Implementation</h2>
<p>I've divided this section into multiple subsections for ease of reading and following along.</p>
<h3 id="heading-remote-setup-creating-mongodb-cluster">Remote Setup: Creating MongoDB Cluster</h3>
<p>As soon as you create an account, it asks you to create a cluster, I selected the free shared cluster and used the below settings (You can pick the default options as well). In the additional settings, you can enable the "Termination Protection" option (Good to have it available with Free Shared Clusters also). You can give a recallable name to your cluster, or go ahead with the default Cluster0 name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670846304500/ZCq2EGyqY.png" alt="Creating MongoDB cluster" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670846422553/hOOGOHihm.png" alt="Additional cluster settings" class="image--center mx-auto" /></p>
<p>As soon as you create a cluster it creates a project for you (Project 0) and asks you to create a database user and the location from where you'll be accessing the cluster. For network access, I selected my local IP address (there is a handy button available for this).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670852165273/fmEI7i0SG.png" alt="Database user creation" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670852237723/KukJiAZ-7.png" alt="Network access option for the cluster" class="image--center mx-auto" /></p>
<p>After completing the above steps you come to your cluster dashboard. From there you can browse your databases and collections. Of course, right now we don't have any of that. When you click on the `Collections` tab just below the cluster name, you can see options to load sample data or add your own data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670852596402/zzRPo1n17.png" alt="Browse collection option" class="image--center mx-auto" /></p>
<p>Click on <code>Add My Own Data</code>, it will ask you to create a database (pick a suitable name) and name your first collection. For my game, I named my first collection <code>games</code> (ironic, isn't it :-)). I left the rest of the two checkboxes unchecked (their default state).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670852883659/RV_ZLTzO4.png" alt="Create database and collection" class="image--center mx-auto" /></p>
<p>Now there is only one step remaining, and that is to create an API Key for interacting with the newly created project. Click on the <code>Access Manager</code> menu at the top, and then click <code>Project Access</code> just above your project name (Project 0 in this case)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670856553952/Na0q2ROWP.png" alt="Access manager settings" class="image--center mx-auto" /></p>
<p>Click on the API Key tab, and create an API Key by giving the appropriate description and the project permissions. Do remember to note down the private key, as you can't retrieve it later on. Additionally, you can also restrict access to your local IP address.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670856946850/bwjJakDyZ.png" alt="API Key creation setting" class="image--center mx-auto" /></p>
<h3 id="heading-the-local-setup">The Local Setup</h3>
<p>To interact with the project from our local machine, we need to install <code>realm-cli.</code> Run the following in your terminal</p>
<p><code>npm install -g mongodb-realm-cli</code></p>
<p>Now we need to log in to the CLI using the API Key we created in the last step of the previous section.</p>
<p><code>realm-cli login --api-key="&lt;public_api_key&gt;" --private-api-key="&lt;private_api_key&gt;"</code></p>
<p>Now we are ready with our setup and it is time to start coding. Create a new react project using</p>
<p><code>yarn create react-app my-app</code></p>
<p>In my case, I moved all of the files to an inner folder called frontend. Then I created a backend folder inside the root directory and created a new MongoDB backend application using</p>
<p><code>realm-cli app create --environment development --cluster &lt;my_cluster_name&gt;</code></p>
<p>Now my project folder structure looks something like the below image (I used <code>my-app-backend</code> as my app name while creating the backend):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670859579483/rSDzifBKx.png" alt="Project folder structure" class="image--center mx-auto" /></p>
<p>If we see our app services project in the MongoDB Atlas UI it will look something like this (we could have created the project from the UI itself, and then pulled it down using the CLI).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670859840160/hRpAaf1qj.png" alt="App services project view" class="image--center mx-auto" /></p>
<h3 id="heading-creating-an-https-endpoint">Creating an HTTPS Endpoint</h3>
<p>After the local project creation, the first task at hand was to be able to create new games and store them in the DB. I needed a backend worker to execute my already tested local game generation code. This backend worker needed to support 2 approaches</p>
<ol>
<li><p>Ability to be called ad-hoc (so that I can readily create new games in case of emergencies)</p>
</li>
<li><p>Ability to be called automatically through some trigger (so that I need not call it ad-hoc :-))</p>
</li>
</ol>
<p>HTTPS Endpoints backed by functions fit the bill perfectly. I couldn't find a way to create an endpoint interactively using the <code>realm-cli</code>, so created the same using the app services UI (Go inside your application in the App Services tab, and pick HTTPS Endpoints from the left sidebar menu).</p>
<p>While creating an endpoint you can configure the Authentication for the function backing this endpoint (which is by default Application Auth). Since I will be calling this endpoint ad-hoc (using Postman and such), I had to change the authentication to <code>System</code>. This is not possible to do while creating the endpoint. We can do it afterwards by going to the backing function settings, and changing the authentication to <code>System</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670860705186/xF5FdwntV.png" alt="Creating an HTTPS endpoint - 1" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670860668844/BPEjTwOMb.png" alt="Creating an HTTPS endpoint - 2" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670860742869/DzY_13FES.png" alt="Creating an HTTPS endpoint - 3" class="image--center mx-auto" /></p>
<p>To give our endpoint a bit of protection, I changed the endpoint authorization to <code>Verify Payload Signature</code>. By doing so, if we want to make a successful call to our endpoint we need to sign our payload with a secret (which we create in the app services UI itself). Since we didn't have any function till now, I also created a new function during the process and accepted the default generated code.</p>
<p>Before testing the function we need to change the function setting and allow it to run as <code>System</code>. We can also make the function <code>private</code> if we don't want to call it from the client. For logging the function arguments etc., we can also enable the corresponding setting from the same page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670861162908/Q1Ph25V7g.png" alt="Changing function setting to run as system" class="image--center mx-auto" /></p>
<p><strong>Don't forget to</strong> <code>"Review &amp; Deploy"</code> <strong>your changes.</strong></p>
<p>Now we're ready to test this endpoint. Launch postman, configure the endpoint URL (You can get it from the HTTPS Endpoint setting we created earlier), select the correct method (POST in my case), add some dummy body, and BAM! <code>Send</code>.</p>
<p>We should get an error</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"error"</span>: <span class="hljs-string">"expected to find Endpoint-Signature in header"</span>,
    <span class="hljs-attr">"error_code"</span>: <span class="hljs-string">"InvalidParameter"</span>,
    <span class="hljs-attr">"link"</span>: <span class="hljs-string">"https://realm.mongodb.com/groups/6385ddf6b41c43346bccf9ff/..."</span>
}
</code></pre>
<p>This is expected because we had configured some auth mechanism earlier, so we need to sign our payload using the secret key we had created, and pass this in a header with the request. In postman, we can do so by adding the following code in the <code>Pre-request Script</code> tab</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> signBytes = CryptoJS.HmacSHA256(pm.request.body.raw, <span class="hljs-string">'&lt;your_secret_code&gt;'</span>);
<span class="hljs-keyword">const</span> signHex = CryptoJS.enc.Hex.stringify(signBytes);
pm.request.headers.add({
    <span class="hljs-attr">key</span>: <span class="hljs-string">"Endpoint-Signature"</span>,
    <span class="hljs-attr">value</span>: <span class="hljs-string">"sha256="</span>+signHex
});
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670862119706/J63jR8Ofa.png" alt="Pre-request Script in postman" class="image--center mx-auto" /></p>
<p>If we hit our backend again, we should get the correct response <code>"Hello World"</code> (if you didn't change anything in the default function code).</p>
<p>I struggled with this for some time, the reason being the App services UI itself. While creating the endpoint it shows how to call your endpoint using <code>curl.</code> If you look closely at the image below, it shows the header name as <code>"X-Hook-Signature".</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670862283110/QWhPyPe3a.png" alt="curl function call as shown in the UI" class="image--center mx-auto" /></p>
<p>After doing some Google searches, and also looking at the error messages in Postman (Why would I look there when the UI itself says <code>X-Hook-Signature</code>), I was able to figure out the issue. I also needed some time to figure out the "sha256=" prefix for the header value. But all's well that ends well :-)</p>
<p>Since our endpoint is working, we can replace the default function code with the below code, which generates new games, find out their solution and saves them to the games collection in our database.</p>
<p>We can do the change either in the functions UI in the browser, or we can pull down the changes to our local project (go to <code>backend -&gt; my-app-backend</code> folder in your terminal, and do <code>realm-cli pull.</code> This will pull the remote changes to your local setup). Then go to the functions folder inside the backend project, and update the code of the appropriate function file.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> ROWS = <span class="hljs-number">6</span>;
<span class="hljs-keyword">const</span> COLS = <span class="hljs-number">6</span>;

<span class="hljs-comment">// Generate a random number between min (included) &amp; max (excluded)</span>
<span class="hljs-keyword">const</span> randomInt = <span class="hljs-function">(<span class="hljs-params">min, max</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * (max - min)) + min;
};

<span class="hljs-keyword">const</span> getCoinsWithWalls = <span class="hljs-function">(<span class="hljs-params">start, end, count</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> coinColIndices = [];
  <span class="hljs-keyword">while</span> (coinColIndices.length &lt; count) {
    <span class="hljs-keyword">const</span> index = randomInt(start, end);
    <span class="hljs-keyword">if</span> (!coinColIndices.includes(index)) {
      coinColIndices.push(index);
    }
  }

  <span class="hljs-keyword">return</span> coinColIndices;
};

<span class="hljs-keyword">const</span> addJob = <span class="hljs-function">(<span class="hljs-params">jobs, src, currJob</span>) =&gt;</span> {
  jobs.push({
    <span class="hljs-attr">coins</span>: <span class="hljs-built_in">JSON</span>.parse(<span class="hljs-built_in">JSON</span>.stringify(currJob.coins)),
    src,
    <span class="hljs-attr">dst</span>: currJob.dst,
    <span class="hljs-attr">pastMoves</span>: <span class="hljs-built_in">JSON</span>.parse(<span class="hljs-built_in">JSON</span>.stringify(currJob.pastMoves)),
    <span class="hljs-attr">total</span>: currJob.total,
  });
};

<span class="hljs-keyword">const</span> handleJob = <span class="hljs-function">(<span class="hljs-params">jobs, job</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> row = job.src[<span class="hljs-number">0</span>];
  <span class="hljs-keyword">const</span> col = job.src[<span class="hljs-number">1</span>];
  <span class="hljs-keyword">const</span> srcNode = job.coins[row][col];

  srcNode.finished = <span class="hljs-literal">true</span>;
  <span class="hljs-keyword">if</span> (row === job.dst[<span class="hljs-number">0</span>] &amp;&amp; col === job.dst[<span class="hljs-number">1</span>]) {
    job.total += srcNode.value;
    job.pastMoves.push(<span class="hljs-string">`<span class="hljs-subst">${job.dst[<span class="hljs-number">0</span>]}</span><span class="hljs-subst">${job.dst[<span class="hljs-number">1</span>]}</span>`</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  }

  <span class="hljs-keyword">const</span> neighbors = {
    <span class="hljs-attr">prevNode</span>: col &gt; <span class="hljs-number">0</span> ? job.coins[row][col - <span class="hljs-number">1</span>] : <span class="hljs-literal">null</span>,
    <span class="hljs-attr">nextNode</span>: col &lt; COLS - <span class="hljs-number">1</span> ? job.coins[row][col + <span class="hljs-number">1</span>] : <span class="hljs-literal">null</span>,
    <span class="hljs-attr">topNode</span>: row &gt; <span class="hljs-number">0</span> ? job.coins[row - <span class="hljs-number">1</span>][col] : <span class="hljs-literal">null</span>,
    <span class="hljs-attr">bottomNode</span>: row &lt; ROWS - <span class="hljs-number">1</span> ? job.coins[row + <span class="hljs-number">1</span>][col] : <span class="hljs-literal">null</span>,
  };

  job.total += srcNode.value;
  job.pastMoves.push(srcNode.id);

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> key <span class="hljs-keyword">in</span> neighbors) {
    <span class="hljs-keyword">const</span> neighbor = neighbors[key];
    <span class="hljs-keyword">if</span> (neighbor &amp;&amp; !neighbor.finished) {
      <span class="hljs-keyword">if</span> (key === <span class="hljs-string">'prevNode'</span> &amp;&amp; neighbor.wall !== <span class="hljs-number">2</span> &amp;&amp; srcNode.wall !== <span class="hljs-number">4</span>) {
        addJob(jobs, [row, col - <span class="hljs-number">1</span>], job);
      }

      <span class="hljs-keyword">if</span> (key === <span class="hljs-string">'nextNode'</span> &amp;&amp; neighbor.wall !== <span class="hljs-number">4</span> &amp;&amp; srcNode.wall !== <span class="hljs-number">2</span>) {
        addJob(jobs, [row, col + <span class="hljs-number">1</span>], job);
      }

      <span class="hljs-keyword">if</span> (key === <span class="hljs-string">'topNode'</span> &amp;&amp; neighbor.wall !== <span class="hljs-number">3</span> &amp;&amp; srcNode.wall !== <span class="hljs-number">1</span>) {
        addJob(jobs, [row - <span class="hljs-number">1</span>, col], job);
      }

      <span class="hljs-keyword">if</span> (key === <span class="hljs-string">'bottomNode'</span> &amp;&amp; neighbor.wall !== <span class="hljs-number">1</span> &amp;&amp; srcNode.wall !== <span class="hljs-number">3</span>) {
        addJob(jobs, [row + <span class="hljs-number">1</span>, col], job);
      }
    }
  }

  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
};

<span class="hljs-keyword">const</span> findBestRoute = <span class="hljs-function">(<span class="hljs-params">coins, start, end</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> src = [<span class="hljs-built_in">parseInt</span>(start[<span class="hljs-number">0</span>]), <span class="hljs-built_in">parseInt</span>(start[<span class="hljs-number">1</span>])];
  <span class="hljs-keyword">const</span> dst = [<span class="hljs-built_in">parseInt</span>(end[<span class="hljs-number">0</span>]), <span class="hljs-built_in">parseInt</span>(end[<span class="hljs-number">1</span>])];

  <span class="hljs-keyword">const</span> jobs = [{ coins, src, dst, <span class="hljs-attr">pastMoves</span>: [], <span class="hljs-attr">total</span>: <span class="hljs-number">0</span> }];
  <span class="hljs-keyword">const</span> results = [];

  <span class="hljs-keyword">while</span> (jobs.length) {
    <span class="hljs-keyword">const</span> job = jobs.shift();
    <span class="hljs-keyword">if</span> (handleJob(jobs, job)) {
      results.push({
        <span class="hljs-attr">total</span>: job.total,
        <span class="hljs-attr">moves</span>: job.pastMoves.length,
        <span class="hljs-attr">path</span>: job.pastMoves,
      });
    }
  }

  <span class="hljs-keyword">if</span> (results.length) {
    results.sort(<span class="hljs-function">(<span class="hljs-params">result1, result2</span>) =&gt;</span> {
      <span class="hljs-keyword">return</span> result2.total - result1.total;
    });

    <span class="hljs-keyword">return</span> results[<span class="hljs-number">0</span>];
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`No valid path found`</span>);
  }
};

<span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">req</span>) </span>{
  <span class="hljs-keyword">let</span> reqBody = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">if</span> (req) {
    <span class="hljs-keyword">if</span> (req.body) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`got a req body: <span class="hljs-subst">${req.body.text()}</span>`</span>);
      reqBody = <span class="hljs-built_in">JSON</span>.parse(req.body.text());
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`got a req without req body: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(req)}</span>`</span>);
    }
  }

  <span class="hljs-keyword">const</span> coins = [];
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> row = <span class="hljs-number">0</span>; row &lt; ROWS; row++) {
    coins.push([]);

    <span class="hljs-keyword">const</span> blockages = getCoinsWithWalls(<span class="hljs-number">0</span>, COLS, <span class="hljs-number">2</span>);
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> col = <span class="hljs-number">0</span>; col &lt; COLS; col++) {
      <span class="hljs-keyword">const</span> coin = {
        <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${row}</span><span class="hljs-subst">${col}</span>`</span>,
        <span class="hljs-attr">value</span>: randomInt(<span class="hljs-number">1</span>, <span class="hljs-number">7</span>),
        <span class="hljs-attr">wall</span>: <span class="hljs-number">0</span>,
      };

      <span class="hljs-keyword">if</span> (blockages.includes(col)) {
        coin.wall = randomInt(<span class="hljs-number">1</span>, <span class="hljs-number">5</span>);
      }

      coins[row].push(coin);
    }
  }

  <span class="hljs-keyword">const</span> start = <span class="hljs-string">`<span class="hljs-subst">${randomInt(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>)}</span><span class="hljs-subst">${randomInt(<span class="hljs-number">2</span>, <span class="hljs-number">4</span>)}</span>`</span>;
  <span class="hljs-keyword">let</span> end = randomInt(<span class="hljs-number">1</span>, <span class="hljs-number">5</span>);
  <span class="hljs-keyword">if</span> (end === <span class="hljs-number">1</span>) {
    end = <span class="hljs-string">'00'</span>;
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (end === <span class="hljs-number">2</span>) {
    end = <span class="hljs-string">`0<span class="hljs-subst">${COLS - <span class="hljs-number">1</span>}</span>`</span>;
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (end === <span class="hljs-number">3</span>) {
    end = <span class="hljs-string">`<span class="hljs-subst">${ROWS - <span class="hljs-number">1</span>}</span>0`</span>;
  } <span class="hljs-keyword">else</span> {
    end = <span class="hljs-string">`<span class="hljs-subst">${ROWS - <span class="hljs-number">1</span>}</span><span class="hljs-subst">${COLS - <span class="hljs-number">1</span>}</span>`</span>;
  }

  <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
  <span class="hljs-keyword">const</span> gameEntry = {
    coins,
    start,
    end,
    <span class="hljs-attr">active</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">createdAt</span>: date,
    <span class="hljs-attr">updatedAt</span>: date,
  };

  <span class="hljs-keyword">const</span> startTime = <span class="hljs-built_in">Date</span>.now();
  <span class="hljs-keyword">const</span> bestMove = findBestRoute(<span class="hljs-built_in">JSON</span>.parse(<span class="hljs-built_in">JSON</span>.stringify(coins)), start, end);
  <span class="hljs-built_in">console</span>.log(
    <span class="hljs-string">`Total time taken for finding bestRoute: <span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now() - startTime}</span> ms`</span>
  );

  <span class="hljs-keyword">if</span> (bestMove) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`best path: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(bestMove)}</span>`</span>);
    gameEntry.maxScore = bestMove.total;
    gameEntry.maxScoreMoves = bestMove.moves;
    gameEntry.hints = bestMove.path;

    <span class="hljs-keyword">const</span> mongoDb = context.services.get(<span class="hljs-string">'cluster-service-name'</span>).db(<span class="hljs-string">'db-name'</span>);
    <span class="hljs-keyword">const</span> gamesCollection = mongoDb.collection(<span class="hljs-string">'games'</span>);
    <span class="hljs-keyword">const</span> appCollection = mongoDb.collection(<span class="hljs-string">'app'</span>);
    <span class="hljs-keyword">const</span> config = <span class="hljs-keyword">await</span> appCollection.findOne({ <span class="hljs-attr">type</span>: <span class="hljs-string">'config'</span> });

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'fetch config data:'</span>, <span class="hljs-built_in">JSON</span>.stringify(config));
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'lastPlayableGame:'</span>, config.lastPlayableGame);

    <span class="hljs-keyword">if</span> (config) {
      <span class="hljs-keyword">if</span> (config.lastPlayableGame) {
        <span class="hljs-keyword">const</span> lastPlayableDate = config.lastPlayableGame.playableAt;
        lastPlayableDate.setUTCDate(lastPlayableDate.getDate() + <span class="hljs-number">1</span>);
        gameEntry.playableAt = lastPlayableDate;
        gameEntry.gameNo = config.lastPlayableGame.gameNo + <span class="hljs-number">1</span>;
        <span class="hljs-keyword">if</span> (reqBody) {
          <span class="hljs-keyword">if</span> (reqBody.active) {
            gameEntry.active = <span class="hljs-literal">true</span>;
          }

          <span class="hljs-keyword">if</span> (reqBody.current) {
            gameEntry.current = <span class="hljs-literal">true</span>;
          }
        }
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> playableDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
        playableDate.setUTCHours(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
        gameEntry.playableAt = playableDate;
        gameEntry.gameNo = <span class="hljs-number">1</span>;
        gameEntry.current = <span class="hljs-literal">true</span>;
        gameEntry.active = <span class="hljs-literal">true</span>;
      }
    }

    <span class="hljs-keyword">let</span> result = <span class="hljs-keyword">await</span> gamesCollection.insertOne(gameEntry);
    <span class="hljs-built_in">console</span>.log(
      <span class="hljs-string">`Successfully inserted game with _id: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(result)}</span>`</span>
    );

    result = <span class="hljs-keyword">await</span> appCollection.updateOne(
      { <span class="hljs-attr">type</span>: <span class="hljs-string">'config'</span> },
      {
        <span class="hljs-attr">$set</span>: {
          <span class="hljs-attr">lastPlayableGame</span>: {
            <span class="hljs-attr">playableAt</span>: gameEntry.playableAt,
            <span class="hljs-attr">gameNo</span>: gameEntry.gameNo,
            <span class="hljs-attr">_id</span>: result.insertedId,
          },
        },
      }
    );

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'result of update operation: '</span>, <span class="hljs-built_in">JSON</span>.stringify(result));
  }

  <span class="hljs-keyword">return</span> gameEntry;
};
</code></pre>
<p>Behind the scenes, I also created another collection called "app" the purpose of which is to store the app-specific settings and information. Since the newly created game will be played sometime in future, and there will be some extra games stored in the games collection, I am utilizing the app collection to make note of the last created game.</p>
<p>Also notice that we are saving the new game only if it has a solution (there might be cases where the game has no solution, as everything is getting generated randomly).</p>
<p>If you did the above changes in your local setup, then you need to run <code>realm-cli push</code> in your terminal before you can call your endpoint.</p>
<p>Now, if you call your endpoint from postman, you should get back the game object as a reply. And also, there should be a new document present in your games collection of the database.</p>
<h3 id="heading-some-gotchas-with-app-services-functions">Some gotchas with App Services Functions</h3>
<p>In the above function also, I struggled for quite some time due to 2 issues.</p>
<ol>
<li><p>Functions documentation says that it supports <code>crypto</code> module partially. I was using <code>crypto.randomInt</code> to do my random number generation. But the function was failing giving unhelpful error messages (<code>TypeError: Value is not an object: undefined</code>). After a lot of trial and error, ultimately I found out that the<code>randomInt</code> method is unavailable in the app services crypto, so I created a new function using <code>Math.random</code> to generate random numbers. It would be great to have some meaningful error messages in such cases.</p>
</li>
<li><p>I was using a <code>Set</code> to keep track of generated walls (<code>blockages</code>). But on checking <code>if (blockages.has(col))</code> I was getting <code>false</code> for all columns except <code>0</code>. Again this needed some testing and figuring out. Finally replaced <code>Set</code> with a <code>list</code> (as is visible in the code above). Testing with a "set" in my local node setup works perfectly fine, so maybe it has something to do with how app services functions have been implemented behind the scenes, or altogether there is some other issue I can't say.</p>
<p> <strong>Update 1:</strong> So I couldn't stop thinking about the issue, and raised it in the official MongoDB Developer Community Forum. Heard back from them with the below response:</p>
<blockquote>
<p>Thank you for raising this: we could reproduce the behaviour, and indeed it looks like <code>Set.has(…)</code> isn’t returning the expected results.</p>
<p>We’re opening an internal ticket about the matter, and will keep this post updated.</p>
</blockquote>
<p> <strong>Update 2 (Final Update): 14 Dec 2022:</strong> Received a new reply from the mongo team that they've identified the bug, and it will be solved in due course (no timeframe). This concludes the mystery around the issue :-)</p>
<blockquote>
<p><em>The Team responsible of the underlying function engine has confirmed that the error is due to mishandling of integer values. Set.has(…) should still work for other types, though.</em></p>
<p>(To clarify: the problem is that the addition of two integers is returning a float, that Set.has() doesn’t compare properly)</p>
</blockquote>
<p> Do notice that we're doing addition while generating a random number <code>Math.floor(Math.random() * (max - min)) + min;</code></p>
</li>
</ol>
<h3 id="heading-data-access-rules">Data Access Rules</h3>
<p>To get data using the realm Web SDK (which I added to the react app), we need to configure the data access rules for each of the collections we create. We can do so easily using the App Services UI (or the JSON files in the local setup, but that takes some time to get familiar with).</p>
<p>We can set these rules from App Services UI easily. There are some preset handy rules which we can use, or we can create our own rules from scratch.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670872474993/Mdhc6quQ4.png" alt="Data access rules" class="image--center mx-auto" /></p>
<h3 id="heading-scheduled-triggers-and-backing-function">Scheduled Triggers and Backing Function</h3>
<p>Since our puzzles should change every day, we need a trigger to do so automatically. App services provide many types of triggers, for my purpose I needed a simple CRON schedule which runs every day at 12:00 AM UTC. Used the advanced schedule type from the UI, and set the schedule to <code>0 0 * * *</code> and let it call another function to change the game.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670872813744/-vb70TVwf.png" alt="Scheduled trigger setting" class="image--center mx-auto" /></p>
<p>Below is the code of the function which makes the current game non-current, and makes the next game in line (recognized by the value of <code>gameNo</code> field of the document).</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> mongoDb = context.services.get(<span class="hljs-string">'cluster-service-nam'</span>).db(<span class="hljs-string">'db-name'</span>);
  <span class="hljs-keyword">const</span> gamesCollection = mongoDb.collection(<span class="hljs-string">'games'</span>);
  <span class="hljs-keyword">const</span> currGame = <span class="hljs-keyword">await</span> gamesCollection.findOne({ <span class="hljs-attr">current</span>: <span class="hljs-literal">true</span> });
  <span class="hljs-keyword">if</span> (currGame) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'got the current game: '</span>, <span class="hljs-built_in">JSON</span>.stringify(currGame));
    <span class="hljs-keyword">const</span> date = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
    <span class="hljs-keyword">const</span> nextGameDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>();
    nextGameDate.setUTCHours(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
    nextGameDate.setUTCDate(nextGameDate.getDate() + <span class="hljs-number">1</span>);
    <span class="hljs-keyword">await</span> gamesCollection.bulkWrite(
      [
        {
          <span class="hljs-attr">updateOne</span>: {
            <span class="hljs-attr">filter</span>: { <span class="hljs-attr">gameNo</span>: currGame.gameNo + <span class="hljs-number">1</span> },
            <span class="hljs-attr">update</span>: {
              <span class="hljs-attr">$set</span>: {
                <span class="hljs-attr">current</span>: <span class="hljs-literal">true</span>,
                <span class="hljs-attr">active</span>: <span class="hljs-literal">true</span>,
                <span class="hljs-attr">updatedAt</span>: date,
                <span class="hljs-attr">playedAt</span>: date,
                <span class="hljs-attr">nextGameAt</span>: nextGameDate,
              },
            },
          },
        },
        {
          <span class="hljs-attr">updateOne</span>: {
            <span class="hljs-attr">filter</span>: { <span class="hljs-attr">_id</span>: currGame._id },
            <span class="hljs-attr">update</span>: { <span class="hljs-attr">$set</span>: { <span class="hljs-attr">current</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">updatedAt</span>: date } },
          },
        },
      ],
      { <span class="hljs-attr">ordered</span>: <span class="hljs-literal">true</span> }
    );

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'after the bulkWrite Op'</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Error! No current game found.'</span>);
  }
};
</code></pre>
<h3 id="heading-database-trigger-and-backing-function">Database Trigger and Backing Function</h3>
<p>Since we're consuming the stored game documents every day automatically (using the implementation above), we also need to find a way to generate games the same way. This can be done using database triggers. Whenever we mark one of the games as the current game, we can listen for the database trigger and generate a new game by calling our old function (which we created in the beginning).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670873410385/8ian9-Sc-.png" alt="Creating database trigger 1" class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670873450276/a5yo3-zAF.png" alt="Creating database trigger 2" class="image--center mx-auto" /></p>
<p>Selected <code>Operation Type as Update</code>. Since on game refresh, 2 documents are updated (one the current game, and the other the next game), so used a match expression (Advanced Optional setting) to only trigger the function for the next game document. We're updating the <code>playedAt</code> field as well on game refresh so I used that in the match expression.</p>
<pre><code class="lang-javascript">{<span class="hljs-string">"updateDescription.updatedFields.playedAt"</span>:{<span class="hljs-string">"$exists"</span>:<span class="hljs-literal">true</span>}}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1670873486380/dMu9n6eNu.png" alt="Creating database trigger 3" class="image--center mx-auto" /></p>
<p>Somehow the match expression with a boolean field (<code>current</code>) was not working.</p>
<pre><code class="lang-javascript">{<span class="hljs-string">"updateDescription.updatedFields"</span>:{<span class="hljs-string">"current"</span>:<span class="hljs-literal">true</span>}}
</code></pre>
<p>I didn't try with the dot notation, maybe that would've worked.</p>
<pre><code class="lang-javascript">{<span class="hljs-string">"updateDescription.updatedFields.current"</span>: <span class="hljs-literal">true</span>}
</code></pre>
<p>We could have created a new game in the scheduled trigger itself (where we refresh our daily puzzle), but there is an upper limit of <code>150 seconds</code> in the app services on any function run time. So database trigger made more sense as it will provide some extra buffer time to my game generation function.</p>
<h3 id="heading-anonymous-authentication-and-auth-trigger">Anonymous Authentication and Auth Trigger</h3>
<p>We also want to store the users' play history. So enabled anonymous auth the same from the UI. Also added an auth trigger so that we can save the newly created users to the DB.</p>
<p>Auth Trigger function code which gets trigged on new user creation.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span> = <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">authEvent</span>) </span>{
  <span class="hljs-keyword">const</span> { user, time } = authEvent;

  <span class="hljs-keyword">const</span> mongoDb = context.services.get(<span class="hljs-string">'cluster-service-name'</span>).db(<span class="hljs-string">'db-name'</span>);
  <span class="hljs-keyword">const</span> usersCollection = mongoDb.collection(<span class="hljs-string">'users'</span>);
  <span class="hljs-keyword">const</span> userData = { <span class="hljs-attr">_id</span>: user.id, ...user, <span class="hljs-attr">createdAt</span>: time, <span class="hljs-attr">updatedAt</span>: time };
  userData.data = {
    <span class="hljs-attr">currStreak</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">longestStreak</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">isCurrLongestStreak</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">solves</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">played</span>: <span class="hljs-number">0</span>,
  };

  <span class="hljs-keyword">delete</span> userData.id;
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> usersCollection.insertOne(userData);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'result of user insert op: '</span>, <span class="hljs-built_in">JSON</span>.stringify(res));
};
</code></pre>
<p>Didn't use any other type of auth provider as I don't want the players to worry about login at this point.</p>
<h3 id="heading-frontend-hosting-with-google-cloud-run">Frontend Hosting with Google Cloud Run</h3>
<p>The frontend is a basic React app which uses the Realm-SDK to interact with the backend which we just created. You can go through the code in the shared GitHub repo.</p>
<p>I wanted to try out App Services hosting also, but apparently, that is only available for paid accounts. Google Cloud to the rescue. Utilized Google Cloud Run to host the application frontend.</p>
<p>To do this we need to create a <code>Dockerfile</code> with the below content in the frontend root folder (wherever the frontend package.json is)</p>
<pre><code class="lang-javascript">FROM node:lts-alpine <span class="hljs-keyword">as</span> react-build
WORKDIR /app
COPY . ./
RUN yarn
RUN yarn build

# server environment
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/configfile.template

COPY --<span class="hljs-keyword">from</span>=react-build /app/build /usr/share/nginx/html

ENV PORT <span class="hljs-number">8080</span>
ENV HOST <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span>
EXPOSE <span class="hljs-number">8080</span>
CMD sh -c <span class="hljs-string">"envsubst '\$PORT' &lt; /etc/nginx/conf.d/configfile.template &gt; /etc/nginx/conf.d/default.conf &amp;&amp; nginx -g 'daemon off;'"</span>
</code></pre>
<p>We also need to create an <code>nginx.conf</code> file in the frontend root folder</p>
<pre><code class="lang-javascript">server {
     listen       $PORT;
     server_name  localhost;

     location / {
         root   /usr/share/nginx/html;
         index  index.html index.htm;
         try_files $uri /index.html;
     }

     gzip on;
     gzip_vary on;
     gzip_min_length <span class="hljs-number">10240</span>;
     gzip_proxied expired no-cache no-store private auth;
     gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml;
     gzip_disable <span class="hljs-string">"MSIE [1-6]\."</span>;
}
</code></pre>
<p>Now we can build the project in a docker container using the below command (Do remember to create a Google Cloud project, and enable Cloud Run API, Google Container Registry API &amp; Cloud Build API for that project)</p>
<pre><code class="lang-javascript">gcloud builds submit --tag gcr.io/<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">google_project_id</span>&gt;</span>/app</span>
</code></pre>
<p>Once the build is successful, we can deploy the application by running</p>
<pre><code class="lang-javascript">gcloud run deploy --image gcr.io/<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">project_id</span>&gt;</span>/app --platform managed</span>
</code></pre>
<p>And voila, we can visit our frontend by going to the service URL as mentioned in the console.</p>
<h2 id="heading-further-enhancements">Further enhancements</h2>
<ol>
<li><p>Frontend code for the gameplay needs some refactoring</p>
</li>
<li><p>Game stats and analytics needs to be added</p>
</li>
<li><p>The current user's game history is getting stored in DB (needs further testing) but it is not getting displayed anywhere. Need to create another app route for the same</p>
</li>
<li><p>Caching has not been used anywhere. Maybe we can utilize Firebase hosting for application caching, as well as for caching the current game data</p>
</li>
<li><p>For user gameplay-related interactions with the DB, need to use change streams/watch for real-time updates.</p>
</li>
<li><p>Notify the user if a new game is available while they're using the app</p>
</li>
</ol>
<h2 id="heading-github-link">GitHub link</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/ra-jeev/goldroad">https://github.com/ra-jeev/goldroad</a></div>
<p> </p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Overall it was a very good experience building the game with MongoDB Atlas &amp; App Services. We can have improved docs, but I am happy to have utilized this time and gotten familiar with Atlas &amp; App Services.</p>
<p>Hope you enjoyed reading the article and will also enjoy playing <a target="_blank" href="https://goldroad.web.app">the game</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Using Python multiprocessing to optimize a problem]]></title><description><![CDATA[Introduction
This is the final part of the Python puzzle game series. In the first part we learnt to create a tiles puzzle game using Turtle module. And then in the second part created an algorithm to find a solution of the puzzle. But the algorithm ...]]></description><link>https://rajeev.dev/using-python-multiprocessing-to-optimize-puzzle-solving</link><guid isPermaLink="true">https://rajeev.dev/using-python-multiprocessing-to-optimize-puzzle-solving</guid><category><![CDATA[Python]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[multiprocessing]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Mon, 21 Nov 2022 12:48:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669189224770/iAsHu7b-l.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>This is the final part of the Python puzzle game series. <a target="_blank" href="https://rajeev.dev/creating-puzzle-game-using-python-turtle-1">In the first part</a> we learnt to create a tiles puzzle game using Turtle module. And then <a target="_blank" href="https://rajeev.dev/solve-puzzle-game-using-python">in the second part</a> created an algorithm to find a solution of the puzzle. But the algorithm can be optimized further, and in this article we will look at some of the ways we can achieve that.</p>
<p>To make sense of this article, I suggest that first you go through the previous posts in this series (if you haven't already done so).</p>
<h2 id="heading-the-need-for-further-optimization">The need for further optimization</h2>
<p>Let's run the final code from the previous post on a different puzzle which requires more number of moves.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1669032374668/RMHO7pz6T.png" alt="A different puzzle" /></p>
<pre><code class="lang-python">play([
    [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'turquoise'</span>],
    [<span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>],
    [<span class="hljs-string">'white'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'yellow'</span>],
    [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>],
    [<span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'yellow'</span>]
])
</code></pre>
<p>These are the results</p>
<pre><code>start time: <span class="hljs-number">0.113825565</span>
changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">4000001111223334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.12959095</span>
changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">400000114313122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.135476449</span>
changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">40000432310211</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.483449261</span>
changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">4433100000122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">49.005072394</span>
No more jobs: final count: <span class="hljs-number">2012130</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">324.622757167</span>
result <span class="hljs-keyword">of</span> operation: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'4433100000122'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">2012130</span>}
</code></pre><p>So even with our previous optimization, we ran close to 2 million end to end sequences, and it took us around 5 minutes and 25 seconds to finish the job. Do remember that we're only looking for one valid solution, and not trying to find all possible sequences of the same length (and believe me, there will be many such sequences).</p>
<p>So, can we do something more? </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/ia3zQtyPYYhcpznFJ2/giphy.gif">https://media.giphy.com/media/ia3zQtyPYYhcpznFJ2/giphy.gif</a></div>
<p>Well, what if we could run some of these jobs in parallel? That should save us some time, don't you think? Let's find out.</p>
<h3 id="heading-modified-play-andamp-findminmoves-functions">Modified <code>play</code> &amp; <code>find_min_moves</code> functions</h3>
<p>We will be using <code>multiprocessing</code> module to run multiple processes in parallel to see if it can make a difference. The reason for going with multiple processes instead of multiple threads is simple, our code is pure compute problem with no IO related tasks. Multiple threads make sense where there are some wait times involved (as in the case of IO tasks, like downloading something from internet etc).</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> timeit <span class="hljs-keyword">import</span> default_timer <span class="hljs-keyword">as</span> timer
<span class="hljs-keyword">import</span> copy
<span class="hljs-keyword">import</span> multiprocessing <span class="hljs-keyword">as</span> mp
<span class="hljs-keyword">import</span> os

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


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_min_moves</span>(<span class="hljs-params">task</span>):</span>
    <span class="hljs-comment"># Create an internal job list for this process and add the incoming task to it</span>
    jobs = [task]
    pid = os.getpid()
    result = {<span class="hljs-string">'min_moves'</span>: <span class="hljs-literal">None</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'pid'</span>: pid}

    print(
        <span class="hljs-string">f'entered pid: <span class="hljs-subst">{pid}</span>: for moves: <span class="hljs-subst">{task[<span class="hljs-string">"curr_move"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># See if we've any jobs left. If there is no job, break the loop</span>
        job = jobs.pop() <span class="hljs-keyword">if</span> len(jobs) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>
        <span class="hljs-keyword">if</span> job <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
            print(
                <span class="hljs-string">f'No more jobs: pid: <span class="hljs-subst">{pid}</span>, final count: <span class="hljs-subst">{result[<span class="hljs-string">"count"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Handle the current job. This will take of the combinations till its logical</span>
        <span class="hljs-comment"># end (until the board is clear). Other encountered combinations will be added</span>
        <span class="hljs-comment"># to the job list for processing in due course</span>
        final_moves_seq = AutoPlay.handle_job_recurse(
            job, jobs, result[<span class="hljs-string">'min_moves_len'</span>])

        result[<span class="hljs-string">'count'</span>] += <span class="hljs-number">1</span>

        <span class="hljs-comment"># If the one processed combination has minimum length, then that is the minimum</span>
        <span class="hljs-comment"># numbers of moves needed to solve the puzzle</span>
        <span class="hljs-keyword">if</span> result[<span class="hljs-string">'min_moves_len'</span>] == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> (final_moves_seq <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>
                                            <span class="hljs-keyword">and</span> len(final_moves_seq) &lt; result[<span class="hljs-string">'min_moves_len'</span>]):
            result[<span class="hljs-string">'min_moves'</span>] = final_moves_seq
            result[<span class="hljs-string">'min_moves_len'</span>] = len(final_moves_seq)
            print(
                <span class="hljs-string">f'pid: <span class="hljs-subst">{pid}</span>, changed min_moves to: <span class="hljs-subst">{result[<span class="hljs-string">"min_moves_len"</span>]}</span>, <span class="hljs-subst">{final_moves_seq}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">return</span> result


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">colors</span>):</span>
    <span class="hljs-comment"># Single game object which holds the tiles in play,</span>
    <span class="hljs-comment"># and the current connections groups and clickables</span>
    game = {
        <span class="hljs-string">'tiles'</span>: [],
        <span class="hljs-string">'clickables'</span>: [],
        <span class="hljs-string">'connection_groups'</span>: []
    }

    <span class="hljs-comment"># Set the board as per the input colors</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        game[<span class="hljs-string">'tiles'</span>].append([])
        <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(AutoPlay.MAX_ROWS):
            tile = {<span class="hljs-string">'id'</span>: (row, col), <span class="hljs-string">"connections"</span>: [],
                    <span class="hljs-string">"clickable"</span>: <span class="hljs-literal">False</span>, <span class="hljs-string">"color"</span>: colors[col][row]}
            game[<span class="hljs-string">'tiles'</span>][col].append(tile)

    <span class="hljs-comment"># Go through the tiles and find out the connections</span>
    <span class="hljs-comment"># between them, and also save the clickables</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        AutoPlay.process_tile(game, <span class="hljs-number">0</span>, col)

    start = timer()
    print(<span class="hljs-string">f'start time: <span class="hljs-subst">{start}</span>'</span>)

    <span class="hljs-comment"># Create as many tasks as there are connection groups.</span>
    <span class="hljs-comment"># We're using deepcopy to create a deeply cloned game</span>
    <span class="hljs-comment"># object for each task. The current move is the first</span>
    <span class="hljs-comment"># entry of every connection group (the lowest column</span>
    <span class="hljs-comment"># index in the bottom row)</span>
    tasks = []
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
        g = copy.deepcopy(game)
        tasks.append(
            {<span class="hljs-string">'game'</span>: g, <span class="hljs-string">'curr_move'</span>: connections[<span class="hljs-number">0</span>], <span class="hljs-string">'past_moves'</span>: <span class="hljs-literal">None</span>})

    <span class="hljs-comment"># Get a managed pool from multiprocessing, and distribute the tasks to these</span>
    <span class="hljs-comment"># pools. By default it will create processes equal to the number returned by</span>
    <span class="hljs-comment"># os.cpu_count().</span>
    <span class="hljs-keyword">with</span> mp.Pool() <span class="hljs-keyword">as</span> pool:
        results = pool.map(find_min_moves, tasks)
        print(<span class="hljs-string">'got results:'</span>, timer())
        <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:
            print(<span class="hljs-string">'result:'</span>, result)
</code></pre>
<pre><code class="lang-python"><span class="hljs-keyword">with</span> mp.Pool() <span class="hljs-keyword">as</span> pool:
</code></pre>
<p>gives us a managed pool which cleans after itself. </p>
<pre><code class="lang-python">results = pool.map(find_min_moves, tasks)
</code></pre>
<p>is a blocking function which takes care of distributing the tasks to the processes from the pool. For every process, <code>find_min_moves</code> function is called with one task from the <code>tasks</code> list.</p>
<h3 id="heading-the-result">The result</h3>
<p>If we try to call our <code>play</code> function now, we will get the below <code>RuntimeError</code> (on Windows and macOS).</p>
<pre><code>An attempt has been made to start a <span class="hljs-keyword">new</span> process before the
current process has finished its bootstrapping phase.

This probably means that you are not using fork to start your
child processes and you have forgotten to use the proper idiom
<span class="hljs-keyword">in</span> the main <span class="hljs-built_in">module</span>:

    <span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
        freeze_support()
        ...

The <span class="hljs-string">"freeze_support()"</span> line can be omitted <span class="hljs-keyword">if</span> the program
is not going to be frozen to produce an executable.
</code></pre><p>As the error is saying, we need to protect the entry point of our program, else it will enter into an endless loop while spawning child processes. Let's modify the calling part</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    play([
        [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'turquoise'</span>],
        [<span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>],
        [<span class="hljs-string">'white'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'yellow'</span>],
        [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>],
        [<span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'yellow'</span>]
    ])
</code></pre>
<p>And here are the results of the execution</p>
<pre><code>start time: <span class="hljs-number">0.05643949</span>
entered pid: <span class="hljs-number">39066</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.120805656</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39066</span>, changed min_moves to: <span class="hljs-number">18</span>, <span class="hljs-number">000001111223334444</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.128779476</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39066</span>, changed min_moves to: <span class="hljs-number">17</span>, <span class="hljs-number">00000111122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.130322022</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39066</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">0000011112423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.131969189</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39066</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">000001144313122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.209503056</span>
entered pid: <span class="hljs-number">39067</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.193182385</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39067</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">1000001223334444</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.203432333</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39067</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">100000122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.206294855</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39067</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">10000012423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.209911962</span>
entered pid: <span class="hljs-number">39068</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">3</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.2225178</span>
entered pid: <span class="hljs-number">39069</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">4</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.22732867</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">18</span>, <span class="hljs-number">300000111122334444</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.254337267</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">17</span>, <span class="hljs-number">30000011112244334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.268903578</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39069</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">4000001111223334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.272185431</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39069</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">400000114313122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.278042101</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">3000001114431224</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.337434568</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">300000114131224</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.351929327</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39069</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">40000432310211</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">1.06737366</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">30000423104211</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">1.918205703</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39067</span>, changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">1004430003122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">2.159644669</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39066</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">00004432310211</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">4.574963226</span>
No more jobs: pid: <span class="hljs-number">39067</span>, final count: <span class="hljs-number">171415</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">52.145841877</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39069</span>, changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">4433100000122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">96.061340938</span>
No more jobs: pid: <span class="hljs-number">39069</span>, final count: <span class="hljs-number">418533</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">115.766546083</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39068</span>, changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">3431000001224</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">217.433144061</span>
No more jobs: pid: <span class="hljs-number">39068</span>, final count: <span class="hljs-number">1166097</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">256.900814533</span>
No more jobs: pid: <span class="hljs-number">39066</span>, final count: <span class="hljs-number">2631991</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">479.720000441</span>
got results: <span class="hljs-number">479.826363948</span>
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'00004432310211'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">14</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">2631991</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39066</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'1004430003122'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">171415</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39067</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'3431000001224'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">1166097</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39068</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'4433100000122'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">418533</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39069</span>}
</code></pre><p><strong>Wow! This is bad...</strong> We took close to 8 minutes, to finish executing the program, and did almost 4.4 million end to end sequences. Please notice that the program was running in parallel for sometime with process ids 39066-39069. We found 3 different sequences all giving us 13 as the minimum number of moves. </p>
<p>So what went wrong as compared to the previous case?</p>
<p>The thing is, in the case of a single process, the <code>min_moves</code> variable was same for all the executions, but here that is not the case. We had different variables (<code>result["min_moves_len"]</code>) for different starting moves. So, even though the code ran in parallel, individually every process did more end-to-end sequences because of the forced silos.</p>
<p><strong>How do we fix this? </strong></p>
<p>We somehow need to have a common <code>min_moves</code> variable between these processes. We can't simply use a global variable for the same as different processes have their own memory space. We need to take help from multiprocessing module itself for achieving data sharing.</p>
<h2 id="heading-the-final-optimization">The final optimization</h2>
<p>We will use a synchronized shared object <code>Value()</code> for storing the min_moves_len, and then we will use it in our processes to make a decision. Since knowing the current min moves length in our processes is sufficient for us, and so, using <code>Value</code> makes more sense (for storing a number, and it is faster too) as compared to other ways of sharing data between processes.</p>
<h3 id="heading-modified-play-and-findminmoves-functions">Modified <code>play</code> and <code>find_min_moves</code> functions</h3>
<p>We are using another function called <code>init_globals</code> to initialize each process with a <code>min_moves</code> global variable for that process. We need to pass this function as initializer while creating the processes pool. We also create one <code>Value()</code> object (<code>min_val = mp.Value('i', 0)</code>, where 'i' denotes a signed integer), and pass that to the initializer function as <code>initargs</code>.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init_globals</span>(<span class="hljs-params">min_val</span>):</span>
    <span class="hljs-keyword">global</span> min_moves
    min_moves = min_val

<span class="hljs-comment"># To use the min_moves variable we just need to use its `value` attribute</span>
<span class="hljs-comment"># like, min_moves.value</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_min_moves</span>(<span class="hljs-params">task</span>):</span>
    <span class="hljs-comment"># Create an interal job list for this process and add the incoming task to it</span>
    jobs = [task]
    pid = os.getpid()
    result = {<span class="hljs-string">'min_moves'</span>: <span class="hljs-literal">None</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'pid'</span>: pid}

    print(
        <span class="hljs-string">f'entered pid: <span class="hljs-subst">{pid}</span>: for moves: <span class="hljs-subst">{task[<span class="hljs-string">"curr_move"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># See if we've any jobs left. If there is no job, break the loop</span>
        job = jobs.pop() <span class="hljs-keyword">if</span> len(jobs) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>
        <span class="hljs-keyword">if</span> job <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
            print(
                <span class="hljs-string">f'No more jobs: pid: <span class="hljs-subst">{pid}</span>, final count: <span class="hljs-subst">{result[<span class="hljs-string">"count"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Handle the current job. This will take of the combinations till its logical</span>
        <span class="hljs-comment"># end (until the board is clear). Other encountered combinations will be added</span>
        <span class="hljs-comment"># to the job list for processing in due course</span>
        final_moves_seq = AutoPlay.handle_job_recurse(
            job, jobs, min_moves.value)

        result[<span class="hljs-string">'count'</span>] += <span class="hljs-number">1</span>

        <span class="hljs-comment"># If the one processed combination has minimum length, then that is the minimum</span>
        <span class="hljs-comment"># numbers of moves needed to solve the puzzle</span>
        <span class="hljs-keyword">if</span> min_moves.value == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> (final_moves_seq <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>
                                    <span class="hljs-keyword">and</span> len(final_moves_seq) &lt; min_moves.value):
            min_moves.value = len(final_moves_seq)
            result[<span class="hljs-string">'min_moves'</span>] = final_moves_seq
            result[<span class="hljs-string">'min_moves_len'</span>] = min_moves.value
            print(
                <span class="hljs-string">f'pid: <span class="hljs-subst">{pid}</span>, changed min_moves to: <span class="hljs-subst">{result[<span class="hljs-string">"min_moves_len"</span>]}</span>, <span class="hljs-subst">{final_moves_seq}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">return</span> result

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">colors</span>):</span>
    <span class="hljs-comment"># Single game object which holds the tiles in play,</span>
    <span class="hljs-comment"># and the current connections groups and clickables</span>
    game = {
        <span class="hljs-string">'tiles'</span>: [],
        <span class="hljs-string">'clickables'</span>: [],
        <span class="hljs-string">'connection_groups'</span>: []
    }

    <span class="hljs-comment"># Set the board as per the input colors</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        game[<span class="hljs-string">'tiles'</span>].append([])
        <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(AutoPlay.MAX_ROWS):
            tile = {<span class="hljs-string">'id'</span>: (row, col), <span class="hljs-string">"connections"</span>: [],
                    <span class="hljs-string">"clickable"</span>: <span class="hljs-literal">False</span>, <span class="hljs-string">"color"</span>: colors[col][row]}
            game[<span class="hljs-string">'tiles'</span>][col].append(tile)

    <span class="hljs-comment"># Go through the tiles and find out the connections</span>
    <span class="hljs-comment"># between them, and also save the clickables</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        AutoPlay.process_tile(game, <span class="hljs-number">0</span>, col)

    start = timer()
    print(<span class="hljs-string">f'start time: <span class="hljs-subst">{start}</span>'</span>)

    <span class="hljs-comment"># Create as many tasks as there are connection groups.</span>
    <span class="hljs-comment"># We're using deepcopy to create a deeply cloned game</span>
    <span class="hljs-comment"># object for each task. The current move is the first</span>
    <span class="hljs-comment"># entry of every connection group (the lowest column</span>
    <span class="hljs-comment"># index in the bottom row)</span>
    tasks = []
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
        g = copy.deepcopy(game)
        tasks.append(
            {<span class="hljs-string">'game'</span>: g, <span class="hljs-string">'curr_move'</span>: connections[<span class="hljs-number">0</span>], <span class="hljs-string">'past_moves'</span>: <span class="hljs-literal">None</span>})

    min_val = mp.Value(<span class="hljs-string">'i'</span>, <span class="hljs-number">0</span>)
    <span class="hljs-comment"># Get a managed pool from multiprocessing, and distribute the tasks to these</span>
    <span class="hljs-comment"># pools. By default it will create processes equal to the number returned by</span>
    <span class="hljs-comment"># os.cpu_count(). Also initialize min_moves global for each process using a </span>
    <span class="hljs-comment"># Value() shared object</span>
    <span class="hljs-keyword">with</span> mp.Pool(initializer=init_globals, initargs=(min_val,)) <span class="hljs-keyword">as</span> pool:
        results = pool.map(find_min_moves, tasks)
        print(<span class="hljs-string">'got results:'</span>, timer())
        <span class="hljs-keyword">for</span> result <span class="hljs-keyword">in</span> results:
            print(<span class="hljs-string">'result:'</span>, result)
</code></pre>
<h3 id="heading-the-result-1">The result</h3>
<pre><code>start time: <span class="hljs-number">0.057702366</span>
entered pid: <span class="hljs-number">39420</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.14133282</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39420</span>, changed min_moves to: <span class="hljs-number">18</span>, <span class="hljs-number">000001111223334444</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.149716058</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39420</span>, changed min_moves to: <span class="hljs-number">17</span>, <span class="hljs-number">00000111122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.151552746</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39420</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">0000011112423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.153877295</span>
entered pid: <span class="hljs-number">39419</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.195602278</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39419</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">100000122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.203628052</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39419</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">10000012423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.205814167</span>
entered pid: <span class="hljs-number">39421</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">3</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.175389625</span>
entered pid: <span class="hljs-number">39422</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">4</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.230612233</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39419</span>, changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">1004430003122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">2.607543404</span>
No more jobs: pid: <span class="hljs-number">39419</span>, final count: <span class="hljs-number">171415</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">61.43746481</span>
No more jobs: pid: <span class="hljs-number">39422</span>, final count: <span class="hljs-number">210959</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">77.746001708</span>
No more jobs: pid: <span class="hljs-number">39421</span>, final count: <span class="hljs-number">536682</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">144.053795224</span>
No more jobs: pid: <span class="hljs-number">39420</span>, final count: <span class="hljs-number">895965</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">210.756625277</span>
got results: <span class="hljs-number">211.040409704</span>
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'0000011112423334'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">16</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">895965</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39420</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'1004430003122'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">171415</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39419</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: None, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">536682</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39421</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: None, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">210959</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39422</span>}
</code></pre><p>Yay! We have made progress. Now it takes only 3 minutes 31 seconds to do the job (as compared to around 5 minutes 25 seconds with single process), with around 1.81 million end to end sequences.</p>
<h2 id="heading-the-cpu-count-and-its-implications">The CPU count and its implications</h2>
<p>Please note that all of the results were collected on my macbook pro having a dual core processor. So <code>os.cpu_count()</code> returns 4 in my case, 2 logical cores for each physical core. That is why 4 processes are getting created in the above examples. We can play around with the number of processes and see if that makes any further difference. In my case, I've found that running 2 processes gives me the best result (The value may be different for you, depending on the number of cores your workhorse has).</p>
<p>Changing one line in the <code>play</code> function</p>
<p><code>with mp.Pool(processes=2, initializer=init_globals, initargs=(min_val,)) as pool:</code></p>
<p>We get the following results:</p>
<pre><code>start time: <span class="hljs-number">0.103170482</span>
entered pid: <span class="hljs-number">39540</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.226195533</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39540</span>, changed min_moves to: <span class="hljs-number">18</span>, <span class="hljs-number">000001111223334444</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.238348483</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39540</span>, changed min_moves to: <span class="hljs-number">17</span>, <span class="hljs-number">00000111122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.239980543</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39540</span>, changed min_moves to: <span class="hljs-number">16</span>, <span class="hljs-number">0000011112423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.265787826</span>
entered pid: <span class="hljs-number">39541</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">0.239561019</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39541</span>, changed min_moves to: <span class="hljs-number">15</span>, <span class="hljs-number">100000122344334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.246132816</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39541</span>, changed min_moves to: <span class="hljs-number">14</span>, <span class="hljs-number">10000012423334</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.247991375</span>
<span class="hljs-attr">pid</span>: <span class="hljs-number">39541</span>, changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">1004430003122</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">1.319787871</span>
No more jobs: pid: <span class="hljs-number">39541</span>, final count: <span class="hljs-number">171415</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">27.815133454</span>
entered pid: <span class="hljs-number">39541</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">3</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">27.816530592</span>
No more jobs: pid: <span class="hljs-number">39541</span>, final count: <span class="hljs-number">533304</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">135.0901852</span>
entered pid: <span class="hljs-number">39541</span>: <span class="hljs-keyword">for</span> moves: (<span class="hljs-number">0</span>, <span class="hljs-number">4</span>), <span class="hljs-attr">time</span>: <span class="hljs-number">135.090335439</span>
No more jobs: pid: <span class="hljs-number">39541</span>, final count: <span class="hljs-number">207783</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">172.9650854</span>
No more jobs: pid: <span class="hljs-number">39540</span>, final count: <span class="hljs-number">895570</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">183.029733485</span>
got results: <span class="hljs-number">183.21633772</span>
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'0000011112423334'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">16</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">895570</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39540</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'1004430003122'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">13</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">171415</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39541</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: None, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">533304</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39541</span>}
<span class="hljs-attr">result</span>: {<span class="hljs-string">'min_moves'</span>: None, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">207783</span>, <span class="hljs-string">'pid'</span>: <span class="hljs-number">39541</span>}
</code></pre><p>Only 2 processes with pids 39540 &amp; 39541 were used in this case. When the process with pid 39540 was going through sequences starting with 0 column id, process with pid 39541 finished processing the sequences starting with remaining column ids (1, 3 &amp; 4 in this case). It took nearly 3 minutes (so a saving of around 30 seconds) to finish the job, with nearly the same 1.8 million end-to-end sequences.</p>
<p>And we've have a winner in our midst :-)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/l44Q6Etd5kdSGttXa/giphy.gif">https://media.giphy.com/media/l44Q6Etd5kdSGttXa/giphy.gif</a></div>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That was one long article with a lot of code, and repeated optimizations. I'm sure further optimizations can be done. I also tried to distribute jobs between different processes using a shared <code>queue</code> object (sgain from multiprocessing), but didn't get favorable results. There is one <code>Manager</code> class also available in the <code>multiprocessing</code> module, which allows us to create shared proxy objects. Using that we wouldn't need to use the initializer and initargs, and can share the object as argument like we are doing for tasks, but the documentation says that it will be slow, so I haven't covered that (of course I tried it myself :-)).</p>
<p>There must be other ways to solve the problem, after all, there are multiple ways to solve any problem in general, and programming in particular. It might be possible that there is a very simple trick to solve this instantly.</p>
<p>Do share how would you solve this problem?</p>
<p>Please hit me up if you have any questions, or if you find any error anywhere.</p>
<p>Enjoy :-) </p>
]]></content:encoded></item><item><title><![CDATA[Solving the turtle tiles puzzle game using Python]]></title><description><![CDATA[Introduction
This is a follow up of my previous article where we learnt to create a puzzle game using Python Turtle module. There we could generate as many new games as we wanted, and also replay a particular game unlimited number of times. 
But ther...]]></description><link>https://rajeev.dev/solve-puzzle-game-using-python</link><guid isPermaLink="true">https://rajeev.dev/solve-puzzle-game-using-python</guid><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[python projects]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Thu, 17 Nov 2022 12:18:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1668620324751/MtqYZMQ4g.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>This is a follow up of my <a target="_blank" href="https://rajeev.dev/creating-puzzle-game-using-python-turtle-1">previous article</a> where we learnt to create a puzzle game using Python Turtle module. There we could generate as many new games as we wanted, and also replay a particular game unlimited number of times. 
But there was one important piece missing from the equation: finding the minimum number of moves needed to solve the puzzle (i.e. to remove all the tiles from the screen).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/3o6Mb2KGFpsvvjWbPW/giphy.gif">https://media.giphy.com/media/3o6Mb2KGFpsvvjWbPW/giphy.gif</a></div>
<p>Worry not! In this article we will look at a crude way of solving the puzzle. Then we'll try to optimize our solution and make it faster. Strap your seatbelts, here we go...</p>
<h2 id="heading-approach">Approach</h2>
<p>As we had learnt in the last article, to clear the board we need to click on the solid color tiles. Any tile which is in the bottom row is clickable (and hence, solid), and tiles which are connected to these bottom row tiles become clickable if they have the same color (see the below image for understanding). When we click on a solid tile, that tile and all the connected tiles get removed from the screen. New connections are formed after every click following the earlier logic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1668656414771/08SxOhgmR.jpg" alt="figure.jpg" /></p>
<p>Now that we understand the game, let's try to come up with a logic to find out the minimum number of moves needed to clear the screen. Below are my observations:</p>
<ol>
<li>Even though we can click on any of the solid tiles (even if that tile is in, say 2nd or 3rd row), it is enough to always click on the bottom row tiles only</li>
<li>If more than one tile in the bottom row are connected to each other, it is enough to click on the tile having the lowest column index</li>
<li>For every move, we will have at most 5 choices (the 5 bottom row tiles). Overall there will be a lot of combinations of moves, and we'll need to go through all of them to find out the solution</li>
<li>After clicking a tile new connections are formed, and if we keep repeating the first 2 steps for any of the possible combinations, there will come a time when the board is clear</li>
<li>Finding one solution (one sequence of moves giving empty board) is sufficient for us</li>
</ol>
<p>With these observations in mind, let's implement our algorithm.</p>
<h2 id="heading-implementation">Implementation</h2>
<p>Below is a screenshot of Nov 16-17, 2022's puzzle from the <a target="_blank" href="https://figure.game/">original site</a>. The same combination has been used to create this article's cover image. We'll use this as a reference to verify if our solution is correct (our solution should give us 8 minimum moves as the answer).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1668624604623/RMiUT4MyE.png" alt="Screen Shot 2022-11-16 at 10.59.22 PM.png" /></p>
<p>We won't be using any custom classes for this implementation, and will rely on good old lists and dictionaries.</p>
<h3 id="heading-the-play-function">The <code>play</code> function</h3>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> timeit <span class="hljs-keyword">import</span> default_timer <span class="hljs-keyword">as</span> timer
<span class="hljs-keyword">import</span> copy

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

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>(<span class="hljs-params">colors</span>):</span>
    <span class="hljs-comment"># Single game object which holds the tiles in play,</span>
    <span class="hljs-comment"># and the current connections groups and clickables</span>
    game = {
        <span class="hljs-string">'tiles'</span>: [],
        <span class="hljs-string">'clickables'</span>: [],
        <span class="hljs-string">'connection_groups'</span>: []
    }

    <span class="hljs-comment"># Set the board as per the input colors</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        game[<span class="hljs-string">'tiles'</span>].append([])
        <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(AutoPlay.MAX_ROWS):
            tile = {<span class="hljs-string">'id'</span>: (row, col), <span class="hljs-string">"connections"</span>: [],
                    <span class="hljs-string">"clickable"</span>: <span class="hljs-literal">False</span>, <span class="hljs-string">"color"</span>: colors[col][row]}
            game[<span class="hljs-string">'tiles'</span>][col].append(tile)

    <span class="hljs-comment"># Go through the tiles and find out the connections</span>
    <span class="hljs-comment"># between them, and also save the clickables</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(AutoPlay.MAX_COLS):
        AutoPlay.process_tile(game, <span class="hljs-number">0</span>, col)

    start = timer()
    print(<span class="hljs-string">f'start time: <span class="hljs-subst">{start}</span>'</span>)

    <span class="hljs-comment"># Create as many tasks as there are connection groups.</span>
    <span class="hljs-comment"># We're using deepcopy to create a deeply cloned game</span>
    <span class="hljs-comment"># object for each task. The current move is the first</span>
    <span class="hljs-comment"># entry of every connection group (the lowest column</span>
    <span class="hljs-comment"># index in the bottom row)</span>
    tasks = []
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
        g = copy.deepcopy(game)
        tasks.append(
            {<span class="hljs-string">'game'</span>: g, <span class="hljs-string">'curr_move'</span>: connections[<span class="hljs-number">0</span>], <span class="hljs-string">'past_moves'</span>: <span class="hljs-literal">None</span>})

    <span class="hljs-comment"># Find the minimum number of moves for the above tasks</span>
    result = find_min_moves(tasks)
    print(<span class="hljs-string">'result of operation:'</span>, result)
</code></pre>
<h3 id="heading-the-findminmoves-function">The <code>find_min_moves</code> function</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_min_moves</span>(<span class="hljs-params">jobs</span>):</span>
    result = {<span class="hljs-string">'min_moves'</span>: <span class="hljs-literal">None</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>}

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># See if we've any jobs left. If there is no job, break the loop</span>
        job = jobs.pop() <span class="hljs-keyword">if</span> len(jobs) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>
        <span class="hljs-keyword">if</span> job <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
            print(
                <span class="hljs-string">f'No more jobs: final count: <span class="hljs-subst">{result[<span class="hljs-string">"count"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Handle the current job. This will take of the combinations till its logical</span>
        <span class="hljs-comment"># end (until the board is clear). Other encountered combinations will be added</span>
        <span class="hljs-comment"># to the job list for processing in due course</span>
        final_moves_seq = AutoPlay.handle_job_recurse(job, jobs)

        result[<span class="hljs-string">'count'</span>] += <span class="hljs-number">1</span>

        <span class="hljs-comment"># If the one processed combination has minimum length, then that is the minimum</span>
        <span class="hljs-comment"># numbers of moves needed to solve the puzzle</span>
        <span class="hljs-keyword">if</span> result[<span class="hljs-string">'min_moves_len'</span>] == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> len(final_moves_seq) &lt; result[<span class="hljs-string">'min_moves_len'</span>]:
            result[<span class="hljs-string">'min_moves'</span>] = final_moves_seq
            result[<span class="hljs-string">'min_moves_len'</span>] = len(final_moves_seq)
            print(
                <span class="hljs-string">f'changed min_moves to: <span class="hljs-subst">{result[<span class="hljs-string">"min_moves_len"</span>]}</span>, <span class="hljs-subst">{final_moves_seq}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">return</span> result
</code></pre>
<h3 id="heading-the-autoplay-module">The <code>AutoPlay</code> module</h3>
<p>Some of the functions are almost the same as in the previous article (small modifications needed for adjusting to non-class approach). You can refer to the first article to get better clarity on these functions.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> copy


MAX_COLS = <span class="hljs-number">5</span>
MAX_ROWS = <span class="hljs-number">5</span>


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_node</span>(<span class="hljs-params">tiles, row, col</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> &lt;= col &lt;= MAX_COLS - <span class="hljs-number">1</span> <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> &lt;= row &lt;= MAX_ROWS - <span class="hljs-number">1</span>:
        col_tiles = tiles[col]
        <span class="hljs-keyword">return</span> col_tiles[row] <span class="hljs-keyword">if</span> row &lt; len(col_tiles) <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connectable</span>(<span class="hljs-params">tiles, first_node, row, col</span>):</span>
    other_node = get_node(tiles, row, col)
    <span class="hljs-keyword">if</span> other_node <span class="hljs-keyword">and</span> first_node[<span class="hljs-string">'color'</span>] == other_node[<span class="hljs-string">'color'</span>]:
        <span class="hljs-keyword">if</span> other_node[<span class="hljs-string">'clickable'</span>]:
            <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>

        <span class="hljs-keyword">return</span> (row, col)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_tile</span>(<span class="hljs-params">game, row, col</span>):</span>
    curr_node = get_node(game[<span class="hljs-string">'tiles'</span>], row, col)
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> curr_node <span class="hljs-keyword">or</span> curr_node[<span class="hljs-string">'clickable'</span>]:
        <span class="hljs-keyword">return</span>

    has_clickable_connections = {
        <span class="hljs-string">'prev'</span>: connectable(game[<span class="hljs-string">'tiles'</span>], curr_node, row, col - <span class="hljs-number">1</span>),
        <span class="hljs-string">'next'</span>: connectable(game[<span class="hljs-string">'tiles'</span>], curr_node, row, col + <span class="hljs-number">1</span>),
        <span class="hljs-string">'below'</span>: connectable(game[<span class="hljs-string">'tiles'</span>], curr_node, row - <span class="hljs-number">1</span>, col),
        <span class="hljs-string">'above'</span>: connectable(game[<span class="hljs-string">'tiles'</span>], curr_node, row + <span class="hljs-number">1</span>, col)
    }

    <span class="hljs-keyword">if</span> row == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">in</span> has_clickable_connections.values():
        curr_node[<span class="hljs-string">'clickable'</span>] = <span class="hljs-literal">True</span>
        <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'next'</span>]:
            curr_node[<span class="hljs-string">'connections'</span>].append((row, col + <span class="hljs-number">1</span>))
        <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'above'</span>]:
            curr_node[<span class="hljs-string">'connections'</span>].append((row + <span class="hljs-number">1</span>, col))

        <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> game[<span class="hljs-string">'clickables'</span>]:
            game[<span class="hljs-string">'clickables'</span>].append((row, col))

        found = <span class="hljs-literal">False</span>
        <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
            <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections:
                found = <span class="hljs-literal">True</span>
                <span class="hljs-keyword">break</span>

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> found:
            game[<span class="hljs-string">'connection_groups'</span>].append([(row, col)])

        <span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> has_clickable_connections.values():
            <span class="hljs-keyword">if</span> isinstance(value, tuple):
                <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
                    <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections <span class="hljs-keyword">and</span> value <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> connections:
                        connections.append(value)
                        <span class="hljs-keyword">break</span>
                process_tile(game, *value)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_tile_click</span>(<span class="hljs-params">game, tile_id</span>):</span>
    <span class="hljs-comment"># Go through each of the connection groups and find the one containing this tile</span>
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>]:
        <span class="hljs-keyword">if</span> tile_id <span class="hljs-keyword">in</span> connections:
            <span class="hljs-comment"># Sort the tiles in reverse order so that we remove them from screen</span>
            <span class="hljs-comment"># from the top right</span>
            tiles_to_remove = sorted(connections, reverse=<span class="hljs-literal">True</span>)

            <span class="hljs-comment"># Make all the clickable tiles as unclickable, as connections will be reformed</span>
            <span class="hljs-keyword">for</span> clickable <span class="hljs-keyword">in</span> game[<span class="hljs-string">'clickables'</span>]:
                game[<span class="hljs-string">'tiles'</span>][clickable[<span class="hljs-number">1</span>]][clickable[<span class="hljs-number">0</span>]][<span class="hljs-string">'clickable'</span>] = <span class="hljs-literal">False</span>

            <span class="hljs-comment"># Actually remove the tiles one by one from the screen</span>
            <span class="hljs-keyword">for</span> tile_to_remove <span class="hljs-keyword">in</span> tiles_to_remove:
                game[<span class="hljs-string">'tiles'</span>][tile_to_remove[<span class="hljs-number">1</span>]].pop(tile_to_remove[<span class="hljs-number">0</span>])

                <span class="hljs-comment"># Change the id of each of the tiles above the removed tile</span>
                <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(tile_to_remove[<span class="hljs-number">0</span>], len(game[<span class="hljs-string">'tiles'</span>][tile_to_remove[<span class="hljs-number">1</span>]])):
                    game[<span class="hljs-string">'tiles'</span>][tile_to_remove[<span class="hljs-number">1</span>]][row][<span class="hljs-string">'id'</span>] = (
                        row, tile_to_remove[<span class="hljs-number">1</span>])
            <span class="hljs-keyword">break</span>

    game[<span class="hljs-string">'clickables'</span>].clear()
    game[<span class="hljs-string">'connection_groups'</span>].clear()
    <span class="hljs-comment"># Form fresh connections and find the clickables</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(MAX_COLS):
        process_tile(game, <span class="hljs-number">0</span>, col)


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_job_recurse</span>(<span class="hljs-params">job, job_list</span>):</span>
    game = job[<span class="hljs-string">'game'</span>]

    handle_tile_click(game, job[<span class="hljs-string">'curr_move'</span>])

    <span class="hljs-comment"># Add the currently executed move to the past_moves sequence (Only storing the</span>
    <span class="hljs-comment"># column id, as row id will always be 0)</span>
    <span class="hljs-keyword">if</span> job[<span class="hljs-string">'past_moves'</span>] <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        job[<span class="hljs-string">'past_moves'</span>] = <span class="hljs-string">f'<span class="hljs-subst">{job[<span class="hljs-string">"curr_move"</span>][<span class="hljs-number">1</span>]}</span>'</span>
    <span class="hljs-keyword">else</span>:
        job[<span class="hljs-string">'past_moves'</span>] = <span class="hljs-string">f'<span class="hljs-subst">{job[<span class="hljs-string">"past_moves"</span>]}</span><span class="hljs-subst">{job[<span class="hljs-string">"curr_move"</span>][<span class="hljs-number">1</span>]}</span>'</span>

    <span class="hljs-comment"># If after the click no new connection groups are left, then that means we've</span>
    <span class="hljs-comment"># cleared the screen, so return this sequence</span>
    <span class="hljs-keyword">if</span> len(game[<span class="hljs-string">'connection_groups'</span>]) == <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> job[<span class="hljs-string">'past_moves'</span>]

    <span class="hljs-comment"># Add the other possible combinations to the job list (we'll be taking the</span>
    <span class="hljs-comment"># 0th index till completion, hence slicing the list from the 1st index)</span>
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>][<span class="hljs-number">1</span>:]:
        job_list.append({<span class="hljs-string">'game'</span>: copy.deepcopy(
            game), <span class="hljs-string">'curr_move'</span>: connections[<span class="hljs-number">0</span>], <span class="hljs-string">'past_moves'</span>: job[<span class="hljs-string">'past_moves'</span>]})

    <span class="hljs-comment"># Take the 0th index to its logical end. 0th index gives us a list which</span>
    <span class="hljs-comment"># contains all the tiles connected with each other. We take the first tile</span>
    <span class="hljs-comment"># from this list (the 0th index), and use recursion to go further</span>
    job[<span class="hljs-string">'curr_move'</span>] = game[<span class="hljs-string">'connection_groups'</span>][<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]

    <span class="hljs-keyword">return</span> handle_job_recurse(job, job_list)
</code></pre>
<h3 id="heading-the-result">The result</h3>
<p>Running the code by using the board from today's puzzle (shown earlier) we get the following results</p>
<pre><code class="lang-python">play([
    [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'yellow'</span>],
    [<span class="hljs-string">'white'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>],
    [<span class="hljs-string">'yellow'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>],
    [<span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'turquoise'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'turquoise'</span>],
    [<span class="hljs-string">'white'</span>, <span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'turquoise'</span>]
])
</code></pre>
<pre><code>start time: <span class="hljs-number">0.060289866</span>
changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">3000011111233</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.068856064</span>
changed min_moves to: <span class="hljs-number">12</span>, <span class="hljs-number">300001111142</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.069172283</span>
changed min_moves to: <span class="hljs-number">11</span>, <span class="hljs-number">30003114012</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">2.164211921</span>
changed min_moves to: <span class="hljs-number">10</span>, <span class="hljs-number">3003114002</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">9.825066422</span>
changed min_moves to: <span class="hljs-number">9</span>, <span class="hljs-number">303104002</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">26.712916026</span>
changed min_moves to: <span class="hljs-number">8</span>, <span class="hljs-number">10010012</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">110.70025369</span>
No more jobs: final count: <span class="hljs-number">1389898</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">214.573611255</span>
result <span class="hljs-keyword">of</span> operation: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'10010012'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">8</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">1389898</span>}
</code></pre><p>And voila, we got the minimum moves length as 8, same as the original puzzle wanted. It took us around 3 minutes and 35 seconds to get the solution. <code>'min_moves': '10010012'</code> gives us the column indices of the bottom row which we need to click one after the other to clear the screen. In total, we processed 1389898 sequences, that is close to 1.39 million sequences (Wow!).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/UtEUhkfriklonVdweC/giphy.gif">https://media.giphy.com/media/UtEUhkfriklonVdweC/giphy.gif</a></div>
<p>Or is it?</p>
<h2 id="heading-first-optimization">First optimization</h2>
<p>Even though this is working, for a different puzzle which requires, say 13 or 14 moves, our code will take much longer to give us a solution. Can we optimize our solution somehow?</p>
<p>If you think about it, we don't need to take all the sequences to their logical end. If any sequence has already had <code>"min_moves_len - 1"</code> past moves, and there still are <code>connection_groups</code> left, then there is no point in pursuing this sequence. That is because, in the best case it will give us the same <code>min_moves_len</code> (if we assume that the next click will clear the screen), but in all the other cases we'll get a final sequence length which is more than the <code>min_moves_len</code>. </p>
<p>Keeping the above observation in mind, we will not be adding such combinations to the job list. Let's make the needed changes:</p>
<h3 id="heading-modified-findminmoves-function">Modified <code>find_min_moves</code> function</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_min_moves</span>(<span class="hljs-params">jobs</span>):</span>
    result = {<span class="hljs-string">'min_moves'</span>: <span class="hljs-literal">None</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">0</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">0</span>}

    <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
        <span class="hljs-comment"># See if we've any jobs left. If there is no job, break the loop</span>
        job = jobs.pop() <span class="hljs-keyword">if</span> len(jobs) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>
        <span class="hljs-keyword">if</span> job <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
            print(
                <span class="hljs-string">f'No more jobs: final count: <span class="hljs-subst">{result[<span class="hljs-string">"count"</span>]}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)
            <span class="hljs-keyword">break</span>

        <span class="hljs-comment"># Handle the current job. This will take of the combinations till its logical</span>
        <span class="hljs-comment"># end (until the board is clear). Other encountered combinations will be added</span>
        <span class="hljs-comment"># to the job list for processing in due course</span>
        final_moves_seq = AutoPlay.handle_job_recurse(
            job, jobs, result[<span class="hljs-string">'min_moves_len'</span>]) <span class="hljs-comment"># Sending the current min_moves_len</span>

        result[<span class="hljs-string">'count'</span>] += <span class="hljs-number">1</span>

        <span class="hljs-comment"># If the one processed combination has minimum length, then that is the minimum</span>
        <span class="hljs-comment"># numbers of moves needed to solve the puzzle</span>
        <span class="hljs-comment"># Now, final_moves_seq can be returned as None, so taking that into account</span>
        <span class="hljs-keyword">if</span> result[<span class="hljs-string">'min_moves_len'</span>] == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> (final_moves_seq <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>
                                            <span class="hljs-keyword">and</span> len(final_moves_seq) &lt; result[<span class="hljs-string">'min_moves_len'</span>]):
            result[<span class="hljs-string">'min_moves'</span>] = final_moves_seq
            result[<span class="hljs-string">'min_moves_len'</span>] = len(final_moves_seq)
            print(
                <span class="hljs-string">f'changed min_moves to: <span class="hljs-subst">{result[<span class="hljs-string">"min_moves_len"</span>]}</span>, <span class="hljs-subst">{final_moves_seq}</span>, time: <span class="hljs-subst">{timer()}</span>'</span>)

    <span class="hljs-keyword">return</span> result
</code></pre>
<h3 id="heading-modified-handlejobrecurse-function">Modified <code>handle_job_recurse</code> function</h3>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_job_recurse</span>(<span class="hljs-params">job, job_list, curr_min_moves_len</span>):</span>
    game = job[<span class="hljs-string">'game'</span>]

    handle_tile_click(game, job[<span class="hljs-string">'curr_move'</span>])

    <span class="hljs-comment"># Add the currently executed move to the past_moves sequence (Only storing the</span>
    <span class="hljs-comment"># column id, as row id will always be 0)</span>
    <span class="hljs-keyword">if</span> job[<span class="hljs-string">'past_moves'</span>] <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        job[<span class="hljs-string">'past_moves'</span>] = <span class="hljs-string">f'<span class="hljs-subst">{job[<span class="hljs-string">"curr_move"</span>][<span class="hljs-number">1</span>]}</span>'</span>
    <span class="hljs-keyword">else</span>:
        job[<span class="hljs-string">'past_moves'</span>] = <span class="hljs-string">f'<span class="hljs-subst">{job[<span class="hljs-string">"past_moves"</span>]}</span><span class="hljs-subst">{job[<span class="hljs-string">"curr_move"</span>][<span class="hljs-number">1</span>]}</span>'</span>

    <span class="hljs-comment"># If after the click no new connection groups are left, then that means we've</span>
    <span class="hljs-comment"># cleared the screen, so return this sequence</span>
    <span class="hljs-keyword">if</span> len(game[<span class="hljs-string">'connection_groups'</span>]) == <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> job[<span class="hljs-string">'past_moves'</span>]

    <span class="hljs-comment"># If there is some min_moves_len and past_moves sequence length is more than </span>
    <span class="hljs-comment"># or equal to min_moves_len - 1, then discard all further combinations in this sequence</span>
    <span class="hljs-keyword">if</span> curr_min_moves_len != <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> len(job[<span class="hljs-string">'past_moves'</span>]) &gt;= (curr_min_moves_len - <span class="hljs-number">1</span>):
        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span>

    <span class="hljs-comment"># Add the other possible combinations to the job list (we'll be taking the</span>
    <span class="hljs-comment"># 0th index till completion, hence slicing the list from the 1st index)</span>
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> game[<span class="hljs-string">'connection_groups'</span>][<span class="hljs-number">1</span>:]:
        job_list.append({<span class="hljs-string">'game'</span>: copy.deepcopy(
            game), <span class="hljs-string">'curr_move'</span>: connections[<span class="hljs-number">0</span>], <span class="hljs-string">'past_moves'</span>: job[<span class="hljs-string">'past_moves'</span>]})

    <span class="hljs-comment"># Take the 0th index to its logical end. 0th index gives us a list which</span>
    <span class="hljs-comment"># contains all the tiles connected with each other. We take the first tile</span>
    <span class="hljs-comment"># from this list (the 0th index), and use recursion to go further</span>
    job[<span class="hljs-string">'curr_move'</span>] = game[<span class="hljs-string">'connection_groups'</span>][<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]

    <span class="hljs-keyword">return</span> handle_job_recurse(job, job_list, curr_min_moves_len)
</code></pre>
<h3 id="heading-the-result">The result</h3>
<p>Running the same starting board as in the previous run, we get the following results</p>
<pre><code>start time: <span class="hljs-number">0.116954505</span>
changed min_moves to: <span class="hljs-number">13</span>, <span class="hljs-number">3000011111233</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.129312172</span>
changed min_moves to: <span class="hljs-number">12</span>, <span class="hljs-number">300001111142</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.130486732</span>
changed min_moves to: <span class="hljs-number">11</span>, <span class="hljs-number">30003114012</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">0.642993931</span>
changed min_moves to: <span class="hljs-number">10</span>, <span class="hljs-number">3003114002</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">1.395034809</span>
changed min_moves to: <span class="hljs-number">9</span>, <span class="hljs-number">303104002</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">2.094061232</span>
changed min_moves to: <span class="hljs-number">8</span>, <span class="hljs-number">10010012</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">3.78454475</span>
No more jobs: final count: <span class="hljs-number">16724</span>, <span class="hljs-attr">time</span>: <span class="hljs-number">4.46511333</span>
result <span class="hljs-keyword">of</span> operation: {<span class="hljs-string">'min_moves'</span>: <span class="hljs-string">'10010012'</span>, <span class="hljs-string">'min_moves_len'</span>: <span class="hljs-number">8</span>, <span class="hljs-string">'count'</span>: <span class="hljs-number">16724</span>}
</code></pre><p>It took us only 4.46 seconds to complete the job, and there were only 16.7K sequences which were taken to their logical end. Now we're talking :-)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/KnKSXq9qxgZDa/giphy.gif">https://media.giphy.com/media/KnKSXq9qxgZDa/giphy.gif</a></div>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this post we looked at one crude way of solving the puzzle which we'd created in the last post. The solution is by no means complete, or fully optimized. We can do many optimizations to find the solution sooner. We will look at some of those optimizations in the next and the final post of this series.</p>
<p>Hope you enjoyed reading the post. Do let me know how would you solve the problem :-).</p>
]]></content:encoded></item><item><title><![CDATA[Creating a puzzle game using Python Turtle module]]></title><description><![CDATA[To read the second and the final installment of this series, please visit this link
The inspiration
Couple of months back came across a twitter post announcing a daily puzzle game. Maybe it was the FOMO created by the Wordle bandwagon (never played t...]]></description><link>https://rajeev.dev/creating-puzzle-game-using-python-turtle-1</link><guid isPermaLink="true">https://rajeev.dev/creating-puzzle-game-using-python-turtle-1</guid><category><![CDATA[Python]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[python projects]]></category><category><![CDATA[python turtle]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Rajeev R. Sharma]]></dc:creator><pubDate>Wed, 09 Nov 2022 19:01:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669189361059/_9kdsc3MI.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>To read the second and the final installment of this series, please visit <a target="_blank" href="https://rajeev.dev/solve-puzzle-game-using-python-multiprocessing">this link</a></p>
<h2 id="heading-the-inspiration">The inspiration</h2>
<p>Couple of months back came across a <a target="_blank" href="https://twitter.com/sumul/status/1545430273113866240?s=20&amp;t=Z60P7bP3Y40QxmZf9PfhVA">twitter post</a> announcing a daily puzzle game. Maybe it was the FOMO created by the Wordle bandwagon (never played that), but I really liked this puzzle game. I've been a regular player of it since then, breaking the streak only 3-4 times. You can try out the original game <a target="_blank" href="https://figure.game/">here</a></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/u10GReM6igVGg/giphy.gif">https://media.giphy.com/media/u10GReM6igVGg/giphy.gif</a></div>
<p>Over the period I've been getting the itch to implement the game logic myself. I've also been dabbling in basic Python in recent times, so I thought why not implement it using Python Turtle module itself? And hence the post :-)</p>
<h2 id="heading-the-game">The game</h2>
<p>The game idea is very simple. There are some tiles (5 x 5 grid) on the screen. All the tiles present in the bottom row are clickable (represented as solid color tiles), and any tile of same color connected to these bottom row tiles are also clickable (see the cover image for an example). When we click on these clickable tiles, the clicked tile as well as the connected tiles get removed from the screen. New connections are formed after every click, following the same earlier logic. </p>
<p>The game ends when all the tiles are removed from the screen in a given number of moves (the minimum moves needed to remove all the tiles).</p>
<h2 id="heading-the-implementation">The implementation</h2>
<p>To keep it simple, we're not going to worry about the minimum moves part in this article, and will only implement the new game creation and its game play part. In the next article we will look at a crude implementation of how we can find out the minimum number of moves needed for a given game board.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/IHnROpQICe4kE/giphy.gif">https://media.giphy.com/media/IHnROpQICe4kE/giphy.gif</a></div>
<p>You can check out the screen grab of the actual game play of this below</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=cRfjimU5KyQ">https://www.youtube.com/watch?v=cRfjimU5KyQ</a></div>
<h3 id="heading-setting-up-the-screen">Setting up the screen</h3>
<p>This part is pretty easy. We import the Turtle module and create the screen, and a turtle (named pen) for drawing everything on the screen.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> turtle
<span class="hljs-keyword">from</span> random <span class="hljs-keyword">import</span> choice

<span class="hljs-keyword">import</span> settings
<span class="hljs-keyword">from</span> Game <span class="hljs-keyword">import</span> Game

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init_game_screen</span>():</span>
    <span class="hljs-string">'''Init the turtle screen and create a pen for drawing on 
      that screen. Also, hiding the pen as we don't really need 
      to see it.
    '''</span>
    screen = turtle.Screen()
    screen.screensize(settings.canv_width(),
                      settings.canv_height(), <span class="hljs-string">'midnight blue'</span>)
    screen.setup(settings.win_width(), settings.win_height())
    screen.title(<span class="hljs-string">'Figure'</span>)
    screen.tracer(<span class="hljs-number">0</span>)

    pen = turtle.Turtle()
    pen.pensize(settings.OUTER_OUTLINE)
    pen.penup()
    pen.hideturtle()

    <span class="hljs-keyword">return</span> screen, pen
</code></pre>
<p>Here <code>settings</code> is a simple module where the game constants, and some utility methods are present. Note that we've put the screen tracer value to 0 (<code>screen.tracer(0)</code>) as we don't want the inbuilt turtle screen refresh delay to make our game sluggish.</p>
<h3 id="heading-generating-the-board">Generating the board</h3>
<p>We use the <code>random</code> module to get a random color (from <code>COLORS = ['hot pink', 'white', 'yellow', 'turquoise']</code>) for each tile. List comprehension makes the code shorter, or we can use nested for loops to get the same result.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">init_game_colors</span>():</span>
    <span class="hljs-keyword">return</span> [[choice(settings.COLORS) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(settings.MAX_ROWS)]
            <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(settings.MAX_COLS)]
</code></pre>
<p>Note that we are storing the tiles colors as rows of columns (<code>colors[col][row]</code> will give us the color of a particular tile). This will helps us in breaking out early while iterating over the tiles during the game play (if there is no tile in a column at say row index 1, then there won't be any tile above it at indices 2, 3 etc. also).</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">play</span>():</span>
    screen, pen = init_game_screen()
    game = {
        <span class="hljs-string">'obj'</span>: <span class="hljs-literal">None</span>,
        <span class="hljs-string">'colors'</span>: []
    }

    start_game(screen, pen, game)

    screen.onkeypress(<span class="hljs-keyword">lambda</span>: start_game(screen, pen, game, <span class="hljs-literal">True</span>), <span class="hljs-string">'space'</span>)
    screen.onkeypress(<span class="hljs-keyword">lambda</span>: start_game(screen, pen, game), <span class="hljs-string">'n'</span>)
    screen.listen()

    screen.mainloop()


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
    play()
</code></pre>
<p>We've also added the options to start a new game, or to replay the current game. We've used the <code>n</code> and the <code>space</code> keys for the corresponding actions. The same screen and pen is reused over multiple game plays.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start_game</span>(<span class="hljs-params">screen, pen, game, replay=False</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> replay <span class="hljs-keyword">or</span> len(game[<span class="hljs-string">'colors'</span>]) == <span class="hljs-number">0</span>:
        game[<span class="hljs-string">'colors'</span>] = init_game_colors()

    <span class="hljs-keyword">if</span> game[<span class="hljs-string">'obj'</span>] <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
        game[<span class="hljs-string">'obj'</span>] = Game(game[<span class="hljs-string">'colors'</span>], screen, pen)
    <span class="hljs-keyword">else</span>:
        game[<span class="hljs-string">'obj'</span>].reset(game[<span class="hljs-string">'colors'</span>])

    pen.clear()
    game[<span class="hljs-string">'obj'</span>].start()
</code></pre>
<p>Since the same function is being used for a new, or a replay game, we clear the drawings made by the pen (<code>pen.clear()</code>) before actually starting the game. I wanted to reuse the existing game class obj even for a new game, that's why we see a <code>reset()</code> method lurking there.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/3o6YfXtqIrtPrApeuI/giphy.gif">https://media.giphy.com/media/3o6YfXtqIrtPrApeuI/giphy.gif</a></div>
<h3 id="heading-the-tile-class">The <code>Tile</code> class</h3>
<p>The <code>Tile</code> class just stores the information related to a particular tile, and should be self explanatory. The only important thing to note is the <code>tile_id</code>, stored as (<code>self._id</code>). Tile id is being saved as a tuple of form (row, col). This is why when we calculate the x, y position of the tile on the screen, we use <code>self._id[1]</code> for getting the column index, and <code>self._id[0]</code> for getting the row index. This is opposite to how we are iterating for generating the colors, or how we'll iterate during the game play. We can change this for consistency if we want to. I haven't changed it, as initially my iterations were columns of rows instead of the current rows of columns, and I am too lazy to change everything now.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> settings

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Tile</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._id = <span class="hljs-literal">None</span>
        self._clickable = <span class="hljs-literal">False</span>
        self._color = <span class="hljs-literal">None</span>
        self._shape = <span class="hljs-literal">None</span>
        self._x = <span class="hljs-number">0</span>
        self._y = <span class="hljs-number">0</span>
        self._x_bounds = [<span class="hljs-number">0</span>, <span class="hljs-number">0</span>]
        self._y_bounds = [<span class="hljs-number">0</span>, <span class="hljs-number">0</span>]
        self._connections = []

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">set_tile_props</span>(<span class="hljs-params">self, tile_id, color=None</span>):</span>
        self._id = tile_id
        self._clickable = <span class="hljs-literal">False</span>
        self._connections.clear()

        <span class="hljs-keyword">if</span> color:
            self._color = color

            index = settings.COLORS.index(color)
            self._shape = settings.INNER_SHAPES[index]

        self._x = (self._id[<span class="hljs-number">1</span>] - (settings.MAX_COLS - <span class="hljs-number">1</span>) / <span class="hljs-number">2</span>) * \
            (settings.OUTER_TILE_SIZE + settings.TILES_GAP)
        self._y = (self._id[<span class="hljs-number">0</span>] - (settings.MAX_ROWS - <span class="hljs-number">1</span>) / <span class="hljs-number">2</span>) * \
            (settings.OUTER_TILE_SIZE + settings.TILES_GAP)

        self._x_bounds[<span class="hljs-number">0</span>] = self._x - settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>
        self._x_bounds[<span class="hljs-number">1</span>] = self._x + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>
        self._y_bounds[<span class="hljs-number">0</span>] = self._y - settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>
        self._y_bounds[<span class="hljs-number">1</span>] = self._y + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">add_connection</span>(<span class="hljs-params">self, conn_id</span>):</span>
        self._connections.append(conn_id)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connections</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self._connections

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">in_bounds</span>(<span class="hljs-params">self, x, y</span>):</span>
        <span class="hljs-keyword">return</span> self._x_bounds[<span class="hljs-number">0</span>] &lt;= x &lt;= self._x_bounds[<span class="hljs-number">1</span>] <span class="hljs-keyword">and</span> \
            self._y_bounds[<span class="hljs-number">0</span>] &lt;= y &lt;= self._y_bounds[<span class="hljs-number">1</span>]

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">id</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self._id

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">color</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self._color

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pos</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self._x, self._y

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">shape</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">return</span> self._shape

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">clickable</span>(<span class="hljs-params">self, can_click=None</span>):</span>
        <span class="hljs-keyword">if</span> can_click <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
            self._clickable = can_click
        <span class="hljs-keyword">else</span>:
            <span class="hljs-keyword">return</span> self._clickable
</code></pre>
<h3 id="heading-the-game-class">The <code>Game</code> class</h3>
<p>This is the brain of the game. We will go through this class step by step.</p>
<h4 id="heading-the-constructor-andamp-the-reset-method">The <code>constructor</code> &amp; the <code>reset</code> method</h4>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> Tile <span class="hljs-keyword">import</span> Tile
<span class="hljs-keyword">import</span> settings

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Game</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, colors, screen, pen</span>):</span>
        self.screen = screen
        self.pen = pen
        self.tiles = []
        self.cache = []
        self.colors = colors
        self.clickables = []
        self.connection_groups = []
        self.moves = <span class="hljs-number">0</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">reset</span>(<span class="hljs-params">self, colors</span>):</span>
        self.tiles.clear()
        self.colors = colors
        self.clickables.clear()
        self.connection_groups.clear()
        self.moves = <span class="hljs-number">0</span>
</code></pre>
<p>The variables to note here are <code>tiles</code>, <code>cache</code>, <code>clickables</code> &amp; <code>connection_groups</code>. </p>
<ul>
<li><code>tiles</code>: stores the tiles currently being shown on the screen</li>
<li><code>cache</code>: stores the tiles which have been removed from the screen (Don't really need it, but we'll be reusing the tiles for a new game or a replay, and hence the presence).</li>
<li><code>clickables</code>: stores the ids of tiles which are clickable at the moment</li>
<li><code>connections_groups</code>: is a list which stores lists of ids, of clickable interconnected tiles</li>
</ul>
<h4 id="heading-the-start-and-other-relevant-methods">The <code>start</code> and other relevant methods</h4>
<p>Below is the code for the <code>start</code> method which internally calls <code>create_tiles</code> &amp; <code>draw_board</code> methods. Notice that till now we haven't really stated listening to tiles clicks events (no point if the game hasn't started yet, right?). We start doing this by listening to screen clicks, and then figuring out which tile was clicked.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">start</span>(<span class="hljs-params">self</span>):</span>
    self.create_tiles()

    self.draw_board()

    self.write_text(<span class="hljs-number">0</span>, -self.screen.window_height() /
                    <span class="hljs-number">2</span> + <span class="hljs-number">100</span>, <span class="hljs-string">'Click any of the colored tiles'</span>, <span class="hljs-number">18</span>)
    self.screen.onclick(self.on_screen_click)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create_tiles</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(settings.MAX_COLS):
        self.tiles.append([])
        <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(settings.MAX_ROWS):
            tile = self.cache.pop() <span class="hljs-keyword">if</span> len(self.cache) &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> Tile()
            tile.set_tile_props((row, col), self.colors[col][row]) <span class="hljs-comment"># tile_id being set as (row, col)</span>
            self.tiles[col].append(tile)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">write_text</span>(<span class="hljs-params">self, x, y, text, size</span>):</span>
    <span class="hljs-keyword">if</span> self.pen.color() != <span class="hljs-string">'white'</span>:
        self.pen.color(<span class="hljs-string">'white'</span>)

    self.pen.goto(x, y)
    self.pen.write(text, align=<span class="hljs-string">'center'</span>, font=(<span class="hljs-string">'Courier'</span>, size, <span class="hljs-string">'normal'</span>))
</code></pre>
<h4 id="heading-the-drawboard-method">The <code>draw_board</code> method</h4>
<p>This is an important method. Its job is to figure out the connections, draw the tiles and make the screen ready for the user. It's like a manager who is going to give the demo, hoping that everyone has done their job correctly. </p>
<p>We don't want no broken windows, do we? ;-)</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/QWjyvdpMDYKbOFLdIv/giphy.gif">https://media.giphy.com/media/QWjyvdpMDYKbOFLdIv/giphy.gif</a></div>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">draw_board</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(settings.MAX_COLS):
        self.process_tile(<span class="hljs-number">0</span>, col)

    self.draw_tiles()

    self.write_text(<span class="hljs-number">0</span>, self.screen.window_height() / <span class="hljs-number">2</span> - <span class="hljs-number">80</span>, <span class="hljs-string">'Figure'</span>, <span class="hljs-number">42</span>)
    <span class="hljs-keyword">if</span> len(self.clickables) == <span class="hljs-number">0</span>:
        self.screen.onclick(<span class="hljs-literal">None</span>)
        self.write_text(<span class="hljs-number">0</span>, <span class="hljs-number">80</span>, <span class="hljs-string">'Game Over'</span>, <span class="hljs-number">36</span>)
        self.write_text(<span class="hljs-number">0</span>, <span class="hljs-number">50</span>, <span class="hljs-string">f'Total <span class="hljs-subst">{self.moves}</span> moves'</span>, <span class="hljs-number">20</span>)
        self.write_text(<span class="hljs-number">0</span>, <span class="hljs-number">-60</span>, <span class="hljs-string">'Press "space" to replay'</span>, <span class="hljs-number">20</span>)
        self.write_text(<span class="hljs-number">0</span>, <span class="hljs-number">-90</span>, <span class="hljs-string">'Press "n" to start a new game'</span>, <span class="hljs-number">20</span>)
    <span class="hljs-keyword">else</span>:
        self.write_text(<span class="hljs-number">0</span>, self.screen.window_height() /
                        <span class="hljs-number">2</span> - <span class="hljs-number">140</span>, <span class="hljs-string">f'<span class="hljs-subst">{self.moves}</span> moves'</span>, <span class="hljs-number">20</span>)

    self.screen.update()
</code></pre>
<p>Notice the <code>self.screen.update()</code> call at the end. Since we had turned off the tracer while creating the screen, we will need to refresh the screen ourselves, the method call does precisely that. </p>
<p>The code below iterates over the bottom row of the tiles, and makes the tiles present as clickable. Every tile, in turn, figures out if we need to go further down the tree and find out other connectable tiles.</p>
<pre><code class="lang-python"><span class="hljs-keyword">for</span> col <span class="hljs-keyword">in</span> range(settings.MAX_COLS):
    self.process_tile(<span class="hljs-number">0</span>, col)
</code></pre>
<h4 id="heading-the-processtile-andamp-other-related-methods">The <code>process_tile</code> &amp; other related methods</h4>
<p>This method figures out which of the tiles are interconnected. We need to call this method before calling <code>draw_tiles</code>, as <code>draw_tiles</code> will also draw the connections on the screen.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_node</span>(<span class="hljs-params">self, row, col</span>):</span>
    <span class="hljs-keyword">if</span> <span class="hljs-number">0</span> &lt;= col &lt;= settings.MAX_COLS - <span class="hljs-number">1</span> <span class="hljs-keyword">and</span> <span class="hljs-number">0</span> &lt;= row &lt;= settings.MAX_ROWS - <span class="hljs-number">1</span>:
        col_tiles = self.tiles[col]
        <span class="hljs-keyword">return</span> col_tiles[row] <span class="hljs-keyword">if</span> row &lt; len(col_tiles) <span class="hljs-keyword">else</span> <span class="hljs-literal">None</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_tile</span>(<span class="hljs-params">self, row, col</span>):</span>
    curr_node = self.get_node(row, col)
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> curr_node <span class="hljs-keyword">or</span> curr_node.clickable():
        <span class="hljs-keyword">return</span>

    has_clickable_connections = {
        <span class="hljs-string">'prev'</span>: self.connectable(curr_node, row, col - <span class="hljs-number">1</span>),
        <span class="hljs-string">'next'</span>: self.connectable(curr_node, row, col + <span class="hljs-number">1</span>),
        <span class="hljs-string">'below'</span>: self.connectable(curr_node, row - <span class="hljs-number">1</span>, col),
        <span class="hljs-string">'above'</span>: self.connectable(curr_node, row + <span class="hljs-number">1</span>, col)
    }

    <span class="hljs-keyword">if</span> row == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">in</span> has_clickable_connections.values():
        curr_node.clickable(<span class="hljs-literal">True</span>)
        <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'next'</span>]:
            curr_node.add_connection((row, col + <span class="hljs-number">1</span>))
        <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'above'</span>]:
            curr_node.add_connection((row + <span class="hljs-number">1</span>, col))

        <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self.clickables:
            self.clickables.append((row, col))

        found = <span class="hljs-literal">False</span>
        <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> self.connection_groups:
            <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections:
                found = <span class="hljs-literal">True</span>
                <span class="hljs-keyword">break</span>

        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> found:
            self.connection_groups.append([(row, col)])

        <span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> has_clickable_connections.values():
            <span class="hljs-keyword">if</span> isinstance(value, tuple):
                <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> self.connection_groups:
                    <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections <span class="hljs-keyword">and</span> value <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> connections:
                        connections.append(value)
                        <span class="hljs-keyword">break</span>
                self.process_tile(*value)
</code></pre>
<p>We get the <code>current_node</code> and if the node is not there, or if it is already clickable then we return from the function as no further processing is needed.</p>
<pre><code class="lang-python">curr_node = self.get_node(row, col)
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> curr_node <span class="hljs-keyword">or</span> curr_node.clickable():
    <span class="hljs-keyword">return</span>
</code></pre>
<p>Then we look around the current node and see if we can become a clickable node by virtue of being connected to another clickable node of same color.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/mfGkfzHM3KfdI0OmIW/giphy.gif">https://media.giphy.com/media/mfGkfzHM3KfdI0OmIW/giphy.gif</a></div>
<pre><code class="lang-python">has_clickable_connections = {
    <span class="hljs-string">'prev'</span>: self.connectable(curr_node, row, col - <span class="hljs-number">1</span>),
    <span class="hljs-string">'next'</span>: self.connectable(curr_node, row, col + <span class="hljs-number">1</span>),
    <span class="hljs-string">'below'</span>: self.connectable(curr_node, row - <span class="hljs-number">1</span>, col),
    <span class="hljs-string">'above'</span>: self.connectable(curr_node, row + <span class="hljs-number">1</span>, col)
}
</code></pre>
<p><em>The <code>connectable</code> method</em></p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">connectable</span>(<span class="hljs-params">self, first_node, row, col</span>):</span>
    other_node = self.get_node(row, col)
    <span class="hljs-keyword">if</span> other_node <span class="hljs-keyword">and</span> first_node.color() == other_node.color():
        <span class="hljs-keyword">if</span> other_node.clickable():
            <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>

        <span class="hljs-keyword">return</span> (row, col)
</code></pre>
<p>The <code>connectable</code> method returns true if the other node exists, is clickable and of the same color. If it is of the same color but not currently clickable, then it returns its calling card (the tile_id). We do this because we need to traverse the tree, and if possible, make this node also clickable. </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/YpvufSuDWxOEB9LwNW/giphy.gif">https://media.giphy.com/media/YpvufSuDWxOEB9LwNW/giphy.gif</a></div>
<p>Rest of the code in the <code>process_tile</code> method is simply about making the current_node clickable based on above findings, and then traverse the tree based on whoever gave their calling cards. We also save the respective ids of the clickable and connected nodes in appropriate locations for game play.</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> row == <span class="hljs-number">0</span> <span class="hljs-keyword">or</span> <span class="hljs-literal">True</span> <span class="hljs-keyword">in</span> has_clickable_connections.values():
    curr_node.clickable(<span class="hljs-literal">True</span>)
    <span class="hljs-comment"># Every clickable node will only store the forward connections (next or above)</span>
    <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'next'</span>]: 
        curr_node.add_connection((row, col + <span class="hljs-number">1</span>))
    <span class="hljs-keyword">if</span> has_clickable_connections[<span class="hljs-string">'above'</span>]:
        curr_node.add_connection((row + <span class="hljs-number">1</span>, col))

    <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> self.clickables:
        self.clickables.append((row, col))

    <span class="hljs-comment"># We find the appropriate list where this id is already present</span>
    found = <span class="hljs-literal">False</span>
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> self.connection_groups:
        <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections:
            found = <span class="hljs-literal">True</span>
            <span class="hljs-keyword">break</span>

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> found:
        <span class="hljs-comment"># Append a new list containing the tile_id to the connection_groups</span>
        self.connection_groups.append([(row, col)])

    <span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> has_clickable_connections.values():
        <span class="hljs-comment"># Here if we've a tuple, means we need to traverse that node</span>
        <span class="hljs-comment"># Also, we need to add this node to the connection list</span>
        <span class="hljs-keyword">if</span> isinstance(value, tuple):
            <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> self.connection_groups:
                <span class="hljs-keyword">if</span> (row, col) <span class="hljs-keyword">in</span> connections <span class="hljs-keyword">and</span> value <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> connections:
                    connections.append(value)
                    <span class="hljs-keyword">break</span>
            <span class="hljs-comment"># Recursive call to traverse this node</span>
            self.process_tile(*value)
</code></pre>
<p>We can optimize the finding of appropriate connection_group list where we need to append the next node. We can do this by finding and storing the list index from the previous call and use that later.</p>
<h4 id="heading-the-drawtiles-andamp-drawtile-method">The <code>draw_tiles</code> &amp; <code>draw_tile</code> method</h4>
<p>This is quite straight forward.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">draw_tiles</span>(<span class="hljs-params">self</span>):</span>
    <span class="hljs-keyword">for</span> col_tiles <span class="hljs-keyword">in</span> self.tiles:
        <span class="hljs-keyword">for</span> tile <span class="hljs-keyword">in</span> col_tiles:
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> tile: <span class="hljs-comment"># there won't be any tile above it also, so break</span>
                <span class="hljs-keyword">break</span>

            self.draw_tile(tile)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">draw_tile</span>(<span class="hljs-params">self, tile</span>):</span>
    pen = self.pen
    pos = tile.pos()
    pen.goto(pos)
    pen.shape(<span class="hljs-string">'square'</span>)
    pen.color(tile.color())
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> tile.clickable():
        pen.fillcolor(<span class="hljs-string">'midnight blue'</span>)
    pen.shapesize(settings.OUTER_SIZE_MULTIPLIER,
                  settings.OUTER_SIZE_MULTIPLIER, settings.OUTER_OUTLINE)
    pen.stamp()

    <span class="hljs-keyword">if</span> tile.clickable():
        pen.color(<span class="hljs-string">'midnight blue'</span>)
    <span class="hljs-keyword">else</span>:
        pen.color(tile.color())
    pen.shapesize(settings.INNER_SIZE_MULTIPLIER,
                  settings.INNER_SIZE_MULTIPLIER, settings.INNER_OUTLINE)

    tilt = <span class="hljs-number">0</span>
    <span class="hljs-keyword">if</span> tile.shape() == <span class="hljs-string">'diamond'</span>:
        pen.shape(<span class="hljs-string">'square'</span>)
        pen.tilt(<span class="hljs-number">45</span>)
        tilt = <span class="hljs-number">-45</span>
    <span class="hljs-keyword">else</span>:
        pen.shape(tile.shape())
        <span class="hljs-keyword">if</span> tile.shape() == <span class="hljs-string">'triangle'</span>:
            pen.tilt(<span class="hljs-number">90</span>)
            tilt = <span class="hljs-number">-90</span>

    pen.stamp()
    pen.tilt(tilt)

    tile_id = tile.id()
    connections = tile.connections()
    <span class="hljs-keyword">if</span> (tile_id[<span class="hljs-number">0</span>], tile_id[<span class="hljs-number">1</span>] + <span class="hljs-number">1</span>) <span class="hljs-keyword">in</span> connections:
        pen.goto(pos[<span class="hljs-number">0</span>] + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>, pos[<span class="hljs-number">1</span>])
        pen.color(tile.color())
        pen.pendown()
        pen.setx(pen.xcor() + settings.TILES_GAP)
        pen.penup()
    <span class="hljs-keyword">if</span> (tile_id[<span class="hljs-number">0</span>] + <span class="hljs-number">1</span>, tile_id[<span class="hljs-number">1</span>]) <span class="hljs-keyword">in</span> connections:
        pen.goto(pos[<span class="hljs-number">0</span>], pos[<span class="hljs-number">1</span>] + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>)
        pen.color(tile.color())
        pen.pendown()
        pen.sety(pen.ycor() + settings.TILES_GAP)
        pen.penup()
</code></pre>
<h4 id="heading-the-onscreenclick-method">The <code>on_screen_click</code> method</h4>
<p>This is the method which gets called when we click anywhere on the screen. It gives us the (x, y) co-ordinates of the point where the click occurred.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_screen_click</span>(<span class="hljs-params">self, x, y</span>):</span>
    <span class="hljs-comment"># Step size is nothing but one tile size + the gap </span>
    <span class="hljs-comment"># between two consecutive tiles</span>
    extreme_x = (settings.MAX_COLS - <span class="hljs-number">1</span>) / <span class="hljs-number">2</span> * \
        settings.STEP_SIZE + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>
    extreme_y = (settings.MAX_ROWS - <span class="hljs-number">1</span>) / <span class="hljs-number">2</span> * \
        settings.STEP_SIZE + settings.OUTER_TILE_SIZE / <span class="hljs-number">2</span>

    <span class="hljs-comment"># If the click is within the tiles area, then only we'll proceed further</span>
    <span class="hljs-keyword">if</span> -extreme_x &lt;= x &lt;= extreme_x <span class="hljs-keyword">and</span> -extreme_y &lt;= y &lt;= extreme_y:
        clicked_tile_id = <span class="hljs-literal">None</span>
        <span class="hljs-comment"># To proceed further, we only look at the clickable tiles </span>
        <span class="hljs-keyword">for</span> clickable <span class="hljs-keyword">in</span> self.clickables:
            <span class="hljs-keyword">if</span> self.tiles[clickable[<span class="hljs-number">1</span>]][clickable[<span class="hljs-number">0</span>]].in_bounds(x, y):
                clicked_tile_id = clickable
                <span class="hljs-keyword">break</span>

        <span class="hljs-keyword">if</span> clicked_tile_id:
            <span class="hljs-comment"># Increment the total moves if we've a valid tile click</span>
            self.moves += <span class="hljs-number">1</span>
            self.handle_tile_click(clicked_tile_id)
</code></pre>
<h4 id="heading-the-handletileclick-method">The <code>handle_tile_click</code> method</h4>
<p>Here we take care of the tile click by deleting that tile and the other connected tiles.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_tile_click</span>(<span class="hljs-params">self, tile_id</span>):</span>
    <span class="hljs-comment"># Find out the connection group which this tile belongs to</span>
    <span class="hljs-keyword">for</span> connections <span class="hljs-keyword">in</span> self.connection_groups:
        <span class="hljs-keyword">if</span> tile_id <span class="hljs-keyword">in</span> connections:
            <span class="hljs-comment"># We'll be deleting the tiles from top to bottom so we sort </span>
            <span class="hljs-comment"># in reverse order. The reason is the same, we don't want </span>
            <span class="hljs-comment"># to delete a lower row index tile and then find out that we</span>
            <span class="hljs-comment"># need to delete the above one as well</span>
            tiles_to_remove = sorted(connections, reverse=<span class="hljs-literal">True</span>)

            <span class="hljs-comment"># Make all the nodes unclickable as we'll be reprocessing</span>
            <span class="hljs-comment"># all the remaining tiles</span>
            <span class="hljs-keyword">for</span> clickable <span class="hljs-keyword">in</span> self.clickables:
                self.tiles[clickable[<span class="hljs-number">1</span>]][clickable[<span class="hljs-number">0</span>]].clickable(<span class="hljs-literal">False</span>)

            <span class="hljs-keyword">for</span> tile_to_remove <span class="hljs-keyword">in</span> tiles_to_remove:
                <span class="hljs-comment"># using pop() to get the removed item so that we can add</span>
                <span class="hljs-comment"># it to the cache</span>
                tile = self.tiles[tile_to_remove[<span class="hljs-number">1</span>]].pop(tile_to_remove[<span class="hljs-number">0</span>])
                self.cache.append(tile)

                <span class="hljs-comment"># After removing a tile, we need to change the tile_id (basically </span>
                <span class="hljs-comment"># the row index) of all the tiles above it</span>
                <span class="hljs-comment"># Notice the appropriate use of row and col indices while getting </span>
                <span class="hljs-comment"># the tile from tiles list, and while using it as tile_id (opposite)</span>
                <span class="hljs-keyword">for</span> row <span class="hljs-keyword">in</span> range(tile_to_remove[<span class="hljs-number">0</span>], len(self.tiles[tile_to_remove[<span class="hljs-number">1</span>]])):
                    self.tiles[tile_to_remove[<span class="hljs-number">1</span>]][row].set_tile_props(
                        (row, tile_to_remove[<span class="hljs-number">1</span>]))
            <span class="hljs-keyword">break</span> <span class="hljs-comment"># if we found the appropriate connection_group then need to break</span>

    <span class="hljs-comment"># Clear everything as we need to remake the connections and redraw the board</span>
    self.clickables.clear()
    self.connection_groups.clear()
    self.pen.clear()

    self.draw_board()
</code></pre>
<h3 id="heading-the-settings-module">The <code>settings</code> module</h3>
<pre><code class="lang-python">DEF_TILE_SIZE = <span class="hljs-number">20</span> <span class="hljs-comment"># This is the default turtle size</span>

MAX_COLS = <span class="hljs-number">5</span>
MAX_ROWS = <span class="hljs-number">5</span>

TILES_GAP = <span class="hljs-number">12</span>
OUTER_SIZE_MULTIPLIER = <span class="hljs-number">2</span> <span class="hljs-comment"># We make the tile 2X the default turtle size</span>
OUTER_OUTLINE = <span class="hljs-number">4</span>

INNER_SIZE_MULTIPLIER = <span class="hljs-number">0.6</span> <span class="hljs-comment"># For the inner shapes (triangle, circle etc.)</span>
INNER_OUTLINE = <span class="hljs-number">1</span>

COLORS = [<span class="hljs-string">'hot pink'</span>, <span class="hljs-string">'white'</span>, <span class="hljs-string">'yellow'</span>, <span class="hljs-string">'turquoise'</span>]
INNER_SHAPES = [<span class="hljs-string">'triangle'</span>, <span class="hljs-string">'square'</span>, <span class="hljs-string">'circle'</span>, <span class="hljs-string">'diamond'</span>]

OUTER_TILE_SIZE = DEF_TILE_SIZE * OUTER_SIZE_MULTIPLIER
INNER_TILE_SIZE = DEF_TILE_SIZE * INNER_SIZE_MULTIPLIER

STEP_SIZE = OUTER_TILE_SIZE + TILES_GAP


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">canv_width</span>():</span>
    <span class="hljs-keyword">return</span> MAX_COLS * OUTER_TILE_SIZE + (MAX_COLS - <span class="hljs-number">1</span>) * TILES_GAP


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">canv_height</span>():</span>
    <span class="hljs-keyword">return</span> MAX_ROWS * OUTER_TILE_SIZE + (MAX_ROWS - <span class="hljs-number">1</span>) * TILES_GAP


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">win_width</span>():</span>
    <span class="hljs-keyword">return</span> canv_width() + <span class="hljs-number">4</span> * OUTER_TILE_SIZE


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">win_height</span>():</span>
    <span class="hljs-keyword">return</span> canv_height() + <span class="hljs-number">8</span> * OUTER_TILE_SIZE
</code></pre>
<p>And that's it. The game is done. </p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://media.giphy.com/media/3oKIPf3C7HqqYBVcCk/giphy.gif">https://media.giphy.com/media/3oKIPf3C7HqqYBVcCk/giphy.gif</a></div>
<p>Thanks for sticking through the article. Please feel free to reach out if you've any questions, or if you find any mistake anywhere.</p>
<p>Have fun playing the game :-)</p>
]]></content:encoded></item></channel></rss>