<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manjunath Patil</title>
    <description>The latest articles on DEV Community by Manjunath Patil (@manjunathpatil).</description>
    <link>https://dev.to/manjunathpatil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3787205%2F74b39a73-12d6-46a1-8203-2c34d853aa1c.jpg</url>
      <title>DEV Community: Manjunath Patil</title>
      <link>https://dev.to/manjunathpatil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manjunathpatil"/>
    <language>en</language>
    <item>
      <title>The model is not the product: lessons from building with local Gemma 4</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Sun, 24 May 2026 20:11:48 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/the-model-is-not-the-product-lessons-from-building-with-local-gemma-4-1kki</link>
      <guid>https://dev.to/manjunathpatil/the-model-is-not-the-product-lessons-from-building-with-local-gemma-4-1kki</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Write About Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The model is not the product
&lt;/h2&gt;

&lt;p&gt;The easiest mistake to make with a capable local model is to treat the model call as the whole application.&lt;/p&gt;

&lt;p&gt;I almost made that mistake while building with &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My project was a local dementia-care assistant called RememberMe CareGrid. The product goal was not to make a chatbot that sounded clever. The goal was to help a confused patient get calm context, help a caregiver understand what happened, and help trusted community members respond safely.&lt;/p&gt;

&lt;p&gt;That changed how I looked at Gemma 4.&lt;/p&gt;

&lt;p&gt;Gemma 4 was not the product. Gemma 4 was the reasoning layer inside a product.&lt;/p&gt;

&lt;p&gt;The product was everything around it: transcription, context boundaries, consent, structured output, fallbacks, latency, UI state, and the decision to keep answers short when a long answer would be harmful.&lt;/p&gt;

&lt;p&gt;That is the main lesson I took away:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Local AI is not just about running a model locally. It is about deciding what the model should own, what the app should own, and what should never be delegated to the model at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why I chose Gemma 4 E2B
&lt;/h2&gt;

&lt;p&gt;Gemma 4 gives developers multiple model choices, and the obvious temptation is to reach for the biggest one.&lt;/p&gt;

&lt;p&gt;For my use case, that would have been the wrong instinct.&lt;/p&gt;

&lt;p&gt;I chose &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; because the application needed local, fast-enough, privacy-conscious reasoning. It did not need the largest possible model. It needed reliable short responses and structured care actions.&lt;/p&gt;

&lt;p&gt;In a dementia-care workflow, the model may need to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Who am I?"&lt;/li&gt;
&lt;li&gt;"Where am I?"&lt;/li&gt;
&lt;li&gt;"Who is Ananya?"&lt;/li&gt;
&lt;li&gt;"Am I safe?"&lt;/li&gt;
&lt;li&gt;"Who is standing in front of me?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not questions where a longer answer is automatically better. A confused person does not need an essay. They need one or two calm sentences and one safe next step.&lt;/p&gt;

&lt;p&gt;That made E2B a good fit. It is small enough for practical local experimentation, but capable enough to reason over care context and produce useful structured responses.&lt;/p&gt;

&lt;p&gt;A larger model could be useful for heavier summarization or complex multi-step analysis, but for the patient-facing loop, model size was not the main bottleneck. Product design was.&lt;/p&gt;

&lt;p&gt;That choice taught me that model selection is not a leaderboard decision; it is a product constraint decision.&lt;/p&gt;

&lt;p&gt;For this build, the best model was not the biggest model. It was the model that made local assistance practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture lesson: separate responsibilities
&lt;/h2&gt;

&lt;p&gt;The breakthrough was realizing that Gemma 4 should not do every job.&lt;/p&gt;

&lt;p&gt;In my first mental model, the flow felt simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user speaks -&amp;gt; AI responds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the real system, that was too vague to be useful.&lt;/p&gt;

&lt;p&gt;The better architecture was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;audio or text input
  -&amp;gt; transcription if needed
  -&amp;gt; care-context retrieval
  -&amp;gt; Gemma 4 reasoning
  -&amp;gt; structured response validation
  -&amp;gt; UI, watch, phone, or caregiver action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component has a different responsibility.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speech-to-text turns audio into words.&lt;/li&gt;
&lt;li&gt;Gemma 4 reasons over the words and care context.&lt;/li&gt;
&lt;li&gt;The application validates what actions are allowed.&lt;/li&gt;
&lt;li&gt;The UI decides how much information the patient should see.&lt;/li&gt;
&lt;li&gt;The caregiver/community flows decide who is allowed to know what.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation made the system much easier to debug. If the patient says something and the response fails, I can ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did transcription return text?&lt;/li&gt;
&lt;li&gt;Did Gemma 4 receive the right context?&lt;/li&gt;
&lt;li&gt;Did the model return valid JSON?&lt;/li&gt;
&lt;li&gt;Did the sanitizer reject an unsafe action?&lt;/li&gt;
&lt;li&gt;Did the delivery route reach the phone or watch?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without those boundaries, "the AI failed" becomes an unhelpful explanation. With those boundaries, failures become observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  JSON mode is useful, but not enough
&lt;/h2&gt;

&lt;p&gt;One of the most important decisions was asking Gemma 4 for structured JSON responses.&lt;/p&gt;

&lt;p&gt;For a normal chatbot, a text response may be enough. For a care assistant, text is only part of the output. The system also needs to know the intent, the risk level, and whether an action should happen.&lt;/p&gt;

&lt;p&gt;A simplified response shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"You are Rajamma. You are safe, and Ananya is your care contact."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"patient_identity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"risk_level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notify_caregiver"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"should_end_session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changes the role of the model. Gemma 4 is not just writing a sentence. It is helping select a safe care path.&lt;/p&gt;

&lt;p&gt;But JSON mode does not remove the need for validation.&lt;/p&gt;

&lt;p&gt;The app still needs to ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the intent one of the allowed intents?&lt;/li&gt;
&lt;li&gt;Is the action one of the allowed actions?&lt;/li&gt;
&lt;li&gt;Is the risk level valid?&lt;/li&gt;
&lt;li&gt;Is the reply short enough for the patient?&lt;/li&gt;
&lt;li&gt;Did the model hallucinate a field that should not exist?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why the wrapper matters. The model can suggest. The app must decide what is allowed.&lt;/p&gt;

&lt;p&gt;This was the most important safety lesson of the build:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Structured output is not the same as safe output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You still need a contract around the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The UX lesson: do not optimize for impressive answers
&lt;/h2&gt;

&lt;p&gt;AI demos often reward dramatic answers. Dementia-care UX does not.&lt;/p&gt;

&lt;p&gt;If the patient is confused, a brilliant paragraph can be worse than a plain sentence. Too much information can increase stress.&lt;/p&gt;

&lt;p&gt;So I used three rules for patient-facing responses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer in one or two calm sentences&lt;/li&gt;
&lt;li&gt;never shame the patient for forgetting&lt;/li&gt;
&lt;li&gt;always give one safe next step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if the patient asks "Who am I?", the answer should not be a biographical essay. It should be something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are Rajamma. Your care notes say you sometimes feel unsure, and that is okay. I am here with you, and I am letting Ananya know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not the most technically impressive output Gemma 4 can produce. But it is the right product output.&lt;/p&gt;

&lt;p&gt;This is where local models become interesting. Once the model is running close to the application, the developer can design the surrounding behavior very carefully. You are not just prompting a model. You are shaping an experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy is an execution path, not a claim
&lt;/h2&gt;

&lt;p&gt;Local inference helps, but it does not automatically make an app private.&lt;/p&gt;

&lt;p&gt;That was one of the clearest lessons from building with Gemma 4. Privacy has to show up in the actual flow of the product.&lt;/p&gt;

&lt;p&gt;In my care assistant, the privacy boundary looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model reasons over care context, but the app controls what context is sent&lt;/li&gt;
&lt;li&gt;trusted-person recall only checks against enrolled people&lt;/li&gt;
&lt;li&gt;unknown faces are not turned into identities&lt;/li&gt;
&lt;li&gt;consent is requested before saving names, photos, or transcripts&lt;/li&gt;
&lt;li&gt;community helpers receive role-specific instructions, not full patient history&lt;/li&gt;
&lt;li&gt;SOS escalation shares location only when safety logic requires it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That made the privacy work concrete. Gemma 4 running locally reduced the need to send sensitive reasoning to a remote model, but the application still had to enforce the rules around identity, consent, retention, and escalation.&lt;/p&gt;

&lt;p&gt;The lesson for me was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A local model is a privacy opportunity. The architecture decides whether that opportunity becomes real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Local AI needs observability
&lt;/h2&gt;

&lt;p&gt;Another lesson: local does not mean simple.&lt;/p&gt;

&lt;p&gt;When a cloud API fails, the error is often external. When a local model fails, the problem might be anywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the model is not loaded&lt;/li&gt;
&lt;li&gt;the model is loaded but cold&lt;/li&gt;
&lt;li&gt;the request timed out&lt;/li&gt;
&lt;li&gt;the prompt was too large&lt;/li&gt;
&lt;li&gt;the model returned malformed JSON&lt;/li&gt;
&lt;li&gt;the local speech recognizer returned no text&lt;/li&gt;
&lt;li&gt;the app routed audio to the wrong component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added provider metadata and diagnostics around the local flow. The app should know whether the response came from Ollama, which model answered, how long it took, and what failed if something went wrong.&lt;/p&gt;

&lt;p&gt;That might sound boring compared with the model itself, but it is what makes a demo feel real.&lt;/p&gt;

&lt;p&gt;The difference between a toy and a tool is often not the happy path. It is whether the system can explain what happened when the happy path breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Gemma 4 felt strongest
&lt;/h2&gt;

&lt;p&gt;Gemma 4 felt strongest when I gave it a narrow job and a clear output contract.&lt;/p&gt;

&lt;p&gt;The pattern that worked best was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specific context + narrow role + constrained output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked better than asking the model to behave like a general-purpose assistant.&lt;/p&gt;

&lt;p&gt;It helped with patient cues, caregiver summaries, training cards, and doctor briefs because each task had a bounded role. Gemma 4 did not need to invent the product flow. It needed to reason inside one.&lt;/p&gt;

&lt;p&gt;That is the part I would reuse in future projects: do not ask the model to own the whole experience. Give it a precise responsibility inside a system that knows what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell another developer
&lt;/h2&gt;

