<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Paul Martin McNeill&apos;s Portfolio</title>
    <description>Welcome to my portfolio. Here you can find my latest projects and learn more about my expertise in web development, AI, and more.</description>
    <link>https://paulmartinmcneill.com/</link>
    <atom:link href="https://paulmartinmcneill.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Wed, 06 May 2026 17:42:43 +0000</pubDate>
    <lastBuildDate>Wed, 06 May 2026 17:42:43 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Building Production RAG Pipelines: Lessons from Real Deployments</title>
        <description>&lt;!-- TODO: REPLACE — placeholder body. Flesh out with real war stories. --&gt;

&lt;p&gt;A demo RAG pipeline takes a weekend. A &lt;em&gt;production&lt;/em&gt; one takes months — and most of
the cost is in the parts you can’t see in a notebook: drift detection, eval suites,
caching strategy, and what happens when retrieval gets it wrong in front of a user.&lt;/p&gt;

&lt;h3 id=&quot;chunking-is-not-preprocessing--its-product-design&quot;&gt;Chunking is not preprocessing — it’s product design&lt;/h3&gt;

&lt;p&gt;Most RAG fails because the chunking strategy was treated as a one-line decision…&lt;/p&gt;

&lt;h3 id=&quot;retrieval-without-grounding-is-just-search&quot;&gt;Retrieval without grounding is just search&lt;/h3&gt;

&lt;p&gt;If your model can’t cite the chunk it pulled from, you can’t debug a hallucination…&lt;/p&gt;

&lt;h3 id=&quot;the-eval-harness-is-the-real-codebase&quot;&gt;The eval harness is the real codebase&lt;/h3&gt;

&lt;p&gt;Models change. Prompts change. Embeddings change. Without an automated regression
suite you’ll ship silently broken retrieval and never know…&lt;/p&gt;
</description>
        <pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/writing/2026/04/01/production-rag-pipelines/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/writing/2026/04/01/production-rag-pipelines/</guid>
        
        
        <category>writing</category>
        
      </item>
    
      <item>
        <title>Why I Moved from Jupyter Notebooks to End-to-End ML Pipelines</title>
        <description>&lt;!-- TODO: REPLACE — placeholder body. --&gt;

&lt;p&gt;Notebooks are wonderful for &lt;strong&gt;exploration&lt;/strong&gt;. They are a disaster for &lt;strong&gt;deployment&lt;/strong&gt;.
This post is about the moment that distinction stopped being theoretical for me, and
what I changed in my workflow as a result…&lt;/p&gt;

&lt;h3 id=&quot;the-hidden-cost-of-it-runs-in-my-notebook&quot;&gt;The hidden cost of “it runs in my notebook”&lt;/h3&gt;

&lt;p&gt;State that lives only in cell outputs. Cell-execution order that nobody else can
reproduce. Imports six layers deep that nobody documents…&lt;/p&gt;

&lt;h3 id=&quot;what-replaces-them&quot;&gt;What replaces them&lt;/h3&gt;

