<?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: Faris Daffa</title>
    <description>The latest articles on DEV Community by Faris Daffa (@faris0520).</description>
    <link>https://dev.to/faris0520</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F620344%2Fb6f254fc-3ac9-4cac-988d-08c7ecc982f4.webp</url>
      <title>DEV Community: Faris Daffa</title>
      <link>https://dev.to/faris0520</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faris0520"/>
    <language>en</language>
    <item>
      <title>Last Slide: the homework card for the lecture deck you never opened</title>
      <dc:creator>Faris Daffa</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:57:43 +0000</pubDate>
      <link>https://dev.to/faris0520/last-slide-the-homework-card-for-the-lecture-deck-you-never-opened-gml</link>
      <guid>https://dev.to/faris0520/last-slide-the-homework-card-for-the-lecture-deck-you-never-opened-gml</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Opening a 40-page lecture slide deck the night before class is pure dread. You skipped the lecture, you did not do the reading, and you know opening that PDF will take forty minutes of skimming through title cards, bullet points, and derivations you do not need right now.&lt;/p&gt;

&lt;p&gt;Last Slide is a skip card for that unread deck. It is not a summarizer. It does not produce study packs, flashcard sets, or WhatsApp digests. When you skip class, you do not want an AI tutor explaining the entire curriculum. You just want the leftover slide still projected on the whiteboard after everyone left the room.&lt;/p&gt;

&lt;p&gt;Last Slide extracts the native text from your PDF directly in your browser tab, passes a budgeted slice to &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; running on your local machine via &lt;strong&gt;Ollama&lt;/strong&gt;, and fills exactly four fields:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What you missed&lt;/strong&gt;: exactly two sentences explaining what the lecture covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Due&lt;/strong&gt;: the one homework assignment or due date if it is actually written on a slide, complete with the slide number. If nothing is due, it explicitly says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They will ask&lt;/strong&gt;: one exam-style question grounded in the slides, so you are ready if called on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can skip&lt;/strong&gt;: one sentence pointing out the appendix, optional reading, or repeated examples you can safely ignore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No files leave your computer. Scanned PDFs without text layers are refused immediately.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live UI&lt;/strong&gt;: &lt;a href="https://last-slide.onrender.com/" rel="noopener noreferrer"&gt;https://last-slide.onrender.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Code&lt;/strong&gt;: &lt;a href="https://github.com/Faris0520/last-slide" rel="noopener noreferrer"&gt;https://github.com/Faris0520/last-slide&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi70ajb7ydmxwnpaw2zhg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi70ajb7ydmxwnpaw2zhg.png" alt="Last Slide drop zone interface with sample deck button and Ollama status check" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Running locally
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; and pull Gemma 3 4B:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull gemma3:4b
ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Clone and start the client:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm sample
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open the local Vite URL in your browser. You can click &lt;strong&gt;Load sample&lt;/strong&gt; to run the bundled 12-page sample deck (&lt;code&gt;CS 101 Hash Tables&lt;/code&gt;), or drag and drop your own lecture PDF.&lt;/p&gt;

&lt;p&gt;If you access a deployed frontend over HTTPS, the browser blocks requests to &lt;code&gt;localhost:11434&lt;/code&gt; by default. Start Ollama with the origin flag to allow cross-origin requests:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OLLAMA_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://last-slide.onrender.com"&lt;/span&gt; ollama serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;On Windows PowerShell:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;OLLAMA_ORIGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://last-slide.onrender.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;serve&lt;/span&gt;&lt;span class="w"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6wy1z8dxuzp2kqkkyy8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6wy1z8dxuzp2kqkkyy8.png" alt="Generated Last Slide skip card showing the two-sentence missed summary, homework on slide 11, likely question, and skip note" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The project is open source under the MIT license:&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/Faris0520" rel="noopener noreferrer"&gt;
        Faris0520
      &lt;/a&gt; / &lt;a href="https://github.com/Faris0520/last-slide" rel="noopener noreferrer"&gt;
        last-slide
      &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;Last Slide&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A skip card for the lecture PDF you never opened.&lt;/p&gt;