&lt;p&gt;If you are building with Gemma 4, I would not start with "How do I use the biggest model?"&lt;/p&gt;

&lt;p&gt;I would start with these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What should the model be responsible for?&lt;/li&gt;
&lt;li&gt;What should the application validate?&lt;/li&gt;
&lt;li&gt;What should never be delegated to the model?&lt;/li&gt;
&lt;li&gt;What does failure look like?&lt;/li&gt;
&lt;li&gt;What context does the model really need?&lt;/li&gt;
&lt;li&gt;What output shape does the rest of the app expect?&lt;/li&gt;
&lt;li&gt;Would a smaller local model create a better product experience?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those questions matter more than they sound.&lt;/p&gt;

&lt;p&gt;Gemma 4 makes local AI feel approachable, but good local AI still needs product boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Building with Gemma 4 changed how I think about local models.&lt;/p&gt;

&lt;p&gt;The model call is the beginning, not the finish line.&lt;/p&gt;

&lt;p&gt;The real work is deciding where the model belongs in the system: what context it gets, what it can output, how the app checks that output, and how the user experiences the result.&lt;/p&gt;

&lt;p&gt;For me, Gemma 4 E2B was powerful because it made local reasoning practical enough to place inside a real care moment.&lt;/p&gt;

&lt;p&gt;If Rajamma is confused outside her home, the goal is not for AI to sound impressive. The goal is for the system to give one calm cue, notify the right person, and avoid exposing more than necessary.&lt;/p&gt;

&lt;p&gt;That is the version of local AI I want more developers to build: not bigger demos, but smaller, safer pieces of intelligence placed exactly where people need help.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>RememberMe CareGrid: Local Gemma 4 for dementia memory and safety</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Sun, 24 May 2026 19:42:26 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/rememberme-caregrid-local-gemma-4-for-dementia-memory-and-safety-3002</link>
      <guid>https://dev.to/manjunathpatil/rememberme-caregrid-local-gemma-4-for-dementia-memory-and-safety-3002</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;RememberMe CareGrid&lt;/strong&gt;, a local AI dementia-care prototype powered by &lt;strong&gt;Gemma 4 E2B running through Ollama&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I built it as a solo participant, from the dashboard and backend to the Android relay, local model pipeline, and demo flow.&lt;/p&gt;

&lt;p&gt;The project is built for people living with dementia, Alzheimer's, or memory loss, and for the caregivers who support them every day.&lt;/p&gt;

&lt;p&gt;The moment I designed for is painfully ordinary: Rajamma steps outside, becomes confused, and cannot remember where she is, why she left, or who is standing in front of her. Her caregiver is not physically beside her. A neighbour may want to help, but should not receive private medical details. Later, a doctor may need a clean timeline, but the caregiver may only remember fragments.&lt;/p&gt;

&lt;p&gt;RememberMe CareGrid turns that moment into a coordinated care flow.&lt;/p&gt;

&lt;p&gt;It gives the patient calm memory context, helps caregivers understand what happened, and lets trusted community helpers respond with only the information they need.&lt;/p&gt;

&lt;p&gt;This is not a diagnosis tool. It is not surveillance. It is a consent-aware memory and safety network.&lt;/p&gt;

&lt;p&gt;The working prototype includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lumo Companion&lt;/strong&gt; for patient-facing memory cues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trusted-person enrollment and recall&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SafePath&lt;/strong&gt; geofencing for wandering safety&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOS escalation&lt;/strong&gt; with location and risk context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CareCircle&lt;/strong&gt; task coordination for trusted helpers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CareLearn&lt;/strong&gt; training cards generated by Gemma 4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reward wallet&lt;/strong&gt; gamification for community participation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doctor Brief&lt;/strong&gt; generation for clinic-ready summaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The guiding idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Help patients remember less alone, and help caregivers care less alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Lz3u-ljxhvA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Direct video link: &lt;a href="https://youtu.be/Lz3u-ljxhvA" rel="noopener noreferrer"&gt;https://youtu.be/Lz3u-ljxhvA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo shows the dashboard, Lumo Companion, local Gemma 4 reasoning, SafePath, CareCircle, CareLearn, trusted-person recall, reward wallet, and Doctor Brief generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ladiesmans217" rel="noopener noreferrer"&gt;
        ladiesmans217
      &lt;/a&gt; / &lt;a href="https://github.com/ladiesmans217/RememberMe-Caregrid" rel="noopener noreferrer"&gt;
        RememberMe-Caregrid
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;RememberMe CareGrid&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;AI memory, wandering safety, community care coordination, and Gemma 4-powered dementia training for Indian families.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RememberMe does not just track dementia patients. It gives them back context: who they met, where they are, what was said, and who can help.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Dementia care in Indian families is usually handled by one exhausted caregiver. Patients may forget trusted people, recent conversations, medicine routines, and safe routes. Existing tools often stop at GPS tracking or family-only reminders.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Solution&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;RememberMe CareGrid creates a consent-aware memory layer around a dementia patient. It recognizes enrolled trusted people, remembers meaningful conversations, maps safe places, alerts caregivers during wandering risk, coordinates community helpers, generates CareLearn training resources, and prepares doctor-ready summaries.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Community Impact&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;The product is built around three rings of care:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patient:&lt;/strong&gt; Lumo Companion, SmritiLens, memory cues, SafePath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Family:&lt;/strong&gt; caregiver dashboard, alerts, timeline, doctor brief.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community:&lt;/strong&gt; neighbour, ASHA worker, pharmacy partner, RWA volunteer, student…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ladiesmans217/RememberMe-Caregrid" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Repository: &lt;a href="https://github.com/ladiesmans217/RememberMe-Caregrid" rel="noopener noreferrer"&gt;https://github.com/ladiesmans217/RememberMe-Caregrid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few places I would point reviewers first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/lib/ai/gemma.ts&lt;/code&gt; - Gemma/Ollama reasoning wrapper&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/ai/ollama.ts&lt;/code&gt; - local Ollama helper and provider behavior&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/api/watch/talk/route.ts&lt;/code&gt; - watch-style text conversation route&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/api/watch/talk-audio/route.ts&lt;/code&gt; - local audio-to-care-response path&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/watch-talk-response.ts&lt;/code&gt; - structured response sanitization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/api/generate-doctor-report/route.ts&lt;/code&gt; - doctor brief generation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/app/api/carelearn/generate-training-card/route.ts&lt;/code&gt; - Gemma 4 CareLearn cards&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/components/app-shell.tsx&lt;/code&gt; - dashboard shell and care navigation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;Gemma 4 is not used as decoration here. It is the local reasoning layer behind the care workflows.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; because this use case needs local, privacy-conscious intelligence more than it needs the largest possible model. Dementia care involves sensitive context: confusion episodes, family relationships, trusted people, location, caregiver notes, and safety status. Sending every interaction to a remote model is not the architecture I wanted to demonstrate.&lt;/p&gt;

&lt;p&gt;The core pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;patient input
  -&amp;gt; local transcription or text
  -&amp;gt; Gemma 4 E2B via Ollama
  -&amp;gt; validated structured response
  -&amp;gt; dashboard / phone / watch-style output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemma 4 powers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patient-facing memory responses&lt;/li&gt;
&lt;li&gt;structured Lumo conversation output&lt;/li&gt;
&lt;li&gt;SafePath safety guidance&lt;/li&gt;
&lt;li&gt;caregiver summaries&lt;/li&gt;
&lt;li&gt;CareLearn training cards&lt;/li&gt;
&lt;li&gt;doctor-ready weekly briefs&lt;/li&gt;
&lt;li&gt;care reasoning and action recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, when the patient asks "Who am I?" or "Where am I?", Gemma 4 receives the care context and generates a short, calm response. When SafePath detects that the patient is outside the safe zone, Gemma 4 helps produce a reassuring safety cue instead of a cold alert. When CareLearn needs a neighbour-facing training card, Gemma 4 turns the situation into practical instructions.&lt;/p&gt;

&lt;p&gt;The model output is not blindly trusted. Patient-facing responses and actions go through structured JSON handling and sanitization because this is a safety-sensitive workflow. The app expects fields like reply, intent, risk level, action, and escalation state. If the model output is malformed, the app falls back to safer behavior instead of breaking the care loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Chose E2B
&lt;/h3&gt;

&lt;p&gt;I chose E2B intentionally. The patient-facing workflow needs short, calm, structured responses, not the largest possible model.&lt;/p&gt;

&lt;p&gt;A 31B Dense model would be useful for heavier reasoning, but it would make the local-first care loop slower and harder to run on modest hardware. E2B fits the product goal better: private local inference, fast enough responses, and enough reasoning ability for structured care actions.&lt;/p&gt;

&lt;p&gt;For this project, the best model was not the biggest model. It was the model that made local dementia-care assistance practical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Snippets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Local Gemma 4 through Ollama, forced into JSON mode
&lt;/h3&gt;