&lt;p&gt;A small, opinionated stack: a feature pipeline, a training script, a model registry,
and a serving layer. Boring. Reliable. Reviewable.&lt;/p&gt;
</description>
        <pubDate>Sat, 14 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/writing/2026/02/14/from-notebooks-to-pipelines/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/writing/2026/02/14/from-notebooks-to-pipelines/</guid>
        
        
        <category>writing</category>
        
      </item>
    
      <item>
        <title>CRM Reporting – Industrial Temps</title>
        <description>&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;A full refresh of CRM analytics for a recruitment agency: &lt;strong&gt;cleaner data, better KPIs, and faster monthly reporting&lt;/strong&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;challenges&quot;&gt;Challenges&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Inconsistent CRM fields → poor KPI accuracy (placements, fill rate, time-to-hire).&lt;/li&gt;
  &lt;li&gt;Slow manual exports → reporting lag and errors.&lt;/li&gt;
  &lt;li&gt;Limited visibility into drop-offs by stage (sourcing → placement).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-i-delivered&quot;&gt;What I Delivered&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Data quality framework&lt;/strong&gt;: mandatory fields, validation rules, audit queries.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ETL/ELT pipeline&lt;/strong&gt; (Access + Power Query) → standardized reporting tables.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Power BI dashboards&lt;/strong&gt;: placements, client performance, consultant scorecards.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scheduled refresh&lt;/strong&gt; from SharePoint &amp;amp; SQL; versioned datasets.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;kpis--metrics&quot;&gt;KPIs &amp;amp; Metrics&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Placement rate&lt;/strong&gt;, &lt;strong&gt;time-to-hire&lt;/strong&gt;, &lt;strong&gt;fill rate&lt;/strong&gt;, &lt;strong&gt;candidate source ROI&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Funnel drop-off&lt;/strong&gt; by stage + reasons (disqualifications, no-shows).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Consultant activity&lt;/strong&gt; vs outcomes (calls, interviews, starts).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;outcomes&quot;&gt;Outcomes&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;3× faster&lt;/strong&gt; month-end reporting (hours → minutes).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;+22% data completeness&lt;/strong&gt; (quarter-over-quarter).&lt;/li&gt;
  &lt;li&gt;Stakeholders self-serve common queries; fewer ad-hoc report requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;implementation-details&quot;&gt;Implementation Details&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Source&lt;/strong&gt;: CRM export + SQL mirror (where available)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Transform&lt;/strong&gt;: Access/Excel (Power Query), data audits, normalization&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Serve&lt;/strong&gt;: Power BI reports with role-level security&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Governance&lt;/strong&gt;: Dataset owners, refresh alerts, change log&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;my-role&quot;&gt;My Role&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Requirement capture, data modeling, DAX measures, stakeholder training.&lt;/li&gt;
  &lt;li&gt;Built “single-click” monthly pack export for leadership.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;tooling&quot;&gt;Tooling&lt;/h3&gt;
&lt;p&gt;Dynamics 365 (or CRM export), Access, Excel/Power Pivot, Power Query, Power BI, SharePoint&lt;/p&gt;

</description>
        <pubDate>Fri, 06 Sep 2024 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/projects/2024/09/06/crm-reporting-industrial-temps/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/projects/2024/09/06/crm-reporting-industrial-temps/</guid>
        
        
        <category>projects</category>
        
      </item>
    
      <item>
        <title>Custom CRM Web Application</title>
        <description>
</description>
        <pubDate>Fri, 06 Sep 2024 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/crm%20software%20development/2024/09/06/project-5/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/crm%20software%20development/2024/09/06/project-5/</guid>
        
        
        <category>CRM Software Development</category>
        
      </item>
    
      <item>
        <title>AI-Enhanced Social Healthcare Management System</title>
        <description>
</description>
        <pubDate>Sun, 01 Sep 2024 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/ai/2024/09/01/ai-project-2/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/ai/2024/09/01/ai-project-2/</guid>
        
        
        <category>AI</category>
        
      </item>
    
      <item>
        <title>AI-Powered AAC App</title>
        <description>&lt;!-- Styles: tile gallery + viewer --&gt;
&lt;style&gt;
/* Keep the entire modal usable on one screen */
.modal .modal-body { max-height: calc(100vh - 160px); overflow-y: auto; }

/* TILES */
.tile-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 8px;
}
.tile {
  background:#fafafa; border-radius:10px; overflow:hidden; position:relative;
  box-shadow: 0 2px 6px rgba(0,0,0,.08); transition: transform .18s ease, box-shadow .18s ease;
}
.tile:focus-within, .tile:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(0,0,0,.18); }
.tile a { display:block; text-decoration:none; outline:0; }
.tile img {
  width:100%; height:180px; object-fit:cover; display:block;
  transition: transform .18s ease;
}
.tile:hover img { transform: scale(1.04); }
.tile .cap {
  position:absolute; left:8px; bottom:8px; right:8px;
  background: rgba(0,0,0,.55); color:#fff; font-size:13px; padding:6px 8px; border-radius:6px;
}