&lt;p&gt;Drop a native-text slide deck. Last Slide reads it in this browser tab (&lt;code&gt;pdf.js&lt;/code&gt;), keeps only a budget of pages (deadline language always included), and asks &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; via local &lt;strong&gt;Ollama&lt;/strong&gt; for four fields: two sentences of what you missed, the one due date if it is actually on a slide, one exam question, and what you can skip. Scans are refused. Nothing is uploaded.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Install Ollama&lt;/a&gt; and pull the model:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:4b
ollama serve&lt;/pre&gt;

&lt;/div&gt;
&lt;ol start="2"&gt;
&lt;li&gt;Install and run the app:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pnpm install
pnpm sample
pnpm dev&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open the URL Vite prints. Load the bundled sample (&lt;code&gt;CS 101 Hash Tables&lt;/code&gt;) or drop your own PDF. Confirm before anything is sent to &lt;code&gt;http://localhost:11434&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you open a &lt;strong&gt;deployed&lt;/strong&gt; https page (Render), the browser will block &lt;code&gt;localhost&lt;/code&gt; unless Ollama allows that origin:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;OLLAMA_ORIGINS=https://YOUR-APP.onrender.com&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Faris0520/last-slide" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;The core logic is divided into three focused TypeScript modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/lib/pdf.ts&lt;/code&gt;: Extracts text per page in the browser tab using &lt;code&gt;pdfjs-dist&lt;/code&gt;. Refuses scans if total text is under 50 characters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/ingest.ts&lt;/code&gt;: Manages a strict 4,500-character budget. Runs a regex pre-pass for deadline keywords (&lt;code&gt;tugas&lt;/code&gt;, &lt;code&gt;homework&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt;, &lt;code&gt;deadline&lt;/code&gt;, &lt;code&gt;submit&lt;/code&gt;) so homework slides are never cut off by early filler slides.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/card.ts&lt;/code&gt;: Defines the strict JSON schema and system prompt that confines Gemma 3 4B to four fields.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/ollama.ts&lt;/code&gt;: Probes local Ollama health and posts to &lt;code&gt;/api/chat&lt;/code&gt; with schema constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the keyword-first budgeting from &lt;code&gt;src/lib/ingest.ts&lt;/code&gt;:&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;CHAR_BUDGET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_500&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;DEADLINE_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(&lt;/span&gt;&lt;span class="sr"&gt;tugas|pengumpulan|homework|assignment|due&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+date|&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;due&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;|deadline|uts|uas|submit&lt;/span&gt;&lt;span class="se"&gt;)\b&lt;/span&gt;&lt;span class="sr"&gt;/i&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;planIngest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PdfPage&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;IngestPlan&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;keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;DEADLINE_RE&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;p&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keywordNs&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&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;sent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PdfPage&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PdfPage&lt;/span&gt;&lt;span class="p"&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="nx"&gt;chars&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;CHAR_BUDGET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CHAR_BUDGET&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;chars&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;page&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="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;page&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="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="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
    &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&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="nx"&gt;chars&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="nx"&gt;length&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Prioritize pages containing deadline language&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;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Fill remaining budget with sequential slides&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;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&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="nx"&gt;keywordNs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="nf"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&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;sentNs&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sent&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&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;skipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sentNs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&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;n&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&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;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;skipped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keywordPages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyword&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="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;chars&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;And the Ollama call in &lt;code&gt;src/lib/ollama.ts&lt;/code&gt; sending the raw JSON schema directly into &lt;code&gt;format&lt;/code&gt;:&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;completeViaOllama&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;model&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;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatTurn&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="nx"&gt;schema&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;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="nb"&gt;Promise&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="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;fetch&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="nf"&gt;base&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="s2"&gt;/api/chat`&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="s1"&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="s1"&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="s1"&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="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;messages&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;schema&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;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;top_p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.9&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;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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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;detail&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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="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;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Ollama answered &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;detail&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;detail&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;200&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="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="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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&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="k"&gt;return&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The stack is intentionally lean: React 19, TypeScript, Vite, &lt;code&gt;pdfjs-dist&lt;/code&gt;, and plain CSS. There is no custom backend server.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Extracting text locally with pdf.js
&lt;/h3&gt;

&lt;p&gt;Instead of uploading files to a cloud document processor, the user drops their PDF directly into the browser tab. &lt;code&gt;pdfjs-dist&lt;/code&gt; runs in a web worker, walks through each page, and collects the text stream. &lt;/p&gt;

&lt;p&gt;If total extractable text across all pages is under 50 characters, Last Slide stops immediately and shows an error. There is no hidden OCR fallback. Lecture slides with real text parse in milliseconds; scanned photo decks are rejected honestly.&lt;/p&gt;

&lt;p&gt;This approach was informed by community discussions on client-side PDF handling, particularly Shadman's writeup on &lt;a href="https://dev.to/shadman/comparing-open-source-pdf-libraries-2025-edition-4f4o"&gt;Comparing open source PDF libraries (2025 edition)&lt;/a&gt;, which details how &lt;code&gt;pdf.js&lt;/code&gt; excels at client-side extraction without server overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The 4,500-character budget
&lt;/h3&gt;

&lt;p&gt;Language models slow down when fed entire 60-page PDF transcripts, especially on consumer laptops running on CPU. To keep inference practical, I capped the ingest at 4,500 characters.&lt;/p&gt;

&lt;p&gt;The challenge with a simple character cutoff is that professors almost always put the homework assignment on the penultimate slide. If a student drops a 25-slide deck and you take the first 4,500 characters, slide 24 gets cut off, and the student misses their assignment.&lt;/p&gt;

&lt;p&gt;The solution in &lt;code&gt;planIngest&lt;/code&gt; is a keyword-first budget plan. It scans for words like &lt;code&gt;homework&lt;/code&gt;, &lt;code&gt;assignment&lt;/code&gt;, &lt;code&gt;due&lt;/code&gt;, &lt;code&gt;deadline&lt;/code&gt;, and &lt;code&gt;tugas&lt;/code&gt;. Pages matching those words get top priority in the character budget. The remaining character allowance is then filled by the earlier slides in sequential order.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Taming Gemma 3 4B with constrained decoding
&lt;/h3&gt;

&lt;p&gt;Small models like Gemma 3 4B are fast and capable of running locally, but when asked for open-ended summaries, they tend to ramble, parrot slide headers, or invent extra homework tasks.&lt;/p&gt;

&lt;p&gt;In my earlier projects (&lt;a href="https://dev.to/faris0520/touch-soil-an-offline-garden-planner-with-gemma-running-in-your-browser-41pe"&gt;Touch Soil&lt;/a&gt; and &lt;a href="https://dev.to/faris0520/my-friend-rambles-in-interviews-so-i-built-him-unramble-1kf5"&gt;Unramble&lt;/a&gt;), I learned that small open-weight models become remarkably reliable when their decoding is strictly constrained.&lt;/p&gt;

&lt;p&gt;Rather than asking the model to write paragraphs and parsing with regular expressions, Last Slide passes a strict JSON schema (&lt;code&gt;CARD_SCHEMA&lt;/code&gt;) directly into Ollama's &lt;code&gt;format&lt;/code&gt; parameter. Under the hood, this enforces grammar-constrained decoding: the runtime physically zeroes out token probabilities that violate the schema. The model cannot output markdown fences, conversational commentary, or unexpected JSON keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Intentional friction and honest CPU wait times
&lt;/h3&gt;

&lt;p&gt;Before any text is sent to Ollama, Last Slide displays an explicit confirmation checkbox stating exactly how many characters will be sent, which slides are included, and which slides were held back. The user must check this box before generating.&lt;/p&gt;

&lt;p&gt;When running on CPU without a dedicated GPU, generation takes between one and two minutes. Last Slide states this honestly on screen while the status indicator runs, rather than hiding the wait behind fake progress bars.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Projector slide aesthetic
&lt;/h3&gt;

&lt;p&gt;The design does not look like a SaaS dashboard or a Notion document. It looks like a lecture slide left on an overhead projector after class:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warm beige paper background (&lt;code&gt;#F3EDE0&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Deep ink typography (&lt;code&gt;#1A1612&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Newsreader for titles, IBM Plex Sans for body text&lt;/li&gt;
&lt;li&gt;One red pen accent (&lt;code&gt;#B42318&lt;/code&gt;) used strictly once: on the homework due date.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This project could not exist in this form on a closed commercial API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Course material privacy&lt;/strong&gt;: Lecture slides, unpublished problem sets, and mid-term exam hints belong to students and universities. Asking students to upload their course PDFs to third-party cloud servers creates data leakage risks. With Ollama and Gemma, the entire workflow remains on the user's laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero per-token costs&lt;/strong&gt;: Commercial APIs charge per input token. When students ingest dozens of slide decks across a semester, API costs accumulate quickly. Running Gemma locally costs nothing beyond electricity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture decoupling&lt;/strong&gt;: The model tag in Last Slide is a plain configuration string (&lt;code&gt;gemma3:4b&lt;/code&gt;). If a user has a laptop with more VRAM, they can switch to an 8B or 12B variant without changing a single line of backend logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resisting feature bloat&lt;/strong&gt;: Cloud AI providers encourage complex multi-step pipelines because their business model relies on API volume. Open-weight models allowed me to build an ultra-focused, four-field skip card designed specifically for students who want to do less work, not more.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The lazy payoff
&lt;/h2&gt;

&lt;p&gt;The Week 3 theme is &lt;strong&gt;Slow and Steady: tools that reward laziness&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Many AI study tools try to make students study harder by generating twenty flashcards, multi-chapter summaries, and interactive quizzes. Last Slide does the exact opposite: &lt;strong&gt;it accepts that you skipped the lecture, and it lets you stay lazy without failing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider the bundled sample deck, &lt;code&gt;CS 101 Hash Tables&lt;/code&gt; (12 slides):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slides 1 to 10 explain dictionaries, collision handling, chaining, linear probing, and load factors.&lt;/li&gt;
&lt;li&gt;Slide 11 is the homework: implement a chain-hash map, due Friday 24 October at 17:00.&lt;/li&gt;
&lt;li&gt;Slide 12 is an appendix containing optional readings from Knuth and office hour schedules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip opening this deck, Last Slide produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What you missed&lt;/strong&gt;: "Hash tables provide expected O(1) dictionary operations by hashing keys to bucket indices. Collisions are inevitable and are handled through chaining or open addressing."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Due&lt;/strong&gt;: "Implement a chain-hash map in the language of the course. Due Friday 24 October, 17:00 (Slide 11)."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They will ask&lt;/strong&gt;: "Explain how load factor determines when a hash table must rehash, and why tombstones are necessary when deleting in open addressing."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can skip&lt;/strong&gt;: "The optional Knuth reading in the appendix and the previous year's binary search tree quiz."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You skip reading 12 slides, you skip the optional appendix, you never miss the assignment, and you have an answer ready if the professor asks you about hash collisions. That is the lazy payoff.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Gemma
&lt;/h3&gt;

&lt;p&gt;Last Slide runs &lt;strong&gt;Gemma 3 4B&lt;/strong&gt; locally via Ollama (&lt;code&gt;gemma3:4b&lt;/code&gt;). It leverages Ollama's schema-constrained decoding (&lt;code&gt;format: CARD_SCHEMA&lt;/code&gt;) to produce a reliable four-field JSON payload on consumer hardware without hallucinating deadlines or rambling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Use of Render
&lt;/h3&gt;

&lt;p&gt;The static frontend of Last Slide is hosted on Render (&lt;code&gt;https://last-slide.onrender.com/&lt;/code&gt;). The architecture separates the user interface from inference: the UI is served globally via Render's static site network, while all document parsing and model inference stay entirely local on the visitor's machine via &lt;code&gt;OLLAMA_ORIGINS&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Touch Soil: an offline garden planner with Gemma running in your browser</title>
      <dc:creator>Faris Daffa</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:36:41 +0000</pubDate>
      <link>https://dev.to/faris0520/touch-soil-an-offline-garden-planner-with-gemma-running-in-your-browser-41pe</link>
      <guid>https://dev.to/faris0520/touch-soil-an-offline-garden-planner-with-gemma-running-in-your-browser-41pe</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Touch Soil&lt;/strong&gt; is a garden planner that tells you what to plant this week, and it runs entirely in your browser. You pick your climate zone, optionally mention what you already grow, and an open-weight &lt;strong&gt;Gemma&lt;/strong&gt; model writes a weekly plan: what to plant now, the one job worth doing, and the thing you should skip.&lt;/p&gt;

&lt;p&gt;No account. No server. Nothing you type ever leaves your device, because there is no "elsewhere" to send it to. After the first model download, it works with the internet unplugged, which is exactly where a garden usually is.&lt;/p&gt;

&lt;p&gt;I built it for this week's theme, Touch Grass: the whole point is that the screen part is short. You open the page, read a plan that takes one minute, and go put your hands in the soil.&lt;/p&gt;

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

&lt;p&gt;Deployed app: &lt;strong&gt;&lt;a href="https://touch-soil.onrender.com" rel="noopener noreferrer"&gt;https://touch-soil.onrender.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Note for first load: the Gemma weights (about 1 GB) download once into your browser cache, then load locally from disk. You need a browser with WebGPU (recent Chrome or Edge).&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v6xbguesozbzlhz9w76.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v6xbguesozbzlhz9w76.png" alt="full page" width="800" height="454"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgca34cfq6pbd1d5ojl3.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdgca34cfq6pbd1d5ojl3.png" alt="full page" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/faris0520/touch-soil" rel="noopener noreferrer"&gt;https://github.com/faris0520/touch-soil&lt;/a&gt;&lt;/strong&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/Faris0520" rel="noopener noreferrer"&gt;
        Faris0520
      &lt;/a&gt; / &lt;a href="https://github.com/Faris0520/touch-soil" rel="noopener noreferrer"&gt;
        touch-soil
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An offline garden planner with Gemma running in your browser
    &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;Touch Soil&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;An offline garden planner: you pick your climate zone, and an open-weight &lt;strong&gt;Gemma&lt;/strong&gt; model running &lt;strong&gt;entirely in your browser&lt;/strong&gt; (via &lt;a href="https://github.com/mlc-ai/web-llm" rel="noopener noreferrer"&gt;WebLLM&lt;/a&gt;) writes a "what to plant this week" plan. No server, no account, and your garden data never leaves your device.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why open&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runs with no internet&lt;/strong&gt; after the first model download: WebGPU inference in the browser, so it works at the garden, on a trail, or anywhere the signal doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data stays local&lt;/strong&gt;: the zone you pick and what you're growing are never sent anywhere; there is no backend at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything is swappable&lt;/strong&gt;: the climate data is a small JSON-style table anyone can fork and correct (frost-date datasets are US-centric, so this one includes tropical wet/dry seasons too), and the model is one string away from being any WebLLM-supported open-weight model.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Engine picker.&lt;/strong&gt; Two ways to run the same open-weight…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Faris0520/touch-soil" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The interesting files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/data/climates.ts&lt;/code&gt;: the climate table. Nine zones, from USDA frost zones to tropical monsoon seasons.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/planner.ts&lt;/code&gt;: the prompt, the JSON schema, and the deterministic offline guide.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/lib/llm.ts&lt;/code&gt;: model selection and the WebLLM engine setup.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The stack is deliberately boring: Vite, React, TypeScript, plain CSS. All the novelty is in how the AI piece works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You choose where the model runs.&lt;/strong&gt; A picker offers two engines for the same open-weight Gemma: downloaded into the browser (WebLLM), or an Ollama server on your own machine. The Ollama path only detects models you have already pulled; it never downloads anything by itself. This also gives browsers without WebGPU a real AI option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy actions ask first.&lt;/strong&gt; Before the first browser download (about 1 GB) and before sending your garden details to the local Ollama server, a confirmation panel explains exactly what is about to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model runs in the browser.&lt;/strong&gt; &lt;a href="https://github.com/mlc-ai/web-llm" rel="noopener noreferrer"&gt;WebLLM&lt;/a&gt; runs open-weight models on your GPU through WebGPU. The app picks the smallest Gemma available in WebLLM's prebuilt list (Gemma 3 1B, quantized) and caches it after the first download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model cannot break the format.&lt;/strong&gt; Small models love to ignore instructions, so instead of trusting prompts I constrain decoding: WebLLM gets a JSON schema via &lt;code&gt;response_format&lt;/code&gt;, Ollama gets the same schema via its &lt;code&gt;format&lt;/code&gt; parameter. The model can only emit &lt;code&gt;{ plants: [{ name, why }], task, skip }&lt;/code&gt;, and the UI renders that structure itself. My first free-form version produced headings like "Plant now (3)" and a greeting, the constrained version cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The climate facts are plain data.&lt;/strong&gt; Frost-date planners assume USDA zones, which is unhelpful if your garden has a wet season instead of a winter. Touch Soil bundles a small table: four temperate bands, tropical lowland and highland (monsoon wet/dry seasons), arid, and Mediterranean. It is intentionally small and honest about being approximate, and it is a JSON file you can fork and correct for your region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is a no-AI fallback.&lt;/strong&gt; If neither engine can run, a rule-based guide written from the same dataset takes over. The app is never empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One war story from the build: Gemma 3's prebuilt WebLLM record ships both a &lt;code&gt;context_window_size&lt;/code&gt; override and a &lt;code&gt;sliding_window_size&lt;/code&gt; base config, and the engine refuses to start with both active. The fix (the engine tells you this, to its credit) is switching the record to sliding-window mode with &lt;code&gt;attention_sink_size: 0&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;For this project, the open pieces are not a detail, they are the whole argument:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It works where the garden is.&lt;/strong&gt; No signal, no problem: once cached, the in-browser model runs offline, and the Ollama engine only ever talks to your own machine. A closed API would make the app useless exactly when and where it should be most useful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your garden stays yours.&lt;/strong&gt; What you grow, your zone, your questions: none of it reaches a cloud. The Ollama engine even shows a confirmation naming the exact server your details will go to, and the WebLLM engine shows one before its one-time download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can swap the brain.&lt;/strong&gt; The model is one string in &lt;code&gt;src/lib/llm.ts&lt;/code&gt;. Want a bigger Gemma, a different open-weight model WebLLM supports, or another local Ollama model? Change a filter or type a name. Want better climate data for your region? Edit a JSON table and open a pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No API bill per plan. The compute is the device you already own.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Gemma (the planner runs on an open-weight Gemma model in the browser)&lt;/li&gt;
&lt;li&gt;Render (the app is deployed on Render: &lt;strong&gt;&lt;a href="https://touch-soil.onrender.com" rel="noopener noreferrer"&gt;https://touch-soil.onrender.com&lt;/a&gt;&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>My friend rambles in interviews, so I built him Unramble</title>
      <dc:creator>Faris Daffa</dc:creator>
      <pubDate>Sat, 03 Oct 2026 14:41:30 +0000</pubDate>
      <link>https://dev.to/faris0520/my-friend-rambles-in-interviews-so-i-built-him-unramble-1kf5</link>
      <guid>https://dev.to/faris0520/my-friend-rambles-in-interviews-so-i-built-him-unramble-1kf5</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;My friend, Agus, has a problem I recognized because I have it too. He knows his stuff. Ask him about a project he shipped and, on paper, the answer is all there. But ask him out loud, in an interview, and the answer wanders: it starts three digressions deep, never lands the result, and buries the one sentence that mattered somewhere in the middle. He is not a weak candidate. He is an unpracticed one, and practice is exactly the thing he cannot do, because practicing means saying half-finished answers out loud to another human, at night, repeatedly, until it stops being embarrassing.&lt;/p&gt;

&lt;p&gt;So I built him &lt;strong&gt;Unramble&lt;/strong&gt;: a practice partner for real interviews that runs entirely on his laptop. He pastes a job description and a few CV bullets. Gemma 3, running locally through Ollama, writes seven interview questions tuned to that role. He answers out loud, into the microphone. Whisper, running locally inside the app's own server, transcribes him. And then the part he actually needed: the report.&lt;/p&gt;

&lt;p&gt;Every sentence of his answer gets a color that says what job it was doing: blue set the Situation, purple named the Task, green carried the Action, yellow landed the Result. Gray is filler. He can literally watch an answer go green, green, gray, gray, gray, and never reach yellow. Below the transcript: relevance and conciseness scores, STAR coverage, a "tangled" meter computed from measured filler density and sentence length, and a rewritten version of his own answer with the detours removed, keeping only his real facts. Any question can be re-answered on its own and rescored, so the weak one gets drilled until it stops being weak.&lt;/p&gt;

&lt;p&gt;One rule shaped the whole build: nothing he says ever leaves his laptop. The recordings live in browser tab memory and are never written anywhere. The transcript, the scores, the report: all in local storage. The models run on his machine. There is no server to upload to.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://unramble.farisdaffa.in" rel="noopener noreferrer"&gt;https://unramble.farisdaffa.in&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8YSrgBjqMIg" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

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

&lt;p&gt;GitHub: &lt;a href="https://github.com/Faris0520/unramble" rel="noopener noreferrer"&gt;Faris0520/unramble&lt;/a&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/Faris0520" rel="noopener noreferrer"&gt;
        Faris0520
      &lt;/a&gt; / &lt;a href="https://github.com/Faris0520/unramble" rel="noopener noreferrer"&gt;
        unramble
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A practice partner for real job interviews that runs entirely on your laptop.
    &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;Unramble&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A practice partner for real job interviews that runs entirely on your laptop.&lt;/p&gt;
&lt;p&gt;Unramble was built for one real person: a friend preparing for interviews whose answers know
all the facts but fall apart in the telling. You speak an answer out loud, and the app shows
the structure behind it: which sentence set the scene, which one carried the action, where
you drifted into filler. Then Gemma rewrites the answer, straight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Everything is local.&lt;/strong&gt; Questions and scores come from Gemma 3 4B through Ollama
Transcription comes from Whisper through transformers.js, inside the app's own server
Recordings live in browser tab memory and are never written anywhere. There is no server to
upload to, so an interview rehearsal stays as private as it should be.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;One-time setup (details in &lt;a href="https://github.com/Faris0520/unramble/./SETUP.md" rel="noopener noreferrer"&gt;SETUP.md&lt;/a&gt;):&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;winget install Ollama.Ollama
ollama pull gemma3:4b
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;npm install
npm run build
npm start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="nofollow noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Faris0520/unramble" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9mv1kif6ck408upnvfq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl9mv1kif6ck408upnvfq.png" alt="first setup" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three open-source pieces, glued together as one local pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question generation and scoring: Gemma 3 4B via Ollama.&lt;/strong&gt; The app talks to Ollama's local REST API with structured output (a JSON schema Ollama enforces), which is how a 4B model stays reliable: generation returns exactly &lt;code&gt;{ questions: string[7] }&lt;/code&gt;, and scoring returns sentence tags plus integer scores, every time, on a laptop CPU. The model tag is one environment variable, so he can swap in a bigger Gemma when his hardware allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transcription: Whisper via transformers.js.&lt;/strong&gt; The browser records the take, decodes it with its own AudioContext, mixes to mono, resamples to 16 kHz, and hands the app's server a plain PCM WAV. The server runs the quantized Whisper model in-process; the weights download once into the project folder and work offline from then on. For Indonesian answers, an environment variable switches to the small model, which hears the language better.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The metrics that do not need a model at all.&lt;/strong&gt; Filler counting, sentence splitting, and the tangled score are computed in plain TypeScript from his actual answer. A model judges structure; the app measures facts. The report never shows a number that was not measured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two details I am proud of. First, the transcript is editable before scoring: if Whisper misheard a word, he fixes it, and the score is computed on what he approved, not on what the machine guessed. Second, the on-screen checklist: the setup page pings Ollama, sees exactly which local piece is missing, and prints the one command that fixes it. A friend who would rather not debug a local stack by hand gets that command with a Copy button.&lt;/p&gt;

&lt;p&gt;The interface follows Notion's design system, on purpose: a report about your weakest answer should read like a calm document, not a dashboard. Pastel tints carry the sentence structure, purple is reserved for the one action per screen, and the same layout works on a phone, because practice happens wherever the laptop opens.&lt;/p&gt;

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

&lt;p&gt;An interview rehearsal is the most private thing a job seeker does. You say half-finished sentences. You forget words you know. You ramble, and you know you are rambling while it is happening. Nobody would want a recording of that on somebody else's server.&lt;/p&gt;

&lt;p&gt;That is the argument for open, local AI in this specific product, and it is not a technical argument, it is a dignity argument. With a closed API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;his voice and his weakest answers would sit in someone else's cloud, on someone else's retention policy;&lt;/li&gt;
&lt;li&gt;every practice rep would cost money, and the whole point of practice is unlimited reps;&lt;/li&gt;
&lt;li&gt;it would need the internet, and the moment he most wants to practice is 2 AM before a morning interview, when he just needs the thing to work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the open stack, the privacy story is inspectable instead of promised. The Ollama process runs on his machine, on a port he can see. The model weights sit in a folder he owns. If he wanted, he could read every line that touches his voice. And because the core is open, the app can grow with him: swap the model when a better open one lands, fine-tune on his own practice answers someday, run it forever at zero cost on hardware he already owns.&lt;/p&gt;

&lt;p&gt;Open innovation also meant &lt;em&gt;I&lt;/em&gt; could build this in a weekend and hand it to one specific person, with no API budget, no accounts, and no server to maintain. The tool exists because one real friend needed it, and it can keep existing on his laptop whether or not I ever touch it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing it over
&lt;/h2&gt;

&lt;p&gt;Handover happened on his actual laptop, in his room, with the setup page doing its job: he installed Ollama, pulled the model, and watched the checklist tick green one piece at a time. He is a computer science student, so halfway through the download he asked what exactly was running on his machine. That question is the reason this app is local, and this friend is the one person I could not answer with &lt;em&gt;trust me&lt;/em&gt;. So I opened the folders instead: model weights in a directory he owns, transcript and scores in local storage, recordings that are never written anywhere. He can read every line that touches his voice. Most people have to take that on faith. He can just check.&lt;/p&gt;

&lt;p&gt;Then I handed him the microphone and sat where he could not see me, because a first take needs an audience of nobody. He pasted a junior developer posting he had actually applied to, answered the first question out loud, and produced the answer he always produces: every fact present, one long green Action doing most of the talking, the yellow Result arriving late and out of breath. The tangled meter flagged it without mercy. The moment that mattered came when the rewrite rendered, though. The app had kept only his real facts and cut the detours, and he read his own answer, straight, and said the thing this whole project was built for: &lt;em&gt;oh. That is what I meant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;He drilled the weakest question again that night, on his own, with me out of the room. I know because a screenshot of the second report arrived later with no caption: same question, less gray, the yellow landing where it belongs. That screenshot is the best evidence this project has produced, because it shows the tool in its natural habitat: 2 AM, one nervous person, one laptop, no budget, no account, no me. He is a computer science student. If he ever wants to make it better, he can read the code and change it. That was always the real handover: not a service I keep alive for him, but a folder of open pieces he fully owns.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Gemma&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Thanks to the open-source projects it stands on: Gemma, Ollama, Whisper, and transformers.js.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Hi there! Welcome to my Dev.to profile</title>
      <dc:creator>Faris Daffa</dc:creator>
      <pubDate>Wed, 25 Sep 2024 23:23:41 +0000</pubDate>
      <link>https://dev.to/faris0520/hi-there-1ec0</link>
      <guid>https://dev.to/faris0520/hi-there-1ec0</guid>
      <description>&lt;p&gt;Hello, welcome to my Dev.to profile /.\ &lt;/p&gt;

&lt;p&gt;I'm Faris Daffa, 18yo, i live in Jambi City, Indonesia.&lt;/p&gt;

&lt;p&gt;I am currently an undergraduate student at Computer Science or Informatics, Faculty of Science and Technology, Jambi University.&lt;/p&gt;

&lt;p&gt;Nice to meet ya!&lt;/p&gt;

&lt;p&gt;26/09/2024&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