&lt;p&gt;Source: &lt;code&gt;src/lib/ai/ollama.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the core local model contract. The app calls Ollama locally, keeps Gemma warm, disables streaming for structured route responses, and asks the model for JSON so downstream patient actions can be validated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateJson&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;AiProviderMetadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OLLAMA_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/generate`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;keep_alive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OLLAMA_KEEP_ALIVE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;num_predict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;OLLAMA_TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;OllamaGenerateResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ollama returned an empty JSON response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extractJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;withMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;_provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ollama&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;_elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;withMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;_mock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;_provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ollama&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;_elapsed_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Watch audio becomes local transcription, then Gemma reasoning
&lt;/h3&gt;

&lt;p&gt;Source: &lt;code&gt;src/app/api/watch/talk-audio/route.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Gemma 4 is not treated as an audio transcription model. The watch audio is transcribed locally first, then the transcript is sent into the same Gemma-powered care reasoning path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processAudioTalk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AudioTalkInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ProgressWriter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio_received&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mime_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;asr_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getAsrConfig&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;transcribeLocalAudio&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimeType&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local_asr_failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local_asr_empty_transcript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;logAudioFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Local ASR failed for session &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;audioFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;summarizeAsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transcribed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;summarizeAsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thinking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thinking with local Gemma&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handleWatchTalkTranscript&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/watch/talk-audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inputMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;audio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;diagnostics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;audio_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;summarizeAsr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Gemma output is sanitized before it can affect the patient flow
&lt;/h3&gt;

&lt;p&gt;Source: &lt;code&gt;src/lib/watch-talk-response.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For a dementia-care workflow, the model cannot be allowed to return arbitrary action strings. This sanitizer constrains the reply, intent, risk level, action, and session-ending flag before anything is shown or executed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeWatchTalkResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WatchTalkResponse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;WatchTalkRouteResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cleanReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;risk_level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;riskLevels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk_level&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;should_end_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;should_end_session&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;should_end_session&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;should_end_session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;risk_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;should_end_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;oneOf&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ReadonlyArray&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. The same Gemma helper powers care features beyond chat
&lt;/h3&gt;

&lt;p&gt;Sources: &lt;code&gt;src/app/api/carelearn/generate-training-card/route.ts&lt;/code&gt; and &lt;code&gt;src/app/api/generate-doctor-report/route.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The same local Gemma helper is reused for two different care workflows: community training and doctor-ready summaries. This is the part that makes the project feel like a care system instead of a single chat screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CareLearn: role-specific dementia-care training cards&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;neighbour&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useCase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use_case&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wandering&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;event_context&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Rajamma left safe zone near MSRIT gate.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;trainingPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventContext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;mockTrainingCard&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Doctor Brief: clinic-ready caregiver summary&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mockDoctorReport&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;doctorReportPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doctor_report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;patient_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;patient_id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;DEMO_PATIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;generated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Consent Model, Not Just a Promise
&lt;/h2&gt;

&lt;p&gt;It is easy to say "this is not surveillance." The important part is designing the system so that statement is true.&lt;/p&gt;

&lt;p&gt;RememberMe CareGrid uses a consent-aware flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trusted people must be enrolled before they can be recognized&lt;/li&gt;
&lt;li&gt;unknown people are not labeled as identities&lt;/li&gt;
&lt;li&gt;unknown faces are not turned into memory profiles&lt;/li&gt;
&lt;li&gt;patient-facing capture flows ask for consent before saving names, photos, or transcripts&lt;/li&gt;
&lt;li&gt;helper tasks expose limited role-based information instead of full medical history&lt;/li&gt;
&lt;li&gt;safety alerts share location and risk context only when escalation is needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distinction matters. The phone relay can capture a frame, Firebase can sync care state, and Twilio can send SOS messages, but those tools are wrapped around a care workflow with enrollment, consent, retention, and role limits.&lt;/p&gt;

&lt;p&gt;The goal is not to watch the patient. The goal is to reduce the time between confusion and safe help.&lt;/p&gt;

&lt;h2&gt;
  
  
  UX Choices for Dementia Care
&lt;/h2&gt;

&lt;p&gt;I designed the AI responses around three rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer in one or two calm sentences&lt;/li&gt;
&lt;li&gt;never shame the patient for forgetting&lt;/li&gt;
&lt;li&gt;always give one safe next step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why Lumo does not behave like a normal chatbot. A long answer may be technically impressive, but it can overwhelm someone who is already confused.&lt;/p&gt;

&lt;p&gt;The same principle applies to community helpers. They receive limited role-based instructions, not the patient's full private history. The product is designed to make help easier without exposing more information than needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main Workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lumo Companion
&lt;/h3&gt;

&lt;p&gt;Lumo is the patient-facing companion. It gives short, respectful memory cues.&lt;/p&gt;

&lt;p&gt;The patient can ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Who am I?"&lt;/li&gt;
&lt;li&gt;"Where am I?"&lt;/li&gt;
&lt;li&gt;"Who is Ananya?"&lt;/li&gt;
&lt;li&gt;"Who is in front of me?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response is intentionally not a long chatbot answer. In dementia care, the best answer is often the one that reduces fear and helps the next safe action happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trusted-Person Recall
&lt;/h3&gt;

&lt;p&gt;A caregiver can enroll trusted people. The system stores consented trusted-person embeddings.&lt;/p&gt;

&lt;p&gt;If the patient cannot remember who is standing in front of them, the phone relay captures a frame and compares it only against enrolled trusted-person embeddings. If there is a match, the patient receives a gentle cue.&lt;/p&gt;

&lt;p&gt;Unknown people are not identified or stored. The system recognizes trusted people, not strangers.&lt;/p&gt;

&lt;h3&gt;
  
  
  SafePath
&lt;/h3&gt;

&lt;p&gt;SafePath checks whether the patient is inside or outside a safe zone.&lt;/p&gt;

&lt;p&gt;If the patient leaves the intended radius, the system creates a risk event and can trigger escalation with location context. The patient message remains calm, for example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are outside the safe zone. You are not alone. Please stay near a familiar place. Ananya is being contacted.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SafePath is not only a map. It is the part of the system that turns location into a care decision: reassure the patient, notify the caregiver, and give helpers enough context to respond safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  CareCircle
&lt;/h3&gt;

&lt;p&gt;CareCircle turns alerts into role-based tasks.&lt;/p&gt;

&lt;p&gt;The important design choice is that not everyone gets the same information. A caregiver may need a detailed event timeline. A neighbour may only need a simple request: "Rajamma may need help near the temple gate. Please contact Ananya." A community health worker may need visit context. A pharmacy partner may need refill-related context. A local safety volunteer may need an emergency protocol.&lt;/p&gt;

&lt;p&gt;This is where RememberMe CareGrid becomes more than a patient app. It becomes a coordination layer for trusted care.&lt;/p&gt;

&lt;h3&gt;
  
  
  CareLearn and Rewards
&lt;/h3&gt;

&lt;p&gt;CareLearn uses Gemma 4 to generate role-specific dementia-care training cards.&lt;/p&gt;

&lt;p&gt;The training is tied to real care moments. If a wandering event happens, the system can generate a card for a neighbour explaining how to approach calmly, what not to say, and when to notify the caregiver. If medication confusion happens, it can generate a different card for a pharmacy partner or caregiver.&lt;/p&gt;

&lt;p&gt;The reward wallet adds a small gamification layer. Helpers earn points for completing training and can redeem them in the demo for care-related rewards such as pharmacy support, safety kits, or caregiver respite vouchers.&lt;/p&gt;

&lt;p&gt;The point is not points for the sake of points. The point is to make community readiness visible and repeatable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doctor Brief
&lt;/h3&gt;

&lt;p&gt;Doctor Brief converts scattered care events into a clinic-ready summary.&lt;/p&gt;

&lt;p&gt;It can include medication adherence, wandering events, known visitors, memory journal topics, health snapshots, caregiver notes, community health worker notes, and suggested discussion points. The goal is to help a caregiver walk into a doctor visit with structured context instead of relying on memory during a stressful appointment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Architecture
&lt;/h2&gt;

&lt;p&gt;The architecture is split around responsibility: Gemma 4 reasons, local transcription handles speech, Firebase synchronizes care state, the Android relay handles camera capture, and the app validates every model response before it becomes a patient-facing action.&lt;/p&gt;

&lt;p&gt;The prototype uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js for the web dashboard&lt;/li&gt;
&lt;li&gt;Firebase for live care state, cues, GPS events, alerts, relay state, and training progress&lt;/li&gt;
&lt;li&gt;Android phone relay for trusted-person recall&lt;/li&gt;
&lt;li&gt;Gemma 4 E2B through Ollama for local reasoning&lt;/li&gt;
&lt;li&gt;local speech-to-text before Gemma reasoning&lt;/li&gt;
&lt;li&gt;face embeddings for consented trusted-person matching&lt;/li&gt;
&lt;li&gt;SafePath geofencing&lt;/li&gt;
&lt;li&gt;Twilio for SOS-style call and SMS workflows&lt;/li&gt;
&lt;li&gt;structured JSON validation for patient-facing responses and actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not only a UI mockup. The demo connects model reasoning, phone relay, live sync, geofence safety, community tasking, training, rewards, and doctor summaries into one care network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Hard
&lt;/h2&gt;

&lt;p&gt;The hardest part was not calling Gemma 4. The hard part was making local Gemma 4 behave like one component inside a safety-sensitive product.&lt;/p&gt;

&lt;p&gt;The first design problem was speech. A remote multimodal API can hide transcription and reasoning inside one call. Gemma 4 E2B in this local setup should not be treated as an audio transcription model. So I split the pipeline: local speech-to-text first, then Gemma reasoning over clean text and care context. That made the system more honest and easier to debug.&lt;/p&gt;

&lt;p&gt;The second problem was structure. A normal chatbot answer is not enough here. The watch flow needs to know whether the response is just an answer, a caregiver notification, a camera-frame request, or an end-session action. That is why the output is constrained and sanitized before the app uses it.&lt;/p&gt;

&lt;p&gt;The third problem was timing. In the trusted-person recall flow, the phone relay, dashboard state, and watch-style cue delivery all have to line up. A result that arrives too late is not useful to the patient. I had to treat latency and state sync as product problems, not just backend problems.&lt;/p&gt;

&lt;p&gt;The main lesson was that assistive AI is not only about model capability. It is about the boundary around the model: what context it receives, what it is allowed to output, and how the product behaves when something fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;The next version would replace the phone relay with a low-cost wearable camera module, improve offline deployment on mobile or wearable hardware, refine localized voice UX, expand caregiver-controlled consent settings, and validate the workflows with elder-care groups, clinics, and community health workers.&lt;/p&gt;

&lt;p&gt;I would also continue optimizing local Gemma 4 inference so the patient-facing experience feels faster on modest hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;RememberMe CareGrid is built around one belief:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Dementia care should not depend on one exhausted caregiver remembering everything alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gemma 4 makes it possible to build local, private, useful intelligence around people who need support in the moment.&lt;/p&gt;

&lt;p&gt;This prototype proved to me that local AI can be more than a private chatbot: it can become a careful coordination layer between a patient, a caregiver, and trusted helpers.&lt;/p&gt;

&lt;p&gt;RememberMe CareGrid helps patients remember less alone, and helps caregivers care less alone.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Google Cloud Next '26 Made One Thing Clear: Agents Need Infrastructure, Not Hype</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Thu, 30 Apr 2026 03:23:48 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/google-cloud-next-26-made-one-thing-clear-agents-need-infrastructure-not-hype-3i7f</link>
      <guid>https://dev.to/manjunathpatil/google-cloud-next-26-made-one-thing-clear-agents-need-infrastructure-not-hype-3i7f</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-cloud-next-2026-04-22"&gt;Google Cloud NEXT Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Google Cloud Next '26 had a very loud headline: the agentic enterprise is here.&lt;/p&gt;

&lt;p&gt;But the more interesting story, at least for developers, was quieter.&lt;/p&gt;

&lt;p&gt;It was not just "agents are smarter now." It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI agents are finally being treated like production software.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That may sound less exciting than a keynote demo, but I think it is the real shift. The serious part of the event was not that an agent can answer a question, generate a document, or call an API. We have seen enough demos like that.&lt;/p&gt;

&lt;p&gt;The serious part was Google saying, in many different ways, that agents now need infrastructure: runtime, identity, memory, observability, evaluation, access control, data grounding, secure sandboxes, and governance.&lt;/p&gt;

&lt;p&gt;In other words, the future of agents is not just about better models. It is about everything around the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The announcement that stood out
&lt;/h2&gt;

&lt;p&gt;The center of Google Cloud Next '26 was the new &lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/introducing-gemini-enterprise-agent-platform" rel="noopener noreferrer"&gt;Gemini Enterprise Agent Platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Google described it as the evolution of Vertex AI: a platform to build, scale, govern, and optimize agents. That framing matters because it moves the conversation away from "Can I build a cool AI demo?" and toward "Can I run thousands of agents safely inside a real organization?"&lt;/p&gt;

&lt;p&gt;Sundar Pichai's Next '26 post captured the shift well. He wrote that the question has moved from "Can we build an agent?" to "How do we manage thousands of them?" That one sentence explains most of the event.&lt;/p&gt;

&lt;p&gt;Google's answer is a stack with pieces like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent Development Kit, or ADK&lt;/li&gt;
&lt;li&gt;Agent Studio&lt;/li&gt;
&lt;li&gt;Agent Runtime&lt;/li&gt;
&lt;li&gt;Agent Registry&lt;/li&gt;
&lt;li&gt;Agent Identity&lt;/li&gt;
&lt;li&gt;Agent Gateway&lt;/li&gt;
&lt;li&gt;Agent Observability&lt;/li&gt;
&lt;li&gt;Agent Simulation&lt;/li&gt;
&lt;li&gt;Agent Evaluation&lt;/li&gt;
&lt;li&gt;Memory Bank&lt;/li&gt;
&lt;li&gt;Agent Sessions&lt;/li&gt;
&lt;li&gt;Model Armor&lt;/li&gt;
&lt;li&gt;Google Cloud MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a lot of product names. But under the naming, there is a real architecture forming.&lt;/p&gt;

&lt;h2&gt;
  
  
  The developer keynote made it real
&lt;/h2&gt;

&lt;p&gt;The opening keynote gave the vision. The developer keynote made it practical.&lt;/p&gt;

&lt;p&gt;The demo was built around planning a marathon in Las Vegas. That sounds like a toy problem until you think about what it actually requires: route planning, constraints, simulation, safety, logistics, evaluation, and constant iteration.&lt;/p&gt;

&lt;p&gt;The system used multiple specialized agents instead of one giant "do everything" agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A planner agent proposed marathon routes.&lt;/li&gt;
&lt;li&gt;An evaluator agent checked those routes against requirements.&lt;/li&gt;
&lt;li&gt;A simulator agent modeled the impact on the city.&lt;/li&gt;
&lt;li&gt;A supply chain agent handled logistics like water stations, medical tents, and portable toilets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the architecture pattern I found most useful: not one magical assistant, but a network of smaller agents with clear jobs.&lt;/p&gt;

&lt;p&gt;The developer keynote showed agents using ADK, MCP, Agent Runtime, Agent Registry, A2A-style agent communication, A2UI-style user interfaces, Memory Bank, runtime traces, Cloud Assist, Cloud Run, GKE, Agent Identity, and Agent Gateway.&lt;/p&gt;

&lt;p&gt;That is much closer to real software engineering than the usual "I typed a prompt and magic happened" demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden message: agents need boring systems
&lt;/h2&gt;

&lt;p&gt;The most underrated part of Next '26 was how much of the agent story was boring in the best possible way.&lt;/p&gt;

&lt;p&gt;Boring like logs.&lt;/p&gt;

&lt;p&gt;Boring like identity.&lt;/p&gt;

&lt;p&gt;Boring like access control.&lt;/p&gt;

&lt;p&gt;Boring like knowing what changed, who changed it, what tool got called, what data was accessed, and why the agent made a bad decision.&lt;/p&gt;

&lt;p&gt;That is what makes software production-grade.&lt;/p&gt;

&lt;p&gt;A chatbot can be loose. A production agent cannot.&lt;/p&gt;

&lt;p&gt;If an agent can read documents, write code, trigger workflows, browse internal systems, call another agent, or act on customer data, then it needs the same discipline we expect from any other production system.&lt;/p&gt;

&lt;p&gt;That is why features like Agent Identity and Agent Gateway matter. Agent Identity gives agents a trackable identity. Agent Gateway becomes a control point for agent-to-agent and agent-to-tool traffic. Agent Registry gives organizations a way to know what agents and tools exist. Agent Observability gives developers traces and logs for debugging.&lt;/p&gt;

&lt;p&gt;This is not glamorous, but it is the difference between "cool demo" and "I would trust this in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  What people seem excited about
&lt;/h2&gt;

&lt;p&gt;After reading the official announcements and community reactions, the excitement is mostly around practical developer workflows.&lt;/p&gt;

&lt;p&gt;ADK is a big one. Developers want a way to build multi-agent systems without inventing the architecture from scratch. Google's graph-based ADK direction is interesting because it acknowledges that agent workflows need structure. Some tasks can be generative, but some need deterministic paths, especially in compliance, security, finance, healthcare, and operations.&lt;/p&gt;

&lt;p&gt;MCP is another major theme. Google is exposing cloud services through Model Context Protocol and also announced an official &lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/level-up-your-agents-announcing-googles-official-skills-repository" rel="noopener noreferrer"&gt;Agent Skills repository&lt;/a&gt;. I like this because it tackles a problem developers already feel: context bloat. Giving an agent the entire internet, all docs, and every internal page is not a strategy. Smaller, task-specific skills are a cleaner way to give agents expertise only when needed.&lt;/p&gt;

&lt;p&gt;Cloud Run updates also caught attention. Developers care about things like managed MCP servers, long-running background agents, sandboxing, SSH support, service bindings, serverless GPUs, and billing caps. Billing caps may not sound like a keynote-worthy feature, but for developers worried about surprise cloud bills, that can be more exciting than another model benchmark.&lt;/p&gt;

&lt;p&gt;There was also interest in the codelabs. Google said there were 55+ new codelabs across Cloud at Next, with labs covering ADK + A2UI, multi-agent systems, secure agents, Google Maps grounding, Agent Engine deployment, Cloud Run, and agent skills. That matters because developers do not just need a keynote. They need something they can run after the keynote ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people are worried about
&lt;/h2&gt;

&lt;p&gt;The concern I saw again and again is AI fatigue.&lt;/p&gt;

&lt;p&gt;Some developers are asking whether Google Cloud Next is becoming "Gemini Next." On Reddit, one person joked about trying to find a session without AI and failing. Another pre-event thread asked what people expected besides "Agentic AI spam."&lt;/p&gt;

&lt;p&gt;That frustration is fair.&lt;/p&gt;

&lt;p&gt;A lot of production teams still care about IAM, networking, Kubernetes, databases, cost controls, observability, migrations, and reliability. If every topic gets wrapped in "agentic AI" language, it can start to feel like the practical infrastructure concerns are being painted over with marketing.&lt;/p&gt;

&lt;p&gt;But I think the best version of Google's Next '26 story actually answers that criticism.&lt;/p&gt;

&lt;p&gt;The useful announcements were not "AI will fix everything." The useful announcements were about the infrastructure agents need when they stop being demos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GKE Agent Sandbox for isolated execution&lt;/li&gt;
&lt;li&gt;Agent Runtime for deployment&lt;/li&gt;
&lt;li&gt;Agent Gateway for governed traffic&lt;/li&gt;
&lt;li&gt;Agent Identity for traceable permissions&lt;/li&gt;
&lt;li&gt;Agent Observability for debugging&lt;/li&gt;
&lt;li&gt;Agent Evaluation for quality checks&lt;/li&gt;
&lt;li&gt;Model Armor for prompt injection and data leakage protection&lt;/li&gt;
&lt;li&gt;Knowledge Catalog for trusted business context&lt;/li&gt;
&lt;li&gt;Cloud Run billing caps for cost safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not just AI features. They are operational features.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data story may be more important than the model story
&lt;/h2&gt;

&lt;p&gt;Google also announced the &lt;a href="https://cloud.google.com/blog/products/data-analytics/whats-new-in-the-agentic-data-cloud" rel="noopener noreferrer"&gt;Agentic Data Cloud&lt;/a&gt;, and I think this might be one of the most important parts of the event.&lt;/p&gt;

&lt;p&gt;Agents are only useful if they understand the business context around the task.&lt;/p&gt;

&lt;p&gt;A generic model may understand the word "margin," but inside a company, "margin" might depend on team-specific definitions, regional rules, product lines, internal dashboards, and messy historical decisions. If an agent does not understand that context, it will confidently do the wrong thing.&lt;/p&gt;

&lt;p&gt;That is why Knowledge Catalog, Cross-Cloud Lakehouse, BigQuery measures, LookML Agent, Data Agent Kit, and Conversational Analytics matter. They are not just data products. They are attempts to make enterprise context usable by agents.&lt;/p&gt;

&lt;p&gt;This is where the agentic enterprise either becomes real or falls apart.&lt;/p&gt;

&lt;p&gt;Without trusted context, agents hallucinate.&lt;/p&gt;

&lt;p&gt;Without permissions, agents leak data.&lt;/p&gt;

&lt;p&gt;Without observability, agents become impossible to debug.&lt;/p&gt;

&lt;p&gt;Without evaluation, agents drift.&lt;/p&gt;

&lt;p&gt;Without cost controls, agents become expensive experiments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security is not optional anymore
&lt;/h2&gt;

&lt;p&gt;The security announcements also stood out. Google is combining Google Threat Intelligence, Security Operations, and Wiz into what it calls Agentic Defense.&lt;/p&gt;

&lt;p&gt;The important idea here is that agents create new attack surfaces.&lt;/p&gt;

&lt;p&gt;If agents can use tools, attackers will try to poison tools.&lt;/p&gt;

&lt;p&gt;If agents can read data, attackers will try to extract data.&lt;/p&gt;

&lt;p&gt;If agents can call other agents, attackers will try to exploit the chain.&lt;/p&gt;

&lt;p&gt;If agents can execute code, attackers will try to turn them into execution paths.&lt;/p&gt;

&lt;p&gt;This is why Agent Identity, Agent Gateway, Model Armor, Wiz, threat detection agents, detection engineering agents, and runtime security all belong in the same conversation as ADK.&lt;/p&gt;

&lt;p&gt;Security cannot be a final checklist after the agent is built. It has to be part of the agent architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  My honest take
&lt;/h2&gt;

&lt;p&gt;I am excited, but cautiously.&lt;/p&gt;

&lt;p&gt;I like that Google is treating agents as systems, not toys. The developer keynote was strongest when it showed the full lifecycle: build, test, remember, debug, deploy, scale, and secure.&lt;/p&gt;

&lt;p&gt;That is the right framing.&lt;/p&gt;

&lt;p&gt;But I also think the industry needs to be careful. "Agentic" is becoming a word that gets attached to everything. Not every workflow needs an autonomous agent. Some need a form. Some need a queue. Some need a cron job. Some need a dashboard. Some need better documentation.&lt;/p&gt;

&lt;p&gt;The best agent systems will not be the ones that automate the most. They will be the ones that know when to act, when to ask, when to stop, and when to hand control back to a human.&lt;/p&gt;

&lt;p&gt;That is why the boring parts matter so much.&lt;/p&gt;

&lt;p&gt;The future is not one giant AI agent running a company. The future is probably many small, specialized agents operating inside strict boundaries, with humans still setting intent, reviewing important decisions, and owning the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Google Cloud Next '26 made one thing clear to me:&lt;/p&gt;

&lt;p&gt;The agent era will not be won by the flashiest chatbot.&lt;/p&gt;

&lt;p&gt;It will be won by the platform that makes agents observable, governable, secure, grounded in real data, and boring enough to trust.&lt;/p&gt;

&lt;p&gt;That is a less dramatic story than "AI will do everything."&lt;/p&gt;

&lt;p&gt;But for developers, it is a much better one.&lt;/p&gt;

&lt;p&gt;Because if agents are going to become part of real software, they need to behave like real software.&lt;/p&gt;

&lt;p&gt;And real software needs infrastructure.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cloudnextchallenge</category>
      <category>googlecloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Earth Day Study Companion: For Teach-Ins and Climate Learning</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 20 Apr 2026 06:54:44 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/earth-day-study-companion-turning-climate-research-into-teach-ins-learning-pathways-and-29jh</link>
      <guid>https://dev.to/manjunathpatil/earth-day-study-companion-turning-climate-research-into-teach-ins-learning-pathways-and-29jh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-04-16"&gt;Weekend Challenge: Earth Day Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Earth Day Study Companion&lt;/strong&gt;, a climate learning workspace designed to help people move from reading, to understanding, to teaching, to action.&lt;/p&gt;

&lt;p&gt;The idea came from a simple problem. A lot of Earth Day and climate learning happens in fragments. Someone reads a long PDF report, watches a few videos, collects some slides, saves a few articles, and then tries to turn all of that into a class, a club session, a workshop, or a community discussion. The information exists, but the workflow is messy. Research is disconnected from teaching. Planning is disconnected from delivery. Good material is often trapped inside dense documents that are hard to search and even harder to teach from.&lt;/p&gt;

&lt;p&gt;I wanted to build a product that makes that process feel connected from start to finish.&lt;/p&gt;

&lt;p&gt;Earth Day Study Companion has three core experiences: &lt;strong&gt;Climate Library&lt;/strong&gt;, &lt;strong&gt;Teach-In Builder&lt;/strong&gt;, and &lt;strong&gt;Teach-In Facilitator&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Climate Library&lt;/strong&gt; is the research layer. Users can upload climate reports, sustainability guides, Earth Day toolkits, policy PDFs, and other learning material. The system stores the documents, processes them, chunks them into smaller pieces, creates embeddings, and uses retrieval before answering questions. That means the experience is not just a generic chatbot on top of a file upload. It is a grounded document assistant built on top of retrieval augmented generation. Users can ask questions about what is inside the document, open the relevant page, and jump back to the exact source material that informed the answer. This is especially useful for long climate documents where the user needs clarity, context, and source confidence.&lt;/p&gt;

&lt;p&gt;The library also becomes more useful because it is connected to live multimodal interaction. A user can turn on the microphone and ask questions naturally. They can also share the screen and ask if a chart, image, article, PDF section, or slide is worth using. This matters in real Earth Day preparation because people do not only work with text. They work with visual material, reports, and presentation content. The screen share helps Gemini reason over what is visible, so the user can ask practical questions like whether a page is relevant, whether a graph is clear enough, or whether a specific visual should be included in a session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teach-In Builder&lt;/strong&gt; is the structure layer. Once the user knows the topic they want to teach, the Builder turns that topic into a learning pathway. Instead of producing only a plain outline, the system creates both a pathway view and a mind map. The pathway helps with order and progression. The mind map helps with relationships and scope. This makes it easier to take a broad subject like renewable energy, climate justice, biodiversity, food systems, or circular economy and turn it into something teachable.&lt;/p&gt;

&lt;p&gt;Each generated module can then be expanded into &lt;strong&gt;Guide&lt;/strong&gt;, &lt;strong&gt;Practice Lab&lt;/strong&gt;, and &lt;strong&gt;Field Media&lt;/strong&gt;. The Guide is for explanation and learning flow. Practice Lab helps turn passive reading into active thinking. Field Media connects the topic to supporting examples and related material. I wanted the Builder to feel like a real educational workspace, not a one shot prompt output. The result is something that can help a student explore a topic, but it can also help a facilitator or organizer design a real Earth Day learning session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teach-In Facilitator&lt;/strong&gt; is the planning layer. This is where the project moves from learning support into real event preparation. The user starts by filling in a session brief with the audience, venue or context, title, duration, goals, focus areas, and available materials. From there, Gemini acts like a live planning partner. The user can talk through the idea naturally, refine the session structure, and decide how the event should feel for that specific audience.&lt;/p&gt;

&lt;p&gt;This is also where the live controls become very important. The microphone supports a natural planning conversation. Screen share allows the facilitator to show slides, PDFs, webpages, images, and other planning material while asking Gemini for feedback. The webcam adds live visual context during the planning session. In practice, this makes the product feel much closer to a real coaching partner than a standard prompt box. A user can ask if a resource looks relevant, if a visual seems clear, or if a piece of material fits the tone of the session they are planning.&lt;/p&gt;

&lt;p&gt;As the session develops, the app turns that planning process into a structured output with a summary, learning objectives, agenda, materials, and community actions. At the end, the system generates a facilitator report as a downloadable PDF. That final step is important because it turns a live planning session into something reusable. The user leaves with a practical artifact they can actually use for a school club, a local workshop, a library event, or a community Earth Day gathering.&lt;/p&gt;

&lt;p&gt;My intended goal with this project was to build something that feels genuinely useful for Earth Day. Not just a climate themed UI, not just an AI wrapper, and not just a static educational demo. I wanted to build a tool that helps people understand climate material, turn it into a teaching structure, and prepare a real session around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Live demo: &lt;a href="https://earthdaycompanion.vercel.app/" rel="noopener noreferrer"&gt;https://earthdaycompanion.vercel.app/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Video demo: &lt;a href="https://youtu.be/V-v_MHj3HJw" rel="noopener noreferrer"&gt;https://youtu.be/V-v_MHj3HJw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the demo, I walk through the product as a full learning and planning flow.&lt;/p&gt;

&lt;p&gt;I start inside Climate Library by uploading a climate related PDF and asking questions about it. This shows how the system indexes the document, retrieves relevant chunks, and answers in a way that stays tied to the uploaded material. I also show page level navigation and jumping back to the relevant part of the PDF, because that is one of the most important parts of the library experience. I wanted viewers to see that the system is not guessing. It is actually working with the document.&lt;/p&gt;

&lt;p&gt;Then I move into Teach-In Builder and generate a structured pathway on an Earth Day topic. I show the pathway view and the mind map view, then open a module to show how the Guide, Practice Lab, and Field Media sections work. This part demonstrates how the product turns a broad environmental topic into a teachable sequence instead of only summarizing it.&lt;/p&gt;

&lt;p&gt;Finally, I open Teach-In Facilitator and show how the app can be used as a live planning partner for a real Earth Day session. I walk through the brief, start the live flow, and show how Gemini helps shape the structure of the teach-in. I also show how screen sharing can be used to review materials visually during the planning process. At the end, I generate the facilitator report PDF to show how the live planning flow becomes something concrete and reusable.&lt;/p&gt;
&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;p&gt;This is the landing experience, where the product introduces the three connected flows: Climate Library, Teach-In Builder, and Teach-In Facilitator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eexbq4t3qexh4xwmltb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eexbq4t3qexh4xwmltb.png" alt="HOMEPAGE" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Climate Library, where uploaded PDFs become grounded, searchable learning material.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfddswuyturinmo12m2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flfddswuyturinmo12m2b.png" alt="CLIMATE LIBRARY" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Teach-In Builder, where a broad Earth Day topic becomes a pathway and a mind map.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuujqm81n2mdc6yjlgqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyuujqm81n2mdc6yjlgqo.png" alt="TEACH IN BUILDER" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the Teach-In Facilitator, where Gemini helps shape a real session and generate a usable report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8o5fyiwty89kewrw7l4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi8o5fyiwty89kewrw7l4.png" alt="TEACH IN FACILITATOR" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;GitHub repository: &lt;a href="https://github.com/ladiesmans217/Earth-Day-Challenge" rel="noopener noreferrer"&gt;https://github.com/ladiesmans217/Earth-Day-Challenge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is built with a React and TypeScript frontend and a Python backend. The frontend handles the user experience across the three main flows, while the backend handles document processing, retrieval, generation, and report output.&lt;/p&gt;

&lt;p&gt;On the document side, the backend stores uploaded PDFs, chunks the content, creates embeddings, and uses ChromaDB for retrieval. On the live interaction side, Gemini powers the voice based multimodal experience. On the planning side, Gemini function calling is used to create a structured teach-in plan. On the output side, the backend generates a facilitator report PDF so the planning session ends with a practical result.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Snippets
&lt;/h3&gt;

&lt;p&gt;This snippet shows how PDF pages are chunked and indexed into ChromaDB for grounded retrieval.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PDF not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_text_from_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to extract text from PDF&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;added_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;metadatas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page_num&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunk_idx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk_idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;}]&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;added_count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chunks_added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;added_count&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet shows part of the Climate Library viewer logic, including the local PDF worker and the in-memory PDF cache used to keep the viewer responsive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pdfjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlobalWorkerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workerSrc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PUBLIC_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/pdf.worker.min.js`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;CachedPdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;objectUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;totalPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pageTextByPage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;pdfDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pdfCacheRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CachedPdf&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getPdfCacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;normalizePdfName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet shows the default Teach-In Facilitator brief and how the experience starts with a strong session setup instead of a blank form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DEFAULT_TEACH_IN_BRIEF&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TeachInBrief&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sessionTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Neighborhood Clean Energy Teach-In&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;High-school students, parents, and community volunteers curious about practical local climate action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;venueContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Public library community room with projector and open discussion seating&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;45 minutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Help attendees understand clean energy in everyday life, connect it to climate justice, and leave with two realistic actions they can take this month.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;focusAreas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Renewable energy basics, energy bills, climate justice, neighborhood resilience, community action&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;materials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Projector, whiteboard, local utility bill example, sticky notes, Earth Day handout, signup sheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I built the product around a three step model: study, structure, and facilitate.&lt;/p&gt;