/* Viewer (single modal, custom controls) */
.viewer-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,.8);
  display: none; align-items: center; justify-content: center; z-index: 9999;
}
.viewer-backdrop.open { display: flex; }
.viewer {
  position: relative; max-width: min(92vw, 1200px); max-height: 86vh;
  background: #111; border-radius: 10px; overflow: hidden;
  box-shadow: 0 12px 40px rgba(0,0,0,.6);
  display:flex; flex-direction:column;
}
.viewer-header {
  display:flex; align-items:center; justify-content:space-between;
  padding:10px 12px; color:#eee; font-size:14px; background:#1b1b1b;
}
.viewer-body { position:relative; flex:1; display:flex; align-items:center; justify-content:center; background:#0f0f0f; }
.viewer-body img { max-width: 100%; max-height: 100%; object-fit: contain; display:block; }
.viewer-caption { color:#eaeaea; background:#151515; font-size:14px; padding:10px 12px; }

/* Controls */
.viewer-btn {
  position:absolute; top:50%; transform: translateY(-50%);
  width:44px; height:44px; border:none; border-radius:50%;
  background: rgba(255,255,255,.1); color:#fff; cursor:pointer;
}
.viewer-btn:hover { background: rgba(255,255,255,.2); }
.viewer-prev { left:10px; }
.viewer-next { right:10px; }
.viewer-close {
  background: transparent; border:none; color:#eee; font-size:20px; line-height:1;
  cursor:pointer; padding:4px 8px;
}

/* Keyboard focus */
.viewer-btn:focus, .viewer-close:focus, .tile a:focus { outline: 2px solid #2c7be5; outline-offset:2px; }

/* Tabs */
.nav-tabs &gt; li &gt; a { padding: 10px 15px; }

/* Small screens: auto height tiles */
@media (max-width: 767px) { .tile img { height: 160px; } }

/* Hide any old carousel-specific blocks if still present */
.portfolio-carousel { display:none !important; }
&lt;/style&gt;

&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;A modern AAC (Augmentative &amp;amp; Alternative Communication) app that helps non-verbal users express themselves faster.&lt;br /&gt;
It combines &lt;strong&gt;predictive text (TensorFlow.js)&lt;/strong&gt;, &lt;strong&gt;accessible UI&lt;/strong&gt;, and a &lt;strong&gt;caregiver analytics dashboard&lt;/strong&gt; for insight-led support.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;problem&quot;&gt;Problem&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;AAC tools are often slow and cognitively heavy.&lt;/li&gt;
  &lt;li&gt;Caregivers lack visibility on what’s working (or not) during sessions.&lt;/li&gt;
  &lt;li&gt;Accessibility (WCAG) and privacy (GDPR) are frequently bolt-ons, not fundamentals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;solution&quot;&gt;Solution&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Predictive AI&lt;/strong&gt; suggests next words/phrases in context; improves with usage.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Accessible UI&lt;/strong&gt; (high-contrast themes, large touch targets, offline first).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Caregiver dashboard&lt;/strong&gt;: session logs, vocab usage, time-to-utter metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Next-word prediction&lt;/strong&gt; via TF.js + custom tokenizer.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Personal vocab folders&lt;/strong&gt; with pictograms; grid size &amp;amp; theme preferences.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Caregiver dashboard&lt;/strong&gt; (Firebase + charts): top phrases, session durations.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;WCAG 2.1 AA&lt;/strong&gt;: keyboard/switch navigation, ARIA roles, focus states.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GDPR-first&lt;/strong&gt;: anonymized events, explicit consent flows, export/delete data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt;: React / React Native (Kiosk mode on tablets)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Model&lt;/strong&gt;: TensorFlow.js in-browser; optional server-assisted re-ranking&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Firebase (Auth/Firestore/Storage)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Telemetry&lt;/strong&gt;: Minimal, aggregated events; per-user opt-in&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;outcomes&quot;&gt;Outcomes&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;35–50% faster&lt;/strong&gt; average time-to-utter for common phrases (pilot, n=7).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;+40% caregiver satisfaction&lt;/strong&gt; with session clarity (survey).&lt;/li&gt;
  &lt;li&gt;Reduced input errors with large targets &amp;amp; latency-friendly cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;my-role&quot;&gt;My Role&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;End-to-end build, model integration, WCAG compliance, privacy workflows.&lt;/li&gt;
  &lt;li&gt;UX research with caregivers; rapid iteration via weekly pilots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;p&gt;React, React Native, TensorFlow.js, Firebase (Auth/Firestore/Hosting), Tailwind, Charting&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;screens--media&quot;&gt;Screens / Media&lt;/h3&gt;

&lt;!-- Tabs --&gt;
&lt;ul class=&quot;nav nav-tabs nav-justified&quot; role=&quot;tablist&quot; style=&quot;margin-top:10px;&quot;&gt;
  &lt;li role=&quot;presentation&quot; class=&quot;active&quot;&gt;
    &lt;a href=&quot;#project-aac-ai-aac-web&quot; aria-controls=&quot;project-aac-ai-aac-web&quot; role=&quot;tab&quot; data-toggle=&quot;tab&quot;&gt;Web App&lt;/a&gt;
  &lt;/li&gt;
  &lt;li role=&quot;presentation&quot;&gt;
    &lt;a href=&quot;#project-aac-ai-aac-mobile&quot; aria-controls=&quot;project-aac-ai-aac-mobile&quot; role=&quot;tab&quot; data-toggle=&quot;tab&quot;&gt;Mobile App&lt;/a&gt;
  &lt;/li&gt;
  &lt;li role=&quot;presentation&quot;&gt;
    &lt;a href=&quot;#project-aac-ai-aac-firebase&quot; aria-controls=&quot;project-aac-ai-aac-firebase&quot; role=&quot;tab&quot; data-toggle=&quot;tab&quot;&gt;Firebase&lt;/a&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;tab-content&quot; style=&quot;margin-top:15px;&quot;&gt;

  &lt;!-- WEB APP (Tile Gallery) --&gt;
  &lt;div role=&quot;tabpanel&quot; class=&quot;tab-pane fade in active&quot; id=&quot;project-aac-ai-aac-web&quot;&gt;
    &lt;h4 class=&quot;text-center&quot;&gt;Web App Dashboard &amp;amp; Admin&lt;/h4&gt;
    &lt;div class=&quot;tile-gallery&quot; data-album=&quot;project-aac-ai-aac-web&quot;&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/admin_dashboard.png&quot; data-caption=&quot;Admin Dashboard&quot; data-index=&quot;0&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/admin_dashboard.png&quot; alt=&quot;Admin Dashboard&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Admin Dashboard&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/CaregiverDashboard.png&quot; data-caption=&quot;Caregiver Dashboard&quot; data-index=&quot;1&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/CaregiverDashboard.png&quot; alt=&quot;Caregiver Dashboard&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Caregiver Dashboard&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/manage_caregivers.png&quot; data-caption=&quot;Manage Caregivers&quot; data-index=&quot;2&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/manage_caregivers.png&quot; alt=&quot;Manage Caregivers&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Manage Caregivers&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/user_management.png&quot; data-caption=&quot;User Management&quot; data-index=&quot;3&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/user_management.png&quot; alt=&quot;User Management&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;User Management&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/voice_dashboard_login.png&quot; data-caption=&quot;Dashboard Login&quot; data-index=&quot;4&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/voice_dashboard_login.png&quot; alt=&quot;Dashboard Login&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Dashboard Login&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/Webapp_Signup.png&quot; data-caption=&quot;Web App Signup&quot; data-index=&quot;5&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/Webapp_Signup.png&quot; alt=&quot;Web App Signup&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Web App Signup&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/CLI_webapp.png&quot; data-caption=&quot;CLI Build Output&quot; data-index=&quot;6&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/CLI_webapp.png&quot; alt=&quot;CLI Build Output&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;CLI Build Output&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;
        &lt;a href=&quot;img/portfolio/aac-ai/ExpoGo_CLI.png&quot; data-caption=&quot;Expo CLI&quot; data-index=&quot;7&quot;&gt;
          &lt;img src=&quot;img/portfolio/aac-ai/ExpoGo_CLI.png&quot; alt=&quot;Expo CLI&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Expo CLI&lt;/div&gt;
        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- MOBILE APP (Tile Gallery) --&gt;
  &lt;div role=&quot;tabpanel&quot; class=&quot;tab-pane fade&quot; id=&quot;project-aac-ai-aac-mobile&quot;&gt;
    &lt;h4 class=&quot;text-center&quot;&gt;Mobile App Screens&lt;/h4&gt;
    &lt;div class=&quot;tile-gallery&quot; data-album=&quot;project-aac-ai-aac-mobile&quot;&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/login_mobile.jpg&quot; data-caption=&quot;Login&quot; data-index=&quot;0&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/login_mobile.jpg&quot; alt=&quot;Login (Mobile)&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Login&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/signup_mobile.jpg&quot; data-caption=&quot;Signup&quot; data-index=&quot;1&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/signup_mobile.jpg&quot; alt=&quot;Signup (Mobile)&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Signup&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/sentencebuilderscreen_mobile.jpg&quot; data-caption=&quot;Sentence Builder&quot; data-index=&quot;2&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/sentencebuilderscreen_mobile.jpg&quot; alt=&quot;Sentence Builder (Mobile)&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Sentence Builder&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/EmotionScreen_mobile.jpg&quot; data-caption=&quot;Emotion Screen&quot; data-index=&quot;3&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/EmotionScreen_mobile.jpg&quot; alt=&quot;Emotion Screen&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Emotion Screen&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/pictograms_mobile.jpg&quot; data-caption=&quot;Pictogram Grid&quot; data-index=&quot;4&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/pictograms_mobile.jpg&quot; alt=&quot;Pictogram Grid&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Pictogram Grid&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/CameraScreen_mobile.jpg&quot; data-caption=&quot;Camera Captioning&quot; data-index=&quot;5&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/CameraScreen_mobile.jpg&quot; alt=&quot;Camera Captioning&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Camera Captioning&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/LiveSceneModeScreen_mobile.jpg&quot; data-caption=&quot;Live Scene Mode&quot; data-index=&quot;6&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/LiveSceneModeScreen_mobile.jpg&quot; alt=&quot;Live Scene Mode&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Live Scene Mode&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/profilescreen.jpg&quot; data-caption=&quot;Profile &amp;amp; Settings&quot; data-index=&quot;7&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/profilescreen.jpg&quot; alt=&quot;Profile &amp;amp; Settings&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Profile &amp;amp; Settings&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/loading_screen_mobile.jpg&quot; data-caption=&quot;Loading Screen&quot; data-index=&quot;8&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/loading_screen_mobile.jpg&quot; alt=&quot;Loading Screen&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Loading Screen&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- FIREBASE (Tile Gallery) --&gt;
  &lt;div role=&quot;tabpanel&quot; class=&quot;tab-pane fade&quot; id=&quot;project-aac-ai-aac-firebase&quot;&gt;
    &lt;h4 class=&quot;text-center&quot;&gt;Firebase Backend &amp;amp; Data&lt;/h4&gt;
    &lt;div class=&quot;tile-gallery&quot; data-album=&quot;project-aac-ai-aac-firebase&quot;&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/firebase_auth.png&quot; data-caption=&quot;Firebase Authentication&quot; data-index=&quot;0&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/firebase_auth.png&quot; alt=&quot;Firebase Authentication&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Firebase Authentication&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/firebase_database.png&quot; data-caption=&quot;Realtime Database&quot; data-index=&quot;1&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/firebase_database.png&quot; alt=&quot;Firebase Realtime Database&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Realtime Database&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/realtimedatabase_rules.png&quot; data-caption=&quot;Realtime DB Rules&quot; data-index=&quot;2&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/realtimedatabase_rules.png&quot; alt=&quot;Realtime Database Rules&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Realtime DB Rules&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/userlogs_firebase.png&quot; data-caption=&quot;User Logs&quot; data-index=&quot;3&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/userlogs_firebase.png&quot; alt=&quot;User Logs Node&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;User Logs&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/users_firebase.png&quot; data-caption=&quot;Users&quot; data-index=&quot;4&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/users_firebase.png&quot; alt=&quot;Users Node&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Users&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
      &lt;div class=&quot;tile&quot;&gt;&lt;a href=&quot;img/portfolio/aac-ai/caregivers_firebase.png&quot; data-caption=&quot;Caregivers&quot; data-index=&quot;5&quot;&gt;
        &lt;img src=&quot;img/portfolio/aac-ai/caregivers_firebase.png&quot; alt=&quot;Caregivers Node&quot; loading=&quot;lazy&quot; /&gt;&lt;div class=&quot;cap&quot;&gt;Caregivers&lt;/div&gt;
      &lt;/a&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;

&lt;!-- Single shared lightbox viewer --&gt;
&lt;div class=&quot;viewer-backdrop&quot; id=&quot;project-aac-ai-viewer&quot; aria-hidden=&quot;true&quot; role=&quot;dialog&quot; aria-label=&quot;Image viewer&quot;&gt;
  &lt;div class=&quot;viewer&quot; role=&quot;document&quot;&gt;
    &lt;div class=&quot;viewer-header&quot;&gt;
      &lt;div&gt;&lt;span id=&quot;project-aac-ai-viewer-pos&quot;&gt;1/1&lt;/span&gt;&lt;/div&gt;
      &lt;button class=&quot;viewer-close&quot; type=&quot;button&quot; aria-label=&quot;Close viewer&quot; id=&quot;project-aac-ai-viewer-close&quot;&gt;✕&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;viewer-body&quot; id=&quot;project-aac-ai-viewer-body&quot;&gt;
      &lt;button class=&quot;viewer-btn viewer-prev&quot; type=&quot;button&quot; aria-label=&quot;Previous image&quot; id=&quot;project-aac-ai-viewer-prev&quot;&gt;‹&lt;/button&gt;
      &lt;img id=&quot;project-aac-ai-viewer-img&quot; alt=&quot;&quot; /&gt;
      &lt;button class=&quot;viewer-btn viewer-next&quot; type=&quot;button&quot; aria-label=&quot;Next image&quot; id=&quot;project-aac-ai-viewer-next&quot;&gt;›&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;viewer-caption&quot; id=&quot;project-aac-ai-viewer-cap&quot;&gt;Caption&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;!-- Minimal JS: album-aware viewer with keyboard + touch (no jQuery) --&gt;
&lt;script&gt;
(function () {
  var VIEWER_ID = &quot;project-aac-ai-viewer&quot;;
  var $backdrop = document.getElementById(VIEWER_ID);
  var $img = document.getElementById(&quot;project-aac-ai-viewer-img&quot;);
  var $cap = document.getElementById(&quot;project-aac-ai-viewer-cap&quot;);
  var $pos = document.getElementById(&quot;project-aac-ai-viewer-pos&quot;);
  var $prev = document.getElementById(&quot;project-aac-ai-viewer-prev&quot;);
  var $next = document.getElementById(&quot;project-aac-ai-viewer-next&quot;);
  var $close = document.getElementById(&quot;project-aac-ai-viewer-close&quot;);

  // Build album maps from DOM
  var albums = {}; // albumId -&gt; [{src, cap, thumbEl}, ...]
  document.querySelectorAll(&apos;.tile-gallery&apos;).forEach(function(g) {
    var id = g.getAttribute(&apos;data-album&apos;);
    var items = [];
    g.querySelectorAll(&apos;a[href][data-index]&apos;).forEach(function(a) {
      items.push({
        src: a.getAttribute(&apos;href&apos;),
        cap: a.getAttribute(&apos;data-caption&apos;) || (a.querySelector(&apos;.cap&apos;) ? a.querySelector(&apos;.cap&apos;).textContent : &apos;&apos;),
        thumbEl: a
      });
    });
    albums[id] = items;
  });

  var state = { albumId: null, index: 0 };

  function openViewer(albumId, index) {
    var items = albums[albumId] || [];
    if (!items.length) return;
    state.albumId = albumId;
    state.index = Math.max(0, Math.min(index, items.length - 1));
    render();
    $backdrop.classList.add(&apos;open&apos;);
    $backdrop.setAttribute(&apos;aria-hidden&apos;, &apos;false&apos;);
    document.body.style.overflow = &apos;hidden&apos;;
    $close.focus();
  }

  function closeViewer() {
    $backdrop.classList.remove(&apos;open&apos;);
    $backdrop.setAttribute(&apos;aria-hidden&apos;, &apos;true&apos;);
    document.body.style.overflow = &apos;&apos;;
  }

  function render() {
    var items = albums[state.albumId];
    var it = items[state.index];
    // simple preload of neighbors
    [state.index + 1, state.index - 1].forEach(function(i){
      if (i&gt;=0 &amp;&amp; i&lt;items.length) { var p = new Image(); p.src = items[i].src; }
    });
    $img.src = it.src;
    $img.alt = it.cap || &quot;Image &quot; + (state.index + 1);
    $cap.textContent = it.cap || &apos;&apos;;
    $pos.textContent = (state.index + 1) + &quot;/&quot; + items.length;
    $prev.style.visibility = (state.index &gt; 0) ? &apos;visible&apos; : &apos;hidden&apos;;
    $next.style.visibility = (state.index &lt; items.length - 1) ? &apos;visible&apos; : &apos;hidden&apos;;
  }

  function next() {
    var items = albums[state.albumId];
    if (state.index &lt; items.length - 1) { state.index++; render(); }
  }
  function prev() {
    if (state.index &gt; 0) { state.index--; render(); }
  }

  // Tile clicks
  document.querySelectorAll(&apos;.tile-gallery a[data-index]&apos;).forEach(function(a) {
    a.addEventListener(&apos;click&apos;, function(e) {
      e.preventDefault();
      var albumEl = a.closest(&apos;.tile-gallery&apos;);
      var albumId = albumEl.getAttribute(&apos;data-album&apos;);
      var idx = parseInt(a.getAttribute(&apos;data-index&apos;), 10) || 0;
      openViewer(albumId, idx);
    });
  });

  // Viewer controls
  $next.addEventListener(&apos;click&apos;, next);
  $prev.addEventListener(&apos;click&apos;, prev);
  $close.addEventListener(&apos;click&apos;, closeViewer);
  $backdrop.addEventListener(&apos;click&apos;, function(e) {
    if (e.target === $backdrop) closeViewer();
  });

  // Keyboard
  document.addEventListener(&apos;keydown&apos;, function(e) {
    if (!$backdrop.classList.contains(&apos;open&apos;)) return;
    if (e.key === &apos;Escape&apos;) closeViewer();
    if (e.key === &apos;ArrowRight&apos;) next();
    if (e.key === &apos;ArrowLeft&apos;) prev();
  });

  // Touch swipe
  (function addSwipe(el){
    var x0=null,y0=null;
    el.addEventListener(&apos;touchstart&apos;, function(e){
      var t=e.touches[0]; x0=t.clientX; y0=t.clientY;
    }, {passive:true});
    el.addEventListener(&apos;touchmove&apos;, function(e){
      if(x0===null) return;
      var t=e.touches[0]; var dx=t.clientX-x0; var dy=t.clientY-y0;
      if(Math.abs(dx)&gt;40 &amp;&amp; Math.abs(dx)&gt;Math.abs(dy)){
        if(dx&lt;0) next(); else prev();
        x0=null; y0=null;
      }
    }, {passive:true});
    el.addEventListener(&apos;touchend&apos;, function(){ x0=null; y0=null; });
  })($backdrop);
})();
&lt;/script&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;notes-on-privacy--safety&quot;&gt;Notes on Privacy &amp;amp; Safety&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Anonymization by default, per-user data export, short retention windows.&lt;/li&gt;
  &lt;li&gt;Clear consent and session visibility for caregivers and admins.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 01 Sep 2024 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/projects/2024/09/01/ai-powered-aac-app/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/projects/2024/09/01/ai-powered-aac-app/</guid>
        
        
        <category>projects</category>
        
      </item>
    
      <item>
        <title>Java-Based Ticket Machine System</title>
        <description>
</description>
        <pubDate>Sat, 24 Aug 2024 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/software%20development/2024/08/24/project-1/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/software%20development/2024/08/24/project-1/</guid>
        
        
        <category>Software Development</category>
        
      </item>
    
      <item>
        <title>Data Structures &amp; Algorithms in Python</title>
        <description>
</description>
        <pubDate>Sat, 15 Jul 2023 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/software%20development/2023/07/15/project-2/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/software%20development/2023/07/15/project-2/</guid>
        
        
        <category>Software Development</category>
        
      </item>
    
      <item>
        <title>Cloud-Based Data Management System</title>
        <description>
</description>
        <pubDate>Tue, 20 Jun 2023 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/cloud%20computing/2023/06/20/project-3/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/cloud%20computing/2023/06/20/project-3/</guid>
        
        
        <category>Cloud Computing</category>
        
      </item>
    
      <item>
        <title>Expense Tracker</title>
        <description>
</description>
        <pubDate>Wed, 10 May 2023 00:00:00 +0000</pubDate>
        <link>https://paulmartinmcneill.com/web%20development/2023/05/10/project-4/</link>
        <guid isPermaLink="true">https://paulmartinmcneill.com/web%20development/2023/05/10/project-4/</guid>
        
        
        <category>Web Development</category>
        
      </item>
    
  </channel>
</rss>
