<?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: Pratik</title>
    <description>The latest articles on DEV Community by Pratik (@prdxdotexe).</description>
    <link>https://dev.to/prdxdotexe</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4011118%2F5a294d4a-d463-485e-8ed1-04fbd6f42e55.png</url>
      <title>DEV Community: Pratik</title>
      <link>https://dev.to/prdxdotexe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prdxdotexe"/>
    <language>en</language>
    <item>
      <title>A CRM where the AI never leaves your laptop</title>
      <dc:creator>Pratik</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:06:59 +0000</pubDate>
      <link>https://dev.to/prdxdotexe/a-crm-where-the-ai-never-leaves-your-laptop-nl</link>
      <guid>https://dev.to/prdxdotexe/a-crm-where-the-ai-never-leaves-your-laptop-nl</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Private AI Lead Assistant&lt;/strong&gt; — a small CRM where you paste a client conversation and a local model turns it into a structured lead.&lt;/p&gt;

&lt;p&gt;A freelancer's WhatsApp or email thread with a prospect is already a CRM record in prose: who they are, what they want built, the budget, the deadline. Extracting it is tedious, and every cloud "AI CRM" solves it by uploading the transcript. This solves it on your own machine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Paste the conversation → &lt;strong&gt;Analyze&lt;/strong&gt; with a local model&lt;/li&gt;
&lt;li&gt;Model returns a validated JSON lead: client, company, project, budget, priority, timeline, status, tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; step — nothing is saved until you approve it (a 1.5B model is confidently wrong sometimes, so a human is the last gate)&lt;/li&gt;
&lt;li&gt;Open the lead → draft a &lt;strong&gt;follow-up message&lt;/strong&gt; or a full &lt;strong&gt;project proposal&lt;/strong&gt;, then copy it out&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Who would pay for it:&lt;/strong&gt; freelancers and 2–10 person agencies. Client conversations contain budgets, deadlines and NDA'd product plans; per-seat AI CRM pricing is hard to justify when you invoice by the project. Running extraction locally makes the marginal cost $0 — which is the difference between a side hustle's tooling and a SaaS bill.&lt;/p&gt;

&lt;p&gt;Stack: React 19 + Tailwind, Express, Postgres, TypeScript, and &lt;strong&gt;Qwen 2.5 (1.5B, open weights) served by Ollama&lt;/strong&gt; — no external AI API anywhere in the codebase.&lt;/p&gt;

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