&lt;p&gt;For the study layer, the main focus was grounding. Climate material is often long, technical, and dense, so I did not want a system that simply accepted a PDF and then answered in a vague way. When a user uploads a document into Climate Library, the backend stores the file, extracts the text, splits it into chunks, creates embeddings, and stores them in ChromaDB. When the user asks a question, the backend retrieves the most relevant chunks and passes them into the model as context. That creates a proper retrieval augmented generation flow instead of a general chat flow. It also allows the app to support citations, page navigation, and highlighted source jumps back into the document.&lt;/p&gt;

&lt;p&gt;That source connection was important to me because climate literacy is not only about getting an answer. It is about trusting where the answer came from. If someone is preparing an Earth Day session, a school lesson, or a community discussion, they need to be able to go back to the original material and verify what they are using.&lt;/p&gt;

&lt;p&gt;For the structure layer, I wanted to go beyond a single generated course outline. That is why Teach-In Builder creates both a pathway and a mind map. Those two views do slightly different jobs. The pathway helps the user think in sequence, while the mind map helps the user think in connections. Once a module is opened, the system expands it into Guide, Practice Lab, and Field Media so the topic becomes something a person can actually work through and teach from. This part of the build was about making generated content feel usable, not just impressive.&lt;/p&gt;

&lt;p&gt;For the facilitate layer, I adapted the live assistant flow into a planning companion for real Earth Day events. The user starts with a structured brief, then moves into a live Gemini session where the focus is on audience fit, session flow, materials, and next steps. Function calling is used to turn that planning flow into a structured output with learning objectives, agenda, materials, and community actions. I wanted this to feel like an event planning tool, not a generic live chat demo.&lt;/p&gt;

&lt;p&gt;Gemini is the key technology across the whole project. I used it for live voice interaction, multimodal context, structured teach-in planning, and document assistance when paired with retrieval. In Climate Library, Gemini helps turn indexed PDFs into an interactive learning experience. In Teach-In Builder, it supports turning broad topics into structured educational pathways. In Teach-In Facilitator, it helps shape a real Earth Day session that can actually be delivered to an audience.&lt;/p&gt;

&lt;p&gt;The shared live control tray also became a meaningful part of the product. The microphone supports a more natural planning and exploration flow. Screen share makes the assistant useful for real materials, not just typed prompts. Webcam adds live visual context to the session. Together, those controls make the app feel more like a working multimodal study and facilitation environment.&lt;/p&gt;

&lt;p&gt;I also spent time on the interface direction because I did not want the project to feel like a generic AI dashboard. I moved away from a loud or overly synthetic look and shaped it into something more like an editorial field guide for climate learning. The goal was to make the experience feel grounded, readable, and specific to Earth Day rather than looking like a general purpose AI tool with green branding.&lt;/p&gt;

&lt;p&gt;The biggest thing I learned while building this was that climate education is a strong and practical Earth Day direction. Many projects in this space focus only on tracking or visualization. Those are useful, but I wanted to build around the human part of environmental action: understanding the material, organizing it, and helping other people learn from it. That is where I think this project is strongest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I am submitting this project for Best Use of Google Gemini.&lt;/p&gt;