&lt;p&gt;The app runs on localhost by design — the point is that the conversation never leaves the laptop, so there is no hosted demo to link. &amp;lt;!-- TODO: drop a Loom/YouTube walkthrough URL in here before publishing --&amp;gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run it:&lt;/strong&gt; &lt;code&gt;npm install &amp;amp;&amp;amp; npm run db:migrate &amp;amp;&amp;amp; npm run dev&lt;/code&gt; (with Ollama up and &lt;code&gt;ollama pull qwen2.5:1.5b&lt;/code&gt;) → client on &lt;code&gt;:5173&lt;/code&gt;, API on &lt;code&gt;:3001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What a full session looks like (real run against &lt;code&gt;qwen2.5:1.5b&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register → land on the dashboard&lt;/li&gt;
&lt;li&gt;Paste a chat where a restaurant owner in Kathmandu asks for an online ordering system&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/ai/analyze&lt;/code&gt; → ~36s cold → extraction: &lt;strong&gt;Sita Gurung · ABC Restaurant · "online ordering system" · 40,000–60,000 NPR · high priority&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Review, approve → lead appears in the list, searchable by &lt;code&gt;q=ABC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open lead → &lt;strong&gt;Generate follow-up&lt;/strong&gt; (~26s) → &lt;strong&gt;Generate proposal&lt;/strong&gt; (~26s)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /api/health&lt;/code&gt; → &lt;code&gt;{"status":"ok","db":"up","ai":"up","model":"qwen2.5:1.5b","provider":"ollama"}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Three pieces that make a small local model trustworthy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Constrained decoding + determinism.&lt;/strong&gt; Extraction is a structured prediction, so the request carries a JSON Schema (&lt;code&gt;format&lt;/code&gt;) and fixed sampling — same conversation, same lead:&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="cm"&gt;/** Deterministic extraction: temperature 0 + fixed seed + explicit context budget. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EXTRACTION_OPTIONS&lt;/span&gt; &lt;span class="o"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;numCtx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;numPredict&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="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;analyzeConversation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AiProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;conversation&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;raw&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;callAi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildLeadExtractionPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LEAD_EXTRACTION_SYSTEM&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="nx"&gt;leadExtractionJsonSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Ollama constrained decoding&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;EXTRACTION_OPTIONS&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;parseLeadExtraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;&lt;strong&gt;2. Never trust the model's JSON.&lt;/strong&gt; LLM output crosses a Zod boundary, or the request fails with a 502 the UI can show:&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="cm"&gt;/** LLM -&amp;gt; JSON -&amp;gt; Zod. Rejects with a useful 502 instead of trusting the model. */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;leadExtractionSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&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="nx"&gt;issues&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;issue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(root)&lt;/span&gt;&lt;span class="dl"&gt;'&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;issue&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="s2"&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;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`AI returned data that failed validation: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;details&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;3. Prompt injection is a real risk with paste-anything input.&lt;/strong&gt; I measured the 1.5B model failing to resist injections in the eval harness, so protection is layered: the system prompt treats conversation text as untrusted data, a detector flags instruction-shaped text &lt;em&gt;before&lt;/em&gt; it reaches the model, and the review UI warns you:&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;INJECTION_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="sr"&gt;/ignore&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;all&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+|any&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?(?:&lt;/span&gt;&lt;span class="sr"&gt;previous|prior|earlier|above|preceding&lt;/span&gt;&lt;span class="se"&gt;)\s&lt;/span&gt;&lt;span class="sr"&gt;+instructions&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="sr"&gt;system&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*system&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&amp;gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/pretend&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;to&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+be|you&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+are&lt;/span&gt;&lt;span class="se"&gt;)\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;an&lt;/span&gt;&lt;span class="se"&gt;?\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)?(?:&lt;/span&gt;&lt;span class="sr"&gt;system|developer|assistant&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&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;detectPromptInjection&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="kr"&gt;string&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;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &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;pattern&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;INJECTION_PATTERNS&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;pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&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="nx"&gt;PROMPT_INJECTION_WARNING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight model, local inference:&lt;/strong&gt; Qwen 2.5 1.5B through &lt;strong&gt;Ollama&lt;/strong&gt; — pulled once, works offline, zero API keys. The 1.5B size is deliberate: it runs on a laptop CPU and keeps the whole thing free, which is what makes it a &lt;em&gt;side hustle&lt;/em&gt; tool rather than another subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider abstraction:&lt;/strong&gt; &lt;code&gt;AiProvider&lt;/code&gt; (&lt;code&gt;health()&lt;/code&gt; / &lt;code&gt;generate(prompt, options)&lt;/code&gt;) with &lt;code&gt;format: 'text' | 'json' | object&lt;/code&gt; for constrained decoding. Swapping to llama.cpp or a bigger Qwen is one class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different sampling per task:&lt;/strong&gt; extraction and proposals run at &lt;code&gt;temperature: 0, seed: 42&lt;/code&gt;; follow-ups run at &lt;code&gt;0.7&lt;/code&gt; so regenerating gives you a different email instead of the same one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 29-conversation eval harness&lt;/strong&gt; (&lt;code&gt;npm run ai:eval -w server&lt;/code&gt;) that prints Expected / Actual / Pass-Fail per case — this is how I found the model's habits, e.g. defaulting &lt;code&gt;priority: "medium"&lt;/code&gt; when the chat never mentions urgency, which is exactly why the review step exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Around it:&lt;/strong&gt; React 19 + Tailwind (dark mode, toasts, skeletons, keyboard shortcuts), Express with Zod validation, bcrypt auth, rate limits, allow-list CORS, Drizzle on Postgres with &lt;code&gt;ilike&lt;/code&gt; search, 109 tests, and a Docker Compose dev stack (React + Express + Postgres, Ollama as an optional profile).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting the containers talking took the usual compose lesson — &lt;code&gt;localhost&lt;/code&gt; inside a container is the container, so the Vite dev proxy switches to the compose service name:&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;proxy&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="s1"&gt;/api&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="nl"&gt;target&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;VITE_API_PROXY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changeOrigin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(I got this from &lt;a href="https://dev.to/omar_lashin_2badc08a1ddf9/the-localhost-trap-networking-microservices-in-docker-compose-k04"&gt;The 'Localhost' Trap: Networking Microservices in Docker Compose&lt;/a&gt; by Omar Lashin, and the whole local-first framing echoes &lt;a href="https://dev.to/scriptgather/how-i-built-a-zero-api-cost-ai-agent-that-runs-100-locally-no-keys-no-cloud-no-bills-3bi2"&gt;How I Built a Zero-API-Cost AI Agent That Runs 100% Locally — No Keys, No Cloud, No Bills&lt;/a&gt; by scriptgather.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project it isn't a preference — it's the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data can't leave.&lt;/strong&gt; Client budgets and NDA'd plans are the input. A closed API means every paste is a disclosure; local inference means the transcript stays on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0 marginal cost.&lt;/strong&gt; The business case for a freelancer is "extract leads for free forever," not "another $20/token-metered seat."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in or pricing risk.&lt;/strong&gt; The model is a file you &lt;code&gt;ollama pull&lt;/code&gt;. If Qwen's license, pricing, or availability changed tomorrow, the pipeline wouldn't notice — and the &lt;code&gt;AiProvider&lt;/code&gt; interface means the vendor layer was never load-bearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open weights make the limitations debuggable.&lt;/strong&gt; I could evaluate the 1.5B model against 29 realistic conversations and &lt;em&gt;see&lt;/em&gt; where it's weak (prompt injection, unstated priority), then engineer around it with a review gate and a detector. With a closed API you only get vibes — and when their model version changes under you, your prompts silently break.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Room to fine-tune for the niche.&lt;/strong&gt; The extraction schema is domain-specific (budget ranges in local currency, project tags). With open weights, a few hundred in-domain examples can specialize it — something a general API handles poorly and you can't tune at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;The full build session — verifying Phases 17–24 of the PRD, writing the Docker stack, the dark-mode sweep, the live end-to-end run against the local model, and drafting this post:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Finishing the Private AI Lead Assistant (PRD Phases 17–24) + Hacktoberfest submission post&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Continue the PRD for private-ai-lead-assistant — Phases 17–24: search/filter, error handling, testing, security, Docker dev environment, UI polish, documentation, and the final MVP demo.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Verifying Phases 17–20 first — the search/filter code, error middleware, test suites, and security measures looked already implemented. Confirming before writing anything new.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-grepsearch"&gt;grep_search&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;listLeads q status priority followUp&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;listLeads q status priority followUp&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;lead.service.ts:40 — plain ilike search over client/company/project; LeadsList.tsx has selects + chips&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npm test (server + client)&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npm test (server + client)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;92 server tests + 17 client tests pass&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Phases 17–20 confirmed complete. Building Phase 21: Docker dev environment — root multi-stage Dockerfile, compose stack (db/server/client + optional ollama profile), and a VITE_API_PROXY so the Vite dev proxy targets the compose service name instead of localhost.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;Dockerfile, .dockerignore, docker-compose.yml&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Dockerfile, .dockerignore, docker-compose.yml&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;node:22-alpine multi-stage (base/server/client), npm ci, migrate-then-dev on server, profiles: ["ollama"] with host.docker.internal:host-gateway&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;client/vite.config.ts — proxy target&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;vite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&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;VITE_API_PROXY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Phase 22 (UI polish): dark mode with a pre-paint inline script in index.html to avoid a flash of wrong theme, Toast/Skeleton/ConfirmDialog/Shortcuts components, centralized light/dark class sets in lib/ui.ts, then a full sweep adding dark: variants across all 15 UI files (grep-verified zero missing pairings).&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-grepsearch"&gt;grep_search&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;dark: pairing sweep across client/src&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;dark: pairing sweep across client/src&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;0 missing dark: variants&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Phase 23 (docs): README rewritten with all 14 required sections + a Browser → Express → AI Service → Ollama → Qwen diagram and an API appendix; AGENTS.md phase list corrected. Phase 24 (demo): running the full flow live against the real local model.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;register → /api/ai/analyze → create lead → search → follow-up → proposal (cur...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ register → /api/ai/analyze → create lead → search → follow-up → proposal (curl, cookie jar)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;qwen2.5:1.5b extracted Sita Gurung / ABC Restaurant / online ordering system / 40,000–60,000 NPR; analyze 36s cold, follow-up 26s, proposal 26s; health: provider=ollama&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Final verification: typecheck clean, 17 client + 92 server tests pass, production build succeeds (355 kB js / 29 kB css). Docker stack authored and YAML-validated but not executed — no Docker daemon in this WSL distro.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;What did we do so far?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Delivered a per-phase status report (17–24, with which were pre-existing vs newly built), verification results, caveats, and closed with the Community Wisdom citations that shaped the Docker networking and dark-mode work.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Write the Hacktoberfest Open-Source AI Challenge Week 2 (Side Hustle) submission post using the challenge template.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Grounded the draft with a semantic search on DEV for local/zero-API-cost AI builds, checked the challenge's full_details (deadline Oct 18 11:59 PM PDT, required tag #hf26challenge), verified tag existence, and staged an unpublished draft: "A CRM where the AI never leaves your laptop" with the template sections filled, two honest TODO markers (video link, repo embed), and the My First User / Prize Categories sections removed per template instructions.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-createarticle"&gt;create_article&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;title: A CRM where the AI never leaves your laptop; tags: hf26challenge, ai, ...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;title: A CRM where the AI never leaves your laptop; tags: hf26challenge, ai, opensource, privacy; published: false&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;draft id 4860930, edit_url returned&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;yes&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Saving this session transcript, embedding it in the draft's My Agent Session section, then publishing.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        12 of 10 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Bato — Plan at home. Hike offline. Phone in your pocket. 🥾</title>
      <dc:creator>Pratik</dc:creator>
      <pubDate>Thu, 08 Oct 2026 15:26:30 +0000</pubDate>
      <link>https://dev.to/prdxdotexe/bato-plan-at-home-hike-offline-phone-in-your-pocket-4g2i</link>
      <guid>https://dev.to/prdxdotexe/bato-plan-at-home-hike-offline-phone-in-your-pocket-4g2i</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Bato is an offline-first trail companion PWA.&lt;/p&gt;

&lt;p&gt;Upload a GPX file, enter your start time and fitness level, and Bato generates a simple one-page hiking plan with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distance and elevation gain/loss&lt;/li&gt;
&lt;li&gt;Highest point and elevation profile&lt;/li&gt;
&lt;li&gt;Estimated hiking time&lt;/li&gt;
&lt;li&gt;Sunrise and sunset&lt;/li&gt;
&lt;li&gt;Latest safe turnaround time&lt;/li&gt;
&lt;li&gt;Pacing, rest, water, and safety guidance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple: &lt;strong&gt;plan at home, then put your phone away and hike.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/PRDXdotEXE/Bato" rel="noopener noreferrer"&gt;https://github.com/PRDXdotEXE/Bato&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://baato-beige.vercel.app/" rel="noopener noreferrer"&gt;https://baato-beige.vercel.app/&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/PRDXdotEXE/Bato" rel="noopener noreferrer"&gt;https://github.com/PRDXdotEXE/Bato&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Bato is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js + TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;WebLLM&lt;/li&gt;
&lt;li&gt;Qwen 1.5B Instruct, quantized&lt;/li&gt;
&lt;li&gt;SunCalc&lt;/li&gt;
&lt;li&gt;IndexedDB&lt;/li&gt;
&lt;li&gt;Service Worker / PWA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GPX is parsed entirely in the browser. Distance, elevation, hiking time, sunrise/sunset, and turnaround time are calculated using deterministic TypeScript.&lt;/p&gt;

&lt;p&gt;The local LLM &lt;strong&gt;does not calculate these values&lt;/strong&gt;. It only turns the verified numbers into a short, human-readable hiking plan.&lt;/p&gt;

&lt;p&gt;If the model fails to load or the device is too weak, Bato falls back to a deterministic template, so the app always produces a plan.&lt;/p&gt;

&lt;p&gt;There is no backend or runtime API dependency. After the initial model download, Bato can work offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Using an open-weight model makes local inference possible.&lt;/p&gt;

&lt;p&gt;Instead of sending trail data to a remote AI API:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPX → Local calculations → Local LLM → Hiking plan&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything stays on the device.&lt;/p&gt;

&lt;p&gt;This makes Bato more suitable for hiking, where internet connectivity may be unavailable, while also giving the project freedom to swap and experiment with different open-weight models.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Open-Weight / Open-Source AI&lt;/li&gt;
&lt;li&gt;Local / On-Device AI&lt;/li&gt;
&lt;li&gt;Touch Grass / Offline-first&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Recipe-maker</title>
      <dc:creator>Pratik</dc:creator>
      <pubDate>Sun, 04 Oct 2026 08:24:56 +0000</pubDate>
      <link>https://dev.to/prdxdotexe/recipe-maker-51ki</link>
      <guid>https://dev.to/prdxdotexe/recipe-maker-51ki</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&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;Family Recipe Memory&lt;/strong&gt; for a friend whose family cooks almost entirely from memory.&lt;/p&gt;

&lt;p&gt;Her grandmother and parents never write recipes down or measure much. They explain them out loud: &lt;em&gt;add some of this, cook it until it smells right, you'll know when it's done.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Those little details are the actual recipe, and they're very easy to lose. Right now, a lot of them live in voice notes buried somewhere in a chat app, often in a mix of Nepali and English, with no practical way to search them.&lt;/p&gt;

&lt;p&gt;So the app lets her just talk.&lt;/p&gt;

&lt;p&gt;She uploads a voice note, and it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transcribes it on her own machine&lt;/li&gt;
&lt;li&gt;Lets her fix any misheard words&lt;/li&gt;
&lt;li&gt;Turns it into a clean recipe with ingredients, steps, tips, and the family story&lt;/li&gt;
&lt;li&gt;Saves it to a small recipe library&lt;/li&gt;
&lt;li&gt;Lets her ask things like &lt;em&gt;"What did Grandma say about making momo?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule I cared about: &lt;strong&gt;if something wasn't said in the recording, the app leaves it blank instead of guessing.&lt;/strong&gt; I didn't want a model quietly rewriting someone's grandmother's recipe.&lt;/p&gt;

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

&lt;p&gt;[&lt;a href="https://www.youtube.com/watch?v=x8TJe7HyIew" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=x8TJe7HyIew&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upload voice note → Transcribe → Review transcript → Extract and save → Browse or ask questions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each recipe is shown as a card with the story, an ingredients table, numbered steps, and tips. Recipes can also be downloaded as Markdown.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://dev.tourl"&gt;&lt;/a&gt;&lt;br&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/PRDXdotEXE" rel="noopener noreferrer"&gt;
        PRDXdotEXE
      &lt;/a&gt; / &lt;a href="https://github.com/PRDXdotEXE/recipe-maker" rel="noopener noreferrer"&gt;
        recipe-maker
      &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;🍲 Family Recipe Memory&lt;/h1&gt;

&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Preserve voice recipes and stories from loved ones using Local AI + Backboard Persistent Memory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Problem&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Family culinary heritage is often trapped in unwritten spoken traditions. Audio notes from grandmothers or parents contain precious nuances, but searching through raw voice recordings is frustrating and inefficient.&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;&lt;strong&gt;Family Recipe Memory&lt;/strong&gt; turns unstructured voice notes into a searchable family knowledge base. Audio transcription and AI structured parsing run &lt;strong&gt;100% locally&lt;/strong&gt;, ensuring raw voice recordings never leave your machine. Only extracted text is persisted in Backboard memory for cross-session natural language retrieval.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture &amp;amp; Privacy Model&lt;/h2&gt;

&lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/PRDXdotEXE/recipe-maker" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;Everything runs locally.&lt;/p&gt;

&lt;p&gt;The stack is Python and Streamlit, with two open models doing the heavy lifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI Whisper&lt;/strong&gt; for speech-to-text. The voice notes mix Nepali and English, and the smallest Whisper model gets shaky on that, so the app defaults to &lt;code&gt;small&lt;/code&gt;, lets you switch to &lt;code&gt;medium&lt;/code&gt;, and lets you pick the spoken language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; for everything language-related. I tested with &lt;code&gt;llama3.2&lt;/code&gt;, &lt;code&gt;gemma2&lt;/code&gt;, and &lt;code&gt;qwen2.5&lt;/code&gt;, and the app lists whichever models are installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The extraction step was the part that took the most care.&lt;/p&gt;

&lt;p&gt;The transcript goes to the model with strict instructions: only use what was said, keep dish and ingredient names as spoken, and leave anything that wasn't mentioned empty.&lt;/p&gt;

&lt;p&gt;The output is JSON, which I then clean up, because small local models love to return a quantity as an object or write &lt;code&gt;"N/A"&lt;/code&gt; instead of leaving a field empty.&lt;/p&gt;

&lt;p&gt;Two design choices came directly from using it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A review step between transcription and extraction.&lt;/strong&gt; Speech-to-text can mishear a Nepali word now and then, and one wrong word can ruin an ingredient. Letting my friend fix the transcript first made the recipes noticeably better.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memory is just a local JSON file.&lt;/strong&gt; Recipes are saved to &lt;code&gt;data/recipes.json&lt;/code&gt;. For questions, the app passes the most relevant saved recipes to the local model and tells it to answer only from them, and to say so if the answer isn't there.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also had a bug early on where the results disappeared the moment I clicked the download button, because Streamlit reruns the script on every click. Moving the state into &lt;code&gt;st.session_state&lt;/code&gt; fixed it.&lt;/p&gt;

&lt;p&gt;My friend's reaction: &lt;strong&gt;[Add your friend's actual feedback here, or remove this line.]&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A family voice note isn't just audio. It's someone's voice, their stories, names, and private conversations, often from people who are getting older. I wasn't comfortable sending that to a cloud API just because it was convenient.&lt;/p&gt;

&lt;p&gt;Open models meant the whole pipeline—from audio to transcript to recipe to answers—runs on a laptop with no account, no API key, and no per-request cost.&lt;/p&gt;

&lt;p&gt;It also meant I could test the extraction prompt as many times as I liked on real recordings without worrying about a bill, and I could swap models freely when one handled Nepali words better than another.&lt;/p&gt;

&lt;p&gt;A closed API could have done the extraction too. What it couldn't give me is the same level of control over keeping the original recordings on my friend's computer.&lt;/p&gt;

&lt;p&gt;For this project, &lt;strong&gt;that mattered more than anything else.&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build for a Friend / Loved One&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  hf26challenge
&lt;/h1&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