&lt;p&gt;Gemini is central to the project, not an extra layer added on top. It powers the live voice interaction, the multimodal reasoning over shared visual context, the structured teach-in planning flow, and the grounded assistance in the document workflow when combined with retrieval.&lt;/p&gt;

&lt;p&gt;This is a solo submission.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>earthday</category>
      <category>gemini</category>
    </item>
    <item>
      <title>TerraRun — A Territory Capture Running App Where Every Loop You Run Becomes Your Turf</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 02 Mar 2026 07:58:17 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/terrarun-a-territory-capture-running-app-where-every-loop-you-run-becomes-your-turf-ie</link>
      <guid>https://dev.to/manjunathpatil/terrarun-a-territory-capture-running-app-where-every-loop-you-run-becomes-your-turf-ie</guid>
      <description>&lt;p&gt;I'm part of the running community — people who lace up every morning, track every mile, and obsess over their Strava stats. Running apps today are incredible at what they do. Strava tracks your routes. Nike Run Club coaches your training. MapMyRun logs your history.&lt;/p&gt;

&lt;p&gt;But here's the gap I kept noticing: &lt;strong&gt;once you finish a run, what do you actually own?&lt;/strong&gt; A line on a map and some numbers. There's no lasting mark on the world. No reason to go back to the same streets, no incentive to explore new ones, and no connection between your physical effort and the neighborhoods you run through every day.&lt;/p&gt;

&lt;p&gt;Runners already feel ownership over their routes — "that's MY morning loop" — but nothing on the map reflects that. I wanted to change that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;TerraRun&lt;/strong&gt; — a territory capture running app where every closed loop you run becomes your turf on the map.&lt;/p&gt;

&lt;p&gt;The concept: go for a run, close a loop (your route connects back to where you started), and the enclosed area fills in with your color on a shared public map. That's your territory now. Other runners can reclaim it by running the same loop. The more territory you hold, the higher you rank.&lt;/p&gt;

&lt;p&gt;Think of it as &lt;strong&gt;Strava meets king-of-the-hill&lt;/strong&gt; — your runs aren't just logged, they're &lt;em&gt;staked&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Territory Capture&lt;/strong&gt; — Run a closed loop, and the exact shape of your route becomes claimed territory on the map. Not a grid, not circles — the actual polygon of your run, just like Strava route art.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Run Simulation&lt;/strong&gt; — A "Watch Demo Run" button animates a runner tracing an irregular street-level route in real-time. When the loop closes, the territory fills in with a capture animation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share Card&lt;/strong&gt; — After capturing territory, a share modal pops up with your route shape rendered as a graphic, plus your stats (distance, time, pace, area). One tap to share on WhatsApp, Telegram, X, or copy the link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive Map&lt;/strong&gt; — Full-screen Mapbox map with 4 styles (Dark, Streets, Satellite, Outdoors), 3D toggle, territory click popups showing owner and capture date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboard&lt;/strong&gt; — Runners ranked by territory held. Top 3 podium. Filters for All Time, This Week, Nearby.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile Dashboard&lt;/strong&gt; — Your stats, achievement badges, active streak, and activity feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsor Zones&lt;/strong&gt; — Brands (Nike, Adidas, Gatorade, Under Armour) place zones on the map. Run through them to unlock real coupon codes. Runners get rewarded. Brands get literal foot traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What makes this different from existing apps:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Strava&lt;/th&gt;
&lt;th&gt;Nike Run Club&lt;/th&gt;
&lt;th&gt;TerraRun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tracks runs&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social competition&lt;/td&gt;
&lt;td&gt;Segments only&lt;/td&gt;
&lt;td&gt;Challenges&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Territory wars&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Map ownership&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your runs claim real map area&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reason to re-run a route&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Defend your territory&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brand engagement&lt;/td&gt;
&lt;td&gt;Ads&lt;/td&gt;
&lt;td&gt;Ads&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Physical sponsor zones you run to&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://youtu.be/Cr7n3qfzARM" rel="noopener noreferrer"&gt;YOUTUBE VIDEO LINK&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Cr7n3qfzARM"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live deployment:&lt;/strong&gt; &lt;a href="https://terrarun-dev.vercel.app/" rel="noopener noreferrer"&gt;TerraRun on Vercel&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/manjunath5513" rel="noopener noreferrer"&gt;
        manjunath5513
      &lt;/a&gt; / &lt;a href="https://github.com/manjunath5513/Dev-Challenge" rel="noopener noreferrer"&gt;
        Dev-Challenge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TerraRun — Run. Loop. Conquer.&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A territory capture running app where every closed loop you run becomes your turf on the map. Built for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28" rel="nofollow"&gt;DEV Weekend Challenge&lt;/a&gt; (Feb 27 – Mar 2, 2026).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://terrarun-dev.vercel.app/" rel="nofollow noopener noreferrer"&gt;terrarun-dev.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video Demo:&lt;/strong&gt; &lt;a href="https://youtu.be/Cr7n3qfzARM" rel="nofollow noopener noreferrer"&gt;youtu.be/Cr7n3qfzARM&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Idea&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Running apps like Strava and Nike Run Club are great at tracking — but once a run is done, all you have is a line on a map. TerraRun changes that. Run a closed loop, and the enclosed area fills in as your territory. Others can reclaim it by running the same loop. Every neighborhood becomes a game board.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Territory Capture&lt;/strong&gt; — Closed-loop runs become filled polygon territories on the map, matching the exact shape of your route&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Run Simulation&lt;/strong&gt; — Animated demo run traces an irregular street-level path, captures territory on loop close&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share Card&lt;/strong&gt; — Post-capture modal with route shape graphic, stats (distance, time…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/manjunath5513/Dev-Challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;Project structure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terrarun/
├── src/
│   ├── app/                    # Next.js App Router pages
│   │   ├── page.tsx            # Landing page
│   │   ├── map/page.tsx        # Main interactive map
│   │   ├── leaderboard/page.tsx
│   │   ├── profile/page.tsx
│   │   └── sponsors/page.tsx
│   ├── components/
│   │   ├── map/                # MapView, TerritoryLayer, RunSimulation, ShareModal
│   │   ├── landing/            # Hero, Features, HowItWorks
│   │   ├── layout/             # Navbar with mobile bottom nav
│   │   ├── leaderboard/        # LeaderboardTable
│   │   └── profile/            # ProfileStats
│   ├── lib/
│   │   ├── mock-data.ts        # 6 users, 7 territories, 4 sponsors
│   │   ├── territory.ts        # Polygon territory generation with Turf.js
│   │   ├── h3-utils.ts         # H3 hex utilities (available for future use)
│   │   └── constants.ts        # Map config, styles, colors
│   ├── store/
│   │   └── useMapStore.ts      # Zustand state management
│   └── types/
│       └── index.ts            # TypeScript interfaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Next.js 16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Framework — App Router, SSR, file-based routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React 19&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UI rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TypeScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Type safety across the entire codebase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tailwind CSS 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Styling — dark theme, glass morphism effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mapbox GL JS 3 + react-map-gl 8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interactive map rendering with 4 style modes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Turf.js 7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Geospatial calculations — polygon area from GPS coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framer Motion 12&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Animations — page transitions, capture celebrations, share modal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zustand 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lightweight state management for map and simulation state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lucide React&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Icon system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How the territory system works
&lt;/h3&gt;

&lt;p&gt;The core mechanic: a runner's GPS route forms a closed polygon. When the loop closes, I use &lt;strong&gt;Turf.js&lt;/strong&gt; to calculate the enclosed area in km², and render it as a filled GeoJSON polygon on the Mapbox map. The territory is the &lt;em&gt;exact shape&lt;/em&gt; of the run — no grids, no approximations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Convert route points to a territory polygon&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routePoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;turfPolygon&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;areaKm2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;turfArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each territory stores: polygon coordinates, owner, color, area, capture date. Territories are rendered as GeoJSON with a 3-layer glow border effect (outer blur + mid blur + core line) for that neon territory look.&lt;/p&gt;

&lt;h3&gt;
  
  
  The run simulation
&lt;/h3&gt;

&lt;p&gt;The demo run isn't a perfect circle — it's an &lt;strong&gt;irregular street-level route&lt;/strong&gt; that zigzags through Central Park paths, because that's how real runs look. The animation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Runner marker moves along the route at running pace&lt;/li&gt;
&lt;li&gt;A trail polyline draws behind in real-time&lt;/li&gt;
&lt;li&gt;When the loop closes → territory fill animation + capture stats&lt;/li&gt;
&lt;li&gt;Share card appears with the route shape, stats, and social share buttons&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Design decisions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polygons over hex grids&lt;/strong&gt; — I initially built with H3 hexagons but realized runners follow streets, not grids. Polygon fills matching the actual run shape feel natural and look like the Strava route screenshots people already share.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark theme default&lt;/strong&gt; — Territory colors glow best on dark backgrounds. The glass morphism UI keeps it clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile-first&lt;/strong&gt; — Bottom navigation bar, touch-friendly controls, responsive everything. Runners use phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share-first capture flow&lt;/strong&gt; — The share modal appears immediately after capture because the screenshot moment is what drives virality in running apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Built in a weekend
&lt;/h3&gt;

&lt;p&gt;This was built from scratch during the DEV Weekend Challenge window. The entire app — 5 pages, interactive map, run simulation, territory system, share flow — was designed, coded, and deployed within the challenge timeframe.&lt;/p&gt;

&lt;p&gt;AI was used as a development tool throughout the process.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Run. Loop. Conquer.&lt;/em&gt; 🏃&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built a GeoGuessr for Languages — Here's How I Made It Speak 8 Languages Overnight</title>
      <dc:creator>Manjunath Patil</dc:creator>
      <pubDate>Mon, 23 Feb 2026 18:29:06 +0000</pubDate>
      <link>https://dev.to/manjunathpatil/i-built-a-geoguessr-for-languages-heres-how-i-made-it-speak-8-languages-overnight-4h1d</link>
      <guid>https://dev.to/manjunathpatil/i-built-a-geoguessr-for-languages-heres-how-i-made-it-speak-8-languages-overnight-4h1d</guid>
      <description>&lt;p&gt;I've sunk embarrassing hours into GeoGuessr. There's something deeply satisfying about squinting at a road sign in Cyrillic, spotting a right-hand-drive car, and triumphantly dropping a pin somewhere in rural Bulgaria. One evening, while half-listening to a Turkish podcast I didn't understand, it clicked — &lt;em&gt;what if the clue wasn't a photo of a street, but the sound of a language?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Think about it. You hear someone speaking. The rhythm, the vowels, the melody of the sentence. Can you tell Japanese from Korean? Portuguese from Spanish? Hindi from Urdu? That question became &lt;strong&gt;LinguaGuessr&lt;/strong&gt; — a game where you listen to a language, pin its origin on a world map, and get scored by how close you land.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, the tech decisions that shaped it, and how I made the entire UI speak 8 languages overnight — using &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: language learning is boring, and i18n is painful
&lt;/h2&gt;

&lt;p&gt;Let's be honest — most language learning apps are glorified flashcard decks. Duolingo gamified vocabulary drills, but the core loop is still &lt;em&gt;memorize → recall → repeat&lt;/em&gt;. GeoGuessr proved that geography can become a game people play for fun, not obligation. Why hasn't anyone done that for linguistics?&lt;/p&gt;

&lt;p&gt;I wanted to build something where you &lt;em&gt;experience&lt;/em&gt; languages rather than study them. Hear a clip, feel the rhythm, take a guess, learn a fun fact. No textbooks, no streaks, no guilt.&lt;/p&gt;

&lt;p&gt;But there was a second problem lurking underneath: if you're building a game &lt;em&gt;about&lt;/em&gt; languages for a global audience, the UI itself needs to speak the player's language. And anyone who's shipped i18n knows the pain — JSON key files, missing translations, string interpolation bugs, a whole parallel codebase just for text. Building a game is hard enough. Making it multilingual felt like signing up for two projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is LinguaGuessr?
&lt;/h2&gt;

&lt;p&gt;The game loop is dead simple — three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Listen&lt;/strong&gt; — Hear a clip of someone speaking a mystery language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin&lt;/strong&gt; — Click anywhere on the world map to place your guess&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — The closer your pin to the language's true origin, the more points you earn (max 5,000 per round)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzokgf35ngtzakn4gpgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvzokgf35ngtzakn4gpgn.png" alt=" " width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;125+ languages&lt;/strong&gt; in the database — from the obvious (English, Spanish, Mandarin) to the obscure (Basque, Yoruba, Guarani, Corsican). Each language comes with geographic coordinates, a difficulty rating, and a fun cultural fact that shows up after you guess.&lt;/p&gt;

&lt;p&gt;The game supports &lt;strong&gt;solo mode&lt;/strong&gt; with a global leaderboard, and &lt;strong&gt;multiplayer mode&lt;/strong&gt; where you create a room, share a code, and compete in real time. Five rounds per game, max 25,000 points, and bragging rights for whoever knows their Amharic from their Tigrinya.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Here's what powers LinguaGuessr under the hood:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 16 (App Router)&lt;/td&gt;
&lt;td&gt;Server components, API routes, fast builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Styling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tailwind CSS v4&lt;/td&gt;
&lt;td&gt;Utility-first, dark theme, zero CSS files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leaflet + react-leaflet&lt;/td&gt;
&lt;td&gt;Open-source, lightweight, great mobile support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Realtime&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supabase (Postgres + Realtime)&lt;/td&gt;
&lt;td&gt;Room codes, presence, broadcast — all in one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;i18n&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lingo.dev (Compiler + SDK + CLI + CI/CD)&lt;/td&gt;
&lt;td&gt;Build-time JSX translation, runtime dynamic translation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Audio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Web Speech API&lt;/td&gt;
&lt;td&gt;Zero-cost TTS with native browser voices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Type safety across the entire stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every choice was deliberate — I wanted a stack that could ship fast, scale to multiplayer, and handle i18n without a separate translation infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The game mechanics — under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Audio: a three-tier fallback
&lt;/h3&gt;

&lt;p&gt;Audio is the core mechanic. If the player can't hear the language, there's no game. So I built a three-tier fallback system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;MP3 files&lt;/strong&gt; — Pre-recorded clips for supported languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Speech API&lt;/strong&gt; — Browser-native text-to-speech as a fallback (free, zero-cost, surprisingly good)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text display&lt;/strong&gt; — If both fail, show the phrase on screen and let the player guess from the script
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Try MP3 first, fall back to Web Speech API&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setUseWebSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;playWithWebSpeech&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;audioRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setUseWebSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;playWithWebSpeech&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;playWithWebSpeech&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Web Speech API is underrated. Every modern browser ships with dozens of language voices. It's not perfect — some voices sound robotic, and coverage varies by OS — but for a game where you just need to &lt;em&gt;hear&lt;/em&gt; the language, it's more than enough. And the price is right: free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Map: Leaflet with custom pins
&lt;/h3&gt;

&lt;p&gt;The map uses Leaflet with OpenStreetMap tiles. When a player clicks, a gradient pin drops at their guess location. After scoring, a dashed line draws from their guess to the correct location, giving immediate visual feedback on how close (or far off) they were.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0boyxbelnxpnd4nl6x2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo0boyxbelnxpnd4nl6x2.png" alt=" " width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring: Haversine formula + exponential decay
&lt;/h3&gt;

&lt;p&gt;The scoring uses the Haversine formula to calculate the great-circle distance between the player's guess and the language's true origin. Then an exponential decay curve converts distance to points:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;haversineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lng2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6371&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Earth's radius in km&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dLat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dLng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lng2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lng1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dLat&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toRad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dLng&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Perfect score&lt;/span&gt;
  &lt;span class="c1"&gt;// Exponential decay: forgiving but steep&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;distanceKm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The curve is deliberately forgiving — you don't need to nail the exact country. Within 200km is a perfect 5,000. At 2,000km you still get ~2,500. But by 10,000km you're down to ~50 points. It rewards knowledge without punishing reasonable guesses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multiplayer with Supabase Realtime
&lt;/h2&gt;

&lt;p&gt;I wanted multiplayer from day one. The idea of friends arguing about whether that clip was Finnish or Estonian is too good to skip.&lt;/p&gt;

&lt;p&gt;Supabase Realtime made this surprisingly simple. The entire multiplayer system runs on &lt;strong&gt;presence tracking&lt;/strong&gt; plus &lt;strong&gt;four broadcast events&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;presence:sync&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Track who's in the room&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;game_start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Host starts the game, sends the language list to all players&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;guess_submitted&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A player submits their guess and score&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;next_round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Advance everyone to the next round&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;game_finished&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show final rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the core channel setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`room:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;presence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;channel&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;presence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;presenceState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// Update player list from presence state&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;game_start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLanguages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guess_submitted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Update scoreboard with player's round score&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next_round&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setCurrentRound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;playing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;broadcast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;game_finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setPhase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;finished&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwp8x6edfxhmfnumah5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwp8x6edfxhmfnumah5w.png" alt=" " width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire multiplayer flow — lobby, gameplay sync, scoreboard — is handled by these events. No custom WebSocket server, no socket.io, no polling. And if Supabase is unavailable? The game gracefully degrades to solo mode with in-memory scores.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making it multilingual with Lingo.dev
&lt;/h2&gt;

&lt;p&gt;Here's where it gets fun. I'm building a game &lt;em&gt;about&lt;/em&gt; languages. The irony of shipping it in English-only was not lost on me. But I also knew from past projects that i18n is a time sink — extracting strings into JSON files, maintaining translation keys, wiring up a provider, hoping nothing breaks when a new string shows up.&lt;/p&gt;

&lt;p&gt;Then I found &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev&lt;/a&gt;, and it changed my whole approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compiler: auto-translate JSX at build time
&lt;/h3&gt;

&lt;p&gt;The Lingo.dev Compiler wraps your Next.js build and automatically translates all JSX text content. No string extraction. No JSON key files for your UI text. You write your components in English, and the compiler handles the rest.&lt;/p&gt;

&lt;p&gt;The setup is minimal — just wrap your Next.js config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withLingo&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@lingo.dev/compiler/next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unoptimized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NextConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withLingo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetLocales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lingo.dev&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Seven target languages. Every &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; in my React components now gets translated at build time into Spanish, French, German, Japanese, Hindi, Arabic, and Portuguese. No &lt;code&gt;t("key")&lt;/code&gt; calls. No &lt;code&gt;intl.formatMessage&lt;/code&gt;. Just write English and ship globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SDK: runtime translation for dynamic content
&lt;/h3&gt;

&lt;p&gt;Static UI text is only half the story. LinguaGuessr has dynamic content — fun facts about each language that come from the database. These can't be translated at build time because they're loaded at runtime. The Lingo.dev SDK handles this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LingoDotDevEngine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lingo.dev/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LingoDotDevEngine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINGODODEV_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;localizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;funFact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;targetLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when a Japanese-speaking player finishes a round, the fun fact about Basque being a language isolate shows up in Japanese. The static UI was already in Japanese from the compiler. The dynamic content gets translated on-the-fly by the SDK. The player sees a fully localized experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD: auto-translate on every push
&lt;/h3&gt;

&lt;p&gt;For static locale files (language names, country names, error messages), I use the Lingo.dev CLI paired with a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/translate.yml&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/locales/en.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;i18n.json'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;translate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Lingo.dev CLI&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx lingo.dev@latest i18n&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;LINGODODEV_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.LINGODODEV_API_KEY }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Commit translations&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git add src/locales/&lt;/span&gt;
          &lt;span class="s"&gt;git diff --staged --quiet || git commit -m "chore: update translations"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time I update the English source strings and push to main, the Action translates everything and commits the results. No manual translation step, no stale translations, no forgotten locales.&lt;/p&gt;

&lt;h3&gt;
  
  
  The language switcher UX
&lt;/h3&gt;

&lt;p&gt;On the frontend, switching languages is instant. The &lt;code&gt;useLingoContext&lt;/code&gt; hook from Lingo's React integration provides &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;setLocale&lt;/code&gt;. A dropdown in the navbar lets you pick any of the 8 languages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLingoContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// When user picks a language&lt;/span&gt;
&lt;span class="nf"&gt;setLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ja&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Switches entire UI to Japanese&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also built a custom toast notification that shows a brief "Translating to Japanese..." message with a spinner when switching — it gives the player feedback that something is happening, even though the switch is nearly instant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyt7pv5ryry9f1p2yove.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyt7pv5ryry9f1p2yove.png" alt=" " width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bugs I hit (and how I fixed them)
&lt;/h2&gt;

&lt;p&gt;No project is complete without war stories. Here are the ones that cost me the most time.&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG icons turning into gibberish
&lt;/h3&gt;

&lt;p&gt;After enabling the Lingo.dev Compiler, I noticed my SVG icons were broken. The globe icon in the navbar was rendering as literal text: &lt;em&gt;"SVG zero, polygon zero..."&lt;/em&gt; The compiler was treating SVG attributes like &lt;code&gt;viewBox&lt;/code&gt; and &lt;code&gt;strokeLinecap&lt;/code&gt; as translatable text and mangling them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; Lingo.dev provides a &lt;code&gt;data-lingo-skip&lt;/code&gt; attribute. Slap it on any element you don't want translated. I went through every SVG in the codebase and added it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;data-lingo-skip&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"16"&lt;/span&gt; &lt;span class="na"&gt;viewBox&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;
  &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;circle&lt;/span&gt; &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"M2 12h20"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This became a pattern — every decorative SVG, every icon, every non-text element gets &lt;code&gt;data-lingo-skip&lt;/code&gt;. It's a small thing, but missing even one SVG can break a whole page.&lt;/p&gt;

&lt;h3&gt;
  
  
  No loading feedback on language switch
&lt;/h3&gt;

&lt;p&gt;The first time someone switched languages, nothing visually happened for a beat. The UI just... changed. Users thought it was broken. I built the &lt;code&gt;TranslationToast&lt;/code&gt; component — a small notification that slides in from the bottom-right with a spinner and auto-dismisses after 3 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;fixed&lt;/span&gt; &lt;span class="na"&gt;bottom-6&lt;/span&gt; &lt;span class="na"&gt;right-6&lt;/span&gt; &lt;span class="na"&gt;z-&lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="na"&gt;100&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt; &lt;span class="na"&gt;flex&lt;/span&gt; &lt;span class="na"&gt;items-center&lt;/span&gt; &lt;span class="na"&gt;gap-3&lt;/span&gt;
  &lt;span class="na"&gt;rounded-xl&lt;/span&gt; &lt;span class="na"&gt;border&lt;/span&gt; &lt;span class="na"&gt;border-border&lt;/span&gt; &lt;span class="na"&gt;bg-surface&lt;/span&gt; &lt;span class="na"&gt;px-4&lt;/span&gt; &lt;span class="na"&gt;py-3&lt;/span&gt; &lt;span class="na"&gt;shadow-2xl&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;data-lingo-skip&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h-4 w-4 animate-spin text-accent"&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Translating to &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;languageName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small touch, big UX difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dev widget stuck on screen
&lt;/h3&gt;

&lt;p&gt;Lingo.dev ships a developer widget that overlays your app in development — useful for debugging translations, but it kept showing up in production screenshots. The fix was a one-liner in the provider config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;devWidget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The lesson
&lt;/h3&gt;

&lt;p&gt;Compiler-based i18n tools are powerful. They eliminate the drudgery of string extraction and key management. But you need to tell them what &lt;em&gt;not&lt;/em&gt; to translate. SVGs, code blocks, brand names, technical terms — anything that shouldn't be localized needs an explicit skip marker. Once I internalized that pattern, the rest was smooth.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Compiler-based i18n is a different paradigm.&lt;/strong&gt; Traditional i18n (react-intl, next-intl, i18next) is key-based: extract every string, assign a key, look it up at runtime. Lingo.dev's compiler approach inverts this — you write natural JSX, and translation happens at the build layer. It's faster to set up, easier to maintain, and eliminates an entire category of "forgot to extract this string" bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Supabase Realtime is underrated for quick multiplayer.&lt;/strong&gt; I expected to need a dedicated WebSocket server. Instead, four broadcast events and presence tracking gave me a complete multiplayer system. The channel API is clean, the latency is low, and the free tier is generous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Speech API is a zero-cost audio solution.&lt;/strong&gt; It's not studio quality, but for a game where the point is to &lt;em&gt;identify&lt;/em&gt; a language, it's perfect. Dozens of language voices, built into every modern browser, no API keys, no usage fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building for the world from day one changes how you think about UX.&lt;/strong&gt; When you know your UI will be in Arabic (right-to-left!) and Japanese (longer text strings!), you design differently. Buttons need flexible widths. Text can't be hardcoded into fixed layouts. It's a constraint that makes you a better designer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;LinguaGuessr is live and free to play.&lt;/p&gt;

&lt;p&gt;Live demo URL: &lt;a href="https://linguaguessr.vercel.app/" rel="noopener noreferrer"&gt;https://linguaguessr.vercel.app/&lt;/a&gt;&lt;br&gt;
GitHub repo URL : &lt;a href="https://github.com/Manjunath3155/linguaguessr" rel="noopener noreferrer"&gt;https://github.com/Manjunath3155/linguaguessr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick a language. Drop a pin. See how close you get.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you speak one of the 125+ languages in the database and catch a wrong coordinate or a bad fun fact, open an issue — the whole point is making this better together.&lt;/p&gt;

&lt;p&gt;Built for the &lt;a href="https://lingo.dev" rel="noopener noreferrer"&gt;Lingo.dev Hackathon&lt;/a&gt;. If you're building anything multilingual, seriously check out their compiler. It turned what I expected to be a week of i18n plumbing into an evening of configuration.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by Manjunath Patil with Next.js, Supabase, Leaflet, and Lingo.dev.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
