<?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: Shahriar Haque Abir</title>
    <description>The latest articles on DEV Community by Shahriar Haque Abir (@shahriarhaqueabir).</description>
    <link>https://dev.to/shahriarhaqueabir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3922343%2Fe0fe1fac-257f-455c-93f0-de859b6b10e3.jpeg</url>
      <title>DEV Community: Shahriar Haque Abir</title>
      <link>https://dev.to/shahriarhaqueabir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shahriarhaqueabir"/>
    <language>en</language>
    <item>
      <title>Building a custom profile in PowerShell</title>
      <dc:creator>Shahriar Haque Abir</dc:creator>
      <pubDate>Tue, 25 Aug 2026 12:43:35 +0000</pubDate>
      <link>https://dev.to/shahriarhaqueabir/building-a-custom-profile-in-powershell-1ng1</link>
      <guid>https://dev.to/shahriarhaqueabir/building-a-custom-profile-in-powershell-1ng1</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built PowershellOps, a terminal dashboard with 126 functions, 90 one-word commands, and a llama.cpp-powered AI hub that never touches the cloud. Full walkthrough, real code, and the forks in the road where you should disagree with me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Windows machine ships with a diagnostics API that most developers never open. CIM classes report your CPU model, battery wear, thermal zones, Defender status, DNS cache, and signed-driver faults, all locally, all queryable from a shell you already have. Pair that with a small language model running on your own GPU and you get something no SaaS dashboard sells: an operations copilot that answers "why is my fan loud" without uploading a byte.&lt;/p&gt;

&lt;p&gt;This post walks the whole build, zero to shipped product. By the end you will have a PowerShell profile where typing &lt;code&gt;dash&lt;/code&gt; renders an 11-suite command center, &lt;code&gt;fix&lt;/code&gt; pipes your last error to a local model for diagnosis, &lt;code&gt;mem&lt;/code&gt; saves notes with automatic secret redaction, and 90 short verbs like &lt;code&gt;temps&lt;/code&gt;, &lt;code&gt;shield&lt;/code&gt;, and &lt;code&gt;dnsbench&lt;/code&gt; resolve from anywhere in your shell. The finished project lives at &lt;a href="https://github.com/shahriarhaqueabir/PowershellOps" rel="noopener noreferrer"&gt;github.com/shahriarhaqueabir/PowershellOps&lt;/a&gt; (v12.0.0, MIT licensed), so you can compare every step against the real source.&lt;/p&gt;

&lt;p&gt;Some steps I hand you completely. Others I leave half-open on purpose, marked &lt;strong&gt;Your turn&lt;/strong&gt;, because you learn the CIM layer by writing your own queries, not by pasting mine. And at each architectural fork I lay out the alternatives, including the ones I rejected and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;The product has four layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A sensor layer.&lt;/strong&gt; Plain functions that query CIM/WMI and return formatted output: hardware specs, uptime, RAM sticks, battery health, temperatures, fans, displays, network adapters, SMB shares, certificate stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An alias surface.&lt;/strong&gt; Every sensor gets a one-word name. &lt;code&gt;raminfo&lt;/code&gt;, &lt;code&gt;patchhistory&lt;/code&gt;, &lt;code&gt;drivehealth&lt;/code&gt;. Ninety of them, all backed by a single ordered hashtable that doubles as the dashboard's data source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An AI hub.&lt;/strong&gt; A local llama.cpp server speaking the OpenAI-compatible protocol on &lt;code&gt;127.0.0.1:8081&lt;/code&gt;. Functions pipe data into it: &lt;code&gt;Get-Process | ai "what's using the most memory?"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A delivery layer.&lt;/strong&gt; A staged installer that works from stock Windows PowerShell 5.1, a hardened profile template, and a 27-test suite that runs hermetically in CI-style isolation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why is this useful when &lt;code&gt;Get-ComputerInfo&lt;/code&gt; exists? Because raw cmdlets answer questions one at a time. A copilot answers the questions you ask at 9am: is anything hot, is anything patched, did my disk start filling up, and what was that registry tweak I saved last month. Speed matters too. Each sensor is one CIM query, so &lt;code&gt;stat&lt;/code&gt; returns a full system pulse in about a second.&lt;/p&gt;

&lt;p&gt;There is also the privacy argument, which turned out to matter more than I expected. The moment you route system telemetry through a cloud API, you need a data policy. Route it through localhost and the policy is trivial: nothing leaves the machine. My memory store holds API keys and hostnames, and I sleep fine because every write passes a redaction filter first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fork Number One: Which Inference Engine
&lt;/h2&gt;

&lt;p&gt;Before any code, pick how the model runs. The candidates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Weaknesses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;llama.cpp server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single exe, OpenAI-compatible HTTP endpoint, GGUF models, full control over flags&lt;/td&gt;
&lt;td&gt;You manage download, port, lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ollama&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-command install, model management built in&lt;/td&gt;
&lt;td&gt;Daemon owns the port; harder to pin versions; another product to trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LM Studio&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nice GUI for model shopping&lt;/td&gt;
&lt;td&gt;GUI-first, weaker headless story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zero setup, best models&lt;/td&gt;
&lt;td&gt;Per-token billing, telemetry leaves the box, breaks offline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I chose llama.cpp for three reasons. First, scriptability: the server is one process I start and stop from PowerShell, so the module can auto-start it on demand. Second, the OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint means my client code would survive a future engine swap unchanged. Third, reproducibility: the installer pins the engine version in &lt;code&gt;vars.ps1&lt;/code&gt; and downloads the exact binary it expects.&lt;/p&gt;

&lt;p&gt;If you run Ollama already, keep it. The client function in step 5 targets an HTTP contract, not a vendor. Point &lt;code&gt;$script:HawkLlamaUri&lt;/code&gt; at Ollama's OpenAI shim and everything downstream works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; decide your engine before writing client code, and write down its URI and health-check endpoint. That decision document will save you an hour later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: A Module Skeleton That Scales
&lt;/h2&gt;

&lt;p&gt;Skip the single-file monolith. It feels faster and costs you later, because PowerShell modules load their &lt;code&gt;.psm1&lt;/code&gt; once and every edit during development means a full reload cycle. Instead, split concerns into numbered files and let the loader dot-source them in filename order:&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="c"&gt;# HawkwardHybrid.psm1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$domainFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$PSScriptRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'*.ps1'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$domainFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Export-ModuleMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s1"&gt;'Get-HawkConfig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Invoke-HawkAI'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Export-ModuleMember&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s1"&gt;'ai'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'fix'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'dash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numeric prefixes (&lt;code&gt;00-Core.ps1&lt;/code&gt;, &lt;code&gt;20-AI.ps1&lt;/code&gt;, &lt;code&gt;80-LegacyMatrix.ps1&lt;/code&gt;) enforce load order: core helpers before features that call them. One manifest, one version, one export surface.&lt;/p&gt;

&lt;p&gt;Now the low-level detail that bites everyone: PowerShell exports are a &lt;em&gt;contract between three places&lt;/em&gt;, and they drift independently. The manifest's &lt;code&gt;FunctionsToExport&lt;/code&gt; controls what &lt;code&gt;Import-Module&lt;/code&gt; exposes. The &lt;code&gt;.psm1&lt;/code&gt;'s &lt;code&gt;Export-ModuleMember&lt;/code&gt; controls the live session. And any alias map you keep in a separate file is a third list that can rot. During this project's final review wave, the manifest listed 90 aliases while the loader exported 57. Every direct &lt;code&gt;Import-Module&lt;/code&gt; consumer saw phantom commands. The fix became a permanent test: assert set equality between all three lists on every run, never a threshold like "at least 80".&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;$m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-Module&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;HawkwardHybrid&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Import-PowerShellDataFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\HawkwardHybrid.psd1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="err"&gt;.AliasesToExport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-notin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExportedAliases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Should&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-BeNullOrEmpty&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; create the folder, write a &lt;code&gt;Get-HelloOps&lt;/code&gt; function, wire the manifest, and prove the import path works before adding anything else. If you skip this and the parity test, you will meet the drift bug eventually, on a worse day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The Sensor Layer, or Talking to Your Motherboard
&lt;/h2&gt;

&lt;p&gt;CIM is the workhorse. Each sensor is a query plus formatting. Two examples show the whole pattern:&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-HawkRamInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_PhysicalMemory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;BankLabel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Speed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Manufacturer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Format-Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AutoSize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Get-HawkThermals&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$zones&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;root/wmi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ClassName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MSAcpi_ThermalZoneTemperature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$zones&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No ACPI thermal zones exposed by this hardware/driver stack."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DarkGray&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;$zones&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$celsius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CurrentTemperature&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;273.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$celsius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'CRITICAL'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$celsius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HOT'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                 &lt;/span&gt;&lt;span class="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$celsius&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ge&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'WARM'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'NOMINAL'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PSCustomObject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Zone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InstanceName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TemperatureC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$celsius&lt;/span&gt;&lt;span class="s2"&gt; C"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Format-Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AutoSize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth noticing in the thermal function, because each is a small lesson:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Kelvin conversion.&lt;/strong&gt; ACPI reports temperature in tenths of a degree Kelvin. Divide by 10, subtract 273.15. If your readings come back around 3000, you forgot the scale, and yes, that is exactly what the raw number looks like the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graceful degradation.&lt;/strong&gt; Many consumer boards expose no thermal zones without vendor drivers, and &lt;code&gt;Win32_Fan&lt;/code&gt; often returns nothing because fan curves live in the embedded controller, outside WMI's reach. Return a friendly dark-gray hint instead of an error. A diagnostics tool that errors on normal hardware states trains users to ignore it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objects, then format.&lt;/strong&gt; Emit &lt;code&gt;[PSCustomObject]&lt;/code&gt; and pipe through &lt;code&gt;Format-Table&lt;/code&gt; at the end. Anyone can then do &lt;code&gt;temps | Where-Object State -eq 'HOT'&lt;/code&gt; themselves. Format early and you've thrown away the data.&lt;/p&gt;

&lt;p&gt;The fork here: &lt;code&gt;Get-CimInstance&lt;/code&gt; versus &lt;code&gt;Get-WmiObject&lt;/code&gt;. The latter is deprecated and absent from PowerShell 7, so the choice is made for you. But within CIM there are still decisions. Some data lives in the default &lt;code&gt;root/cimv2&lt;/code&gt; namespace, thermals hide in &lt;code&gt;root/wmi&lt;/code&gt;, disk health sits in &lt;code&gt;root\Microsoft\Windows\Storage&lt;/code&gt; with class &lt;code&gt;MSFT_PhysicalDisk&lt;/code&gt;. I query the storage namespace directly rather than calling &lt;code&gt;Get-PhysicalDisk&lt;/code&gt;, which keeps behavior identical across Windows builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; write &lt;code&gt;battery health&lt;/code&gt; yourself. Query &lt;code&gt;Win32_Battery&lt;/code&gt;, compute &lt;code&gt;FullChargeCapacity / DesignCapacity * 100&lt;/code&gt;, round to one decimal, and handle the desktop case where the query returns nothing. Then try &lt;code&gt;Win32_Processor&lt;/code&gt;, &lt;code&gt;Win32_VideoController&lt;/code&gt;, and &lt;code&gt;Get-HotFix&lt;/code&gt; on your own machine. Ten functions in, you'll stop needing my snippets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Ninety Aliases Without Losing Your Mind
&lt;/h2&gt;

&lt;p&gt;Short names are the product. Nobody types &lt;code&gt;Get-HawkScheduledTaskRiskAudit&lt;/code&gt;; they type &lt;code&gt;taskaudit&lt;/code&gt;. But ninety &lt;code&gt;Set-Alias&lt;/code&gt; calls scattered across files become unmaintainable, so the entire alias surface lives in one ordered hashtable:&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;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HawkAliasMap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Invoke-HawkAI'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;dash&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Show-HawkDashboard'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;temps&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Get-HawkThermals'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;shield&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Get-HawkShield'&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# ... 86 more&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration happens twice, by design. Once in module scope at dot-source time, so &lt;code&gt;Export-ModuleMember -Alias&lt;/code&gt; publishes a truthful surface to anyone who imports the module. Once in global scope via &lt;code&gt;Set-HawkAliases&lt;/code&gt; at profile load, so interactive sessions get the shortcuts even without an explicit import. Same map, both scopes, one source of truth.&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Set-HawkAliases&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HawkAliasMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Set-Alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Global&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HawkAliasMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest objection: aliases die at the process boundary. Call &lt;code&gt;temps&lt;/code&gt; from a script or another tool and PowerShell resolves it only if the profile loaded. Native executables survive everywhere. If your audience lives in &lt;code&gt;cmd.exe&lt;/code&gt; and scheduled tasks, consider shipping a thin compiled CLI instead, or generating &lt;code&gt;.cmd&lt;/code&gt; shims. I stayed with aliases because the target user is me, inside PowerShell, dozens of times a day, and the dashboard-to-command guarantee (every tile renders a name that resolves) only needs in-shell truth.&lt;/p&gt;

&lt;p&gt;That guarantee is testable, by the way, and worth testing. Extract tile aliases from the dashboard source with a regex, diff them against the alias map, fail the suite on either direction of mismatch. This exact test caught dead tiles after I renamed functions without updating the menu.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; add three sensors of your own from step 2, give each an alias, and extend the parity test to cover them. Notice how the map format makes the dashboard update free: the menu iterates the same structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: A Dashboard Worth Staring At
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dash&lt;/code&gt; prints eleven suites, each a titled grid of emoji-tiled commands. The implementation is an ordered dictionary of suites, each holding a title, an icon, and rows of &lt;code&gt;(glyph, label, alias)&lt;/code&gt; triples, rendered by one loop. No TUI framework, no ncurses clone. Write-Host with colors covers it.&lt;/p&gt;

&lt;p&gt;Two low-level notes from the trenches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UTF-16 surrogate pairs.&lt;/strong&gt; Emojis above the Basic Multilingual Plane (the robot 🤖 is U+1F916) encode as two UTF-16 code units. If you probe render output character-by-character, remember &lt;code&gt;[char]0xD83E&lt;/code&gt; followed by &lt;code&gt;[char]0xDD16&lt;/code&gt; forms the glyph, and piping chars through some stringifications mangles them into decimal digits. Test with &lt;code&gt;.Contains($pair)&lt;/code&gt; on the joined pair.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Console encoding.&lt;/strong&gt; Wrap &lt;code&gt;[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()&lt;/code&gt; in try/catch. On some hosts the setter throws, and a crash inside your profile renderer poisons every session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; design your own suite row before looking at mine. Decide first what belongs on a glanceable menu and what deserves omission. Curation is the actual product skill here; rendering is twenty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Wiring the Local Model
&lt;/h2&gt;

&lt;p&gt;The AI hub starts with a server manager: download the pinned llama.cpp build (the installer handles this in &lt;code&gt;steps/01-engine.ps1&lt;/code&gt; and &lt;code&gt;02-model.ps1&lt;/code&gt;), place a GGUF model under &lt;code&gt;~\Models\GGUF&lt;/code&gt;, then start &lt;code&gt;llama-server.exe&lt;/code&gt; with a port and context flags. Health check via TCP probe with a retry loop, because the server accepts TCP connections a moment before it can serve.&lt;/p&gt;

&lt;p&gt;The client function carries the design weight. Its signature tells the story:&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Invoke-HawkAI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CmdletBinding&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ValueFromPipeline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$InputData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;HawkLlamaModelPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$TimeoutSec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$RedactSensitive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;switch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$PassThru&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c"&gt;# buffer pipeline input, stringify, redact, POST to /v1/chat/completions, stream reply&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipeline support is what makes it feel native. &lt;code&gt;Get-EventLog System -Newest 50 | ai "summarize failures"&lt;/code&gt; buffers objects through the process block, stringifies with &lt;code&gt;Out-String&lt;/code&gt;, and sends the bundle as evidence alongside your instruction. Streaming keeps perceived latency low; a 4B-parameter model on a modest GPU starts answering in about a second.&lt;/p&gt;

&lt;p&gt;Redaction runs before the payload leaves the process, always available, opt-in per call:&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'(?im)^(\s*[^=\r\n]*(?:secret|token|password|api.?key|private.?key)[^=\r\n]*\s*=\s*).+$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s1"&gt;'$1&amp;lt;REDACTED&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;plus a second pass for the JSON &lt;code&gt;"key": "value"&lt;/code&gt; shape. Regex redaction is beatable, and I say so in the docs. Structured secrets in weird formats can slip. But the common cases, dotenv lines and JSON config blobs, get caught, and the defense costs zero dependencies.&lt;/p&gt;

&lt;p&gt;Small local models bend prompt constraints, so expect to iterate on wording. Mine instructs the model to answer from provided data first, preserve units, stay concise, and suggest the smallest next check when unsure. Prompt engineering for 4B-class models is constraint engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fork number two: model choice.&lt;/strong&gt; Bigger is better until your RAM disagrees. A Q4-quantized 4B model runs in under 4 GB and answers fast; an 8B wants about 6 GB and reasons better; anything above 14B turns your copilot into a slideshow unless you own real VRAM. I ship with Qwen3-family GGUFs via the Unsloth quants, and the installer lets you pick. Benchmark on your machine: time ten &lt;code&gt;stat | ai&lt;/code&gt; calls at each size and let the numbers choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; implement the retry logic. On connection refused, wait two seconds, re-probe, auto-start the server if configured, and cap total attempts. My first version raced the server startup and failed at random, which is why &lt;code&gt;Start-HawkLlamaServer&lt;/code&gt; now polls instead of sleeping on a fixed timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Verbs That Do Something
&lt;/h2&gt;

&lt;p&gt;Raw AI chat is table stakes; the value shows in composed verbs. &lt;code&gt;fix&lt;/code&gt; is the best example, and it is twelve lines:&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Invoke-HawkShortFix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;Error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'No error found in $Error to remediate.'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Yellow&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="kr"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Last PowerShell error:&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;`n`n&lt;/span&gt;&lt;span class="s2"&gt;Invocation context:&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvocationInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PositionMessage&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;`n`n&lt;/span&gt;&lt;span class="s2"&gt;Extra context:&lt;/span&gt;&lt;span class="se"&gt;`n&lt;/span&gt;&lt;span class="nv"&gt;$Context&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-HawkAI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Instruction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Diagnose this PowerShell error and give a concrete fix (commands where possible). Be concise.'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RedactSensitive&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$Error[0]&lt;/code&gt; plus &lt;code&gt;InvocationInfo.PositionMessage&lt;/code&gt; gives the model the exception text &lt;em&gt;and&lt;/em&gt; the exact line where it threw. That pairing fixes most errors in one shot because the model sees what you saw.&lt;/p&gt;

&lt;p&gt;On top of these sit router functions with &lt;code&gt;ValidateSet&lt;/code&gt; parameters: &lt;code&gt;hub fix&lt;/code&gt;, &lt;code&gt;hub stat&lt;/code&gt;, &lt;code&gt;sysdiag temp&lt;/code&gt;, &lt;code&gt;auditdiag defender&lt;/code&gt;. A dispatcher is a switch statement over validated types, each case delegating to the underlying function. ValidateSet buys tab-completion and rejects typos before your switch ever runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; add an &lt;code&gt;explain&lt;/code&gt; verb that takes a command name, pulls its help with &lt;code&gt;Get-Help -Full&lt;/code&gt;, and asks the model to produce a plain-language explanation with two examples. You will need exactly one new concept beyond what this post covered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Memory That Redacts Itself
&lt;/h2&gt;

&lt;p&gt;The memory system stores notes as JSON Lines, one entry per line, at &lt;code&gt;Documents\PowerShell\Memory\hawk-memory.jsonl&lt;/code&gt;. Entries carry an id, typed tags (note/preference/fact/incident/command/link), a confidence score, a pinned flag, and the text. Search scores each term: plus two per term matching the text, plus one matching a tag, flat plus-two bonus for pinned entries, drop zeros, sort descending:&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$terms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$score&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-join&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$score&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pinned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$score&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fork everyone argues: why not embeddings? Vector search would find "that quantization thing" when you search "model compression formats", which term matching misses. My counterargument: this store holds hundreds of entries, not millions. Term scoring with tags covers recall at this scale, adds zero dependencies, stays human-readable when you open the file, and never needs an embedding model loaded. When the store crosses a few thousand entries, revisit. Ship the boring version first and write down the migration trigger. SQLite would also work and gives you real indexes; JSONL won because the whole database fits in one &lt;code&gt;Get-Content&lt;/code&gt; call and survives any editor.&lt;/p&gt;

&lt;p&gt;Every write funnels through the same redaction filter as AI payloads, so &lt;code&gt;mem api_key=sk-whatever&lt;/code&gt; stores &lt;code&gt;&amp;lt;REDACTED&amp;gt;&lt;/code&gt;. Test this in the suite: save a fake key, read the file, assert the secret is gone. That test has earned its keep.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; implement rotation. When the file passes N entries, archive the oldest unpinned third to a dated sidecar file. Decide what N should be and defend it in a comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: The Installer Problem Nobody Escapes
&lt;/h2&gt;

&lt;p&gt;Shipping a PowerShell product to other machines means answering an awkward question: which PowerShell? Windows still boots with 5.1, while serious modules want 7. My answer is a staged bootstrap: &lt;code&gt;install.ps1&lt;/code&gt; runs on whatever shell invoked it, checks for &lt;code&gt;pwsh&lt;/code&gt;, installs it via &lt;code&gt;winget&lt;/code&gt; if missing, then re-executes itself under 7 with the original arguments forwarded.&lt;/p&gt;

&lt;p&gt;After the handoff, numbered steps run in order: fetch the pinned engine, fetch the model, copy the module into the user's &lt;code&gt;Documents\PowerShell\Modules&lt;/code&gt;, write config JSON, install the hardened profile template, register an update check. Each step logs to a file and exits nonzero on failure, so automation can detect partial installs.&lt;/p&gt;

&lt;p&gt;The profile template deserves its own sentence: wrap every initialization phase in try/catch. A profile that throws strands users in a broken shell, and since agents wrote much of this code, defensive loading is non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Tests That Keep Everyone Honest
&lt;/h2&gt;

&lt;p&gt;Twenty-seven tests run in seconds, and they share one property: they touch nothing outside a staging directory. The suite redirects the PowerShell profile root to a temp location (&lt;code&gt;Set-HawkStagingRoot&lt;/code&gt;), isolates config via &lt;code&gt;HAWK_CONFIG_PATH&lt;/code&gt;, and treats any network touch as a defect. Early versions could hit real downloads mid-test; the fence went up, and now any run that touches the network fails.&lt;/p&gt;

&lt;p&gt;Beyond unit checks, the valuable tests are contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parity:&lt;/strong&gt; psd1 exports equal psm1 exports equal alias-map keys, set equality both directions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Surface:&lt;/strong&gt; every dashboard tile resolves to a registered alias; every alias appears in the manual's index; counts match the header claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke:&lt;/strong&gt; stage the real profile template, load it in a fresh process, assert &lt;code&gt;dash&lt;/code&gt;, &lt;code&gt;specs&lt;/code&gt;, and &lt;code&gt;ai&lt;/code&gt; resolve with zero errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; the manual mentions only exported functions; phantom references fail the build. (This test exists because earlier docs referenced commands I had retired. Docs lie without getting caught; tests make lying expensive.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you build with AI assistance, as I did, this suite is the difference between delegation and hope. Agents generate plausible code without hesitation. Set-equality gates catch the drift that confidence hides. My final review wave found 17 issues across ten reviewer passes; one round fixed eight of them because the verification commands existed and ran green afterward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your turn:&lt;/strong&gt; take whatever you built in steps 1 through 7 and write the parity test first, before the next feature. Then break something on purpose (rename a function without updating the manifest) and watch the right test fail with the right message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Ship It
&lt;/h2&gt;

&lt;p&gt;Documentation earns its push. Two artifacts mattered most: a README whose alias index is generated-checked against the manifest (all 90 present, zero phantoms), and a manual mapping every command to its underlying cmdlets and CIM classes. Writing the sensor map exposed three stale references and two phantom aliases in my own docs, which is the point: the audit is for you.&lt;/p&gt;

&lt;p&gt;GitHub-specific lessons, learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anchor links break when headings contain decorative characters. GitHub's slugger keeps trailing hyphens from &lt;code&gt;── SECTION ──&lt;/code&gt; styles, so &lt;code&gt;#features&lt;/code&gt; 404s while &lt;code&gt;#-features-&lt;/code&gt; resolves. Strip decorations from headings or compute slugs exactly.&lt;/li&gt;
&lt;li&gt;Tag the release: &lt;code&gt;git tag -a v12.0.0 -m "..."&lt;/code&gt; then &lt;code&gt;git push origin v12.0.0&lt;/code&gt;. Draft the Release page from the tag and paste real release notes; changelogs written from memory lie.&lt;/li&gt;
&lt;li&gt;Fill the About description and topics. Searchable repos get contributors; invisible ones get neither.&lt;/li&gt;
&lt;li&gt;If you publish to PSGallery, know that the legacy &lt;code&gt;Publish-Module&lt;/code&gt; shells out to &lt;code&gt;dotnet pack&lt;/code&gt; and demands a .NET SDK. The modern &lt;code&gt;Publish-PSResource&lt;/code&gt; packs in-process and skips that requirement. I skipped gallery publishing altogether; GitHub plus the installer covers my distribution needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where You Take It Next
&lt;/h2&gt;

&lt;p&gt;The natural extensions, about in order of payoff: scheduled &lt;code&gt;hawkdaily&lt;/code&gt; runs writing timestamped reports to disk; WMI event subscriptions pushing alerts instead of polling; more dispatch verbs as your sensor collection grows; swapping the term-scoring memory for embeddings when it earns the complexity. Fork the repo, keep what you use, delete the rest. Ninety aliases is a menu, and menus invite substitution.&lt;/p&gt;

&lt;p&gt;If you want the companion piece, the repository also documents how this whole product was built with an AI coding agent: phase maps, prompt patterns, the review loop, and a museum of failures that became guardrails (&lt;a href="https://github.com/shahriarhaqueabir/PowershellOps/blob/main/docs/AGENTIC-IDE-TUTORIAL.md" rel="noopener noreferrer"&gt;docs/AGENTIC-IDE-TUTORIAL.md&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Build your own suite. Break my defaults. The whole point of a local copilot is that it answers to you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>cli</category>
      <category>llm</category>
    </item>
    <item>
      <title>Building an AI-Augmented Web Portfolio with Qwen, WebLLM and WebGPU</title>
      <dc:creator>Shahriar Haque Abir</dc:creator>
      <pubDate>Sun, 09 Aug 2026 23:00:16 +0000</pubDate>
      <link>https://dev.to/shahriarhaqueabir/building-an-ai-augmented-web-portfolio-with-qwen-webllm-and-webgpu-36h</link>
      <guid>https://dev.to/shahriarhaqueabir/building-an-ai-augmented-web-portfolio-with-qwen-webllm-and-webgpu-36h</guid>
      <description>&lt;h2&gt;
  
  
  Building an AI-Augmented Web Portfolio with Qwen, WebLLM and WebGPU
&lt;/h2&gt;

&lt;p&gt;Build an AI-augmented web portfolio with Next.js 16.2.12, React 19, Tailwind CSS v4, WebLLM, WebGPU, and Qwen2.5–1.5B-Instruct running locally in the browser.&lt;/p&gt;

&lt;p&gt;Most portfolios are built to be browsed. You open the homepage, read the About section, look through a few projects, maybe download a résumé, scroll and leave.&lt;/p&gt;

&lt;p&gt;I wanted to change how visitors interact with a web portfolio. Not by putting a chatbot in the corner of the screen, but by making the AI part of the portfolio itself.&lt;/p&gt;

&lt;p&gt;So I built an &lt;strong&gt;AI-Augmented Web Portfolio&lt;/strong&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%2Fod6dh4u1m44s46hy0tql.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%2Fod6dh4u1m44s46hy0tql.png" alt="AI-Augmented Web Portfolio" width="799" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The website is still more or less a normal web application. There are pages, projects, experience, skills, navigation and all the things you'd expect from a typical portfolio. The difference is that there is another layer sitting on top of all that. A local AI system that can understand questions, find relevant information, navigate the portfolio and have a conversation with the visitor.&lt;/p&gt;

&lt;p&gt;Before we dive into this, let's take a moment to align ourselves with the way of thinking. We are not just throwing everything at the LLM. No, if someone says "show me your projects", I don't need an LLM to figure out what that means. Or if someone asks for a basic fact about my experience, I don't need an LLM for that either. The application handles those things itself. The model is there when a more contextual answer is useful. That became the basic idea behind the architecture:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't make the AI the application. Make the AI another component of the application.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework&lt;/strong&gt;: Next.js 16.2.12 (App Router, Static Generation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language&lt;/strong&gt;: TypeScript 5.8&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Library&lt;/strong&gt;: React 19.2 (Canary)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Runtime&lt;/strong&gt;: WebLLM (MLC-AI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Model&lt;/strong&gt;: Qwen2.5–1.5B-Instruct-q4f16_1-MLC (4-bit quantized)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inference&lt;/strong&gt;: WebGPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling&lt;/strong&gt;: Tailwind CSS v4 (CSS-first config)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation&lt;/strong&gt;: Motion v12 (formerly Framer Motion)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice&lt;/strong&gt;: Web Speech API (STT) + ElevenLabs (TTS via Proxy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment&lt;/strong&gt;: Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing&lt;/strong&gt;: Playwright&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use your own stack and I am sure there are plenty of alternatives out there but for me at the moment of developing this project, this stack was the best option available to my knowledge. The important part isn't any individual library. It's where the pieces run.&lt;/p&gt;

&lt;p&gt;The portfolio UI, routing logic, fallback engine and LLM inference run on the client. While the voice output is the exception: ElevenLabs is accessed through a small server-side proxy so the API key doesn't have to be exposed in the browser. The voice input and output is an additional accessibility feature and can be excluded if a truly serverless development is desired.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: A Cooperative Brain
&lt;/h2&gt;

&lt;p&gt;The first version of the idea was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Visitor
   ↓
LLM
   ↓
Answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked, but it created a problem.&lt;/p&gt;

&lt;p&gt;The model takes time to initialize. The browser has to load the model shards, and WebGPU has to initialize. Furthermore, there are plenty of situations where using an LLM is simply unnecessary.&lt;/p&gt;

&lt;p&gt;So the next question was "What can the application do when the model isn't available and when does the LLM step in?"&lt;/p&gt;

&lt;p&gt;Instead of making every interaction go through the model, the portfolio became a cooperative parallel handoff system across several tiers of intelligence.&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%2Fewus9s0wji26ksjm84pl.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%2Fewus9s0wji26ksjm84pl.png" alt="3 Tier handoff system" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application doesn't sit around waiting for the LLM to become ready. It does what it can immediately. When the model is available, it can take the interaction further.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Offloading Reasoning to a WebWorker
&lt;/h3&gt;

&lt;p&gt;Running an LLM in the browser is not exactly lightweight. There is model initialization, WebGPU setup, token generation, and streaming to deal with.&lt;/p&gt;

&lt;p&gt;I didn't want that work competing with the UI thread. So WebLLM runs inside a &lt;strong&gt;Web Worker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The worker owns the model instance and communicates with the main application through messages. Accessing WebGPU inside a Worker is the secret sauce, as it offloads the heavy GPGPU work from the main thread entirely.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/worker.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Qwen2.5-1.5B-Instruct-q4f16_1-MLC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebLLMEngineSingleton&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MLCEngine&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getInstance&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;CreateMLCEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;initProgressCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The singleton isn't there because singletons are fashionable. It's there because I only want one model engine managing the local inference session.&lt;/p&gt;

&lt;p&gt;If React causes the surrounding application to re-render, I don't want another initialization path accidentally creating another engine — triggering parallel ~950MB redownloads of the model weight shards and ballooning the ~1.6GB VRAM active execution footprint which is already quite heavy.&lt;/p&gt;

&lt;p&gt;The worker then sends progress and generated text back to the main thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;progress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setProgress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMessages&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;updateLastMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application consumes the results. The UI doesn't have to directly manage the model.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Don't Ask an LLM to Do Something a Router Can Do (Tier 1)
&lt;/h3&gt;

&lt;p&gt;One of the simplest things the assistant can do is navigate the portfolio.&lt;/p&gt;

&lt;p&gt;If someone says: &lt;em&gt;"Show me the projects."&lt;/em&gt; I don't need Qwen to reason about that.&lt;/p&gt;

&lt;p&gt;A small command router can handle it instantly. The router scores known keywords against the available views and returns the strongest match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/hooks/useCommandRouter.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ViewKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewMap&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;keywordScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kw&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;total&lt;/span&gt; &lt;span class="o"&gt;+&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;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&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;0&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;keywordScore&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bestView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestScore&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(),&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;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;bestScore&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bestScore&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="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's deliberately boring. And that's the point. This prevents the model from inventing a navigation decision when the application already knows the answer.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. The Fallback Engine (Tier 2)
&lt;/h3&gt;

&lt;p&gt;Navigation isn't the only thing that can bypass the model. There are plenty of questions where the portfolio already has the information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is Shahriar?&lt;/li&gt;
&lt;li&gt;What technologies does he use?&lt;/li&gt;
&lt;li&gt;What is his experience?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these, there is a lightweight intent engine that evaluates queries in &lt;code&gt;&amp;lt;5ms&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="c1"&gt;// src/lib/fallback-engine.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intentPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;who_is&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shahriar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;background&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answerAboutBio&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;skills&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tech stack&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;languages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;answerSkillsTools&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildFallbackAnswer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;intentPatterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;scoreIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lower&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="mf"&gt;0.3&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;hit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the application something useful to say even if the LLM isn't ready. The portfolio shouldn't feel broken during the several seconds the model takes to initialize.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Then Let the Model Take Over (Tier 3)
&lt;/h3&gt;

&lt;p&gt;Once the local model is ready, questions that don't fit the deterministic paths can be passed to Qwen.&lt;/p&gt;

&lt;p&gt;Instead of just matching: &lt;em&gt;"What are your skills?"&lt;/em&gt; the model can deal with something more open-ended:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm looking for someone with experience working across support, networking and AI tooling. What have you actually built that would be relevant?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much better use for an LLM.&lt;/p&gt;

&lt;p&gt;The application already has the portfolio data. The model's job is to turn that information into a useful response for the visitor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Known problem
    ↓
Deterministic solution

Unknown / contextual problem
    ↓
Local LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a much better division of labour.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Letting the Model Control the Interface
&lt;/h3&gt;

&lt;p&gt;I didn't just want the model to talk about the portfolio. I wanted it to be able to interact with the portfolio.&lt;/p&gt;

&lt;p&gt;If a visitor asks to see my projects, or asks a question where navigating to the projects section would be helpful, the model can emit a control directive such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INITIATING_NAVIGATION: projects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application detects that directive and performs the navigation itself. The model doesn't directly manipulate the React application.&lt;/p&gt;

&lt;p&gt;Because LLMs stream token-by-token, a tag might split across two chunks. We store the accumulated stream and check the full text rather than assuming a control directive will arrive in one chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/hooks/usePortfolioWorker.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawText&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INITIATING_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INITIATING_NAVIGATION:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rawText&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INITIATING_NAVIGATION:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;VALID_VIEWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onNavigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea is simple: &lt;strong&gt;The model proposes. The application validates.&lt;/strong&gt; Following this idea keeps the LLM inside a controlled boundary.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. The TTS and STT voice problem.
&lt;/h3&gt;

&lt;p&gt;The AI itself is local, but voice is different.&lt;/p&gt;

&lt;p&gt;We can also enable speech input for the portfolio by using the browser's Web Speech API. For speech output, I used ElevenLabs free tier.&lt;/p&gt;

&lt;p&gt;The TTS request goes through a stateless Next.js API route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   │
   │ text
   ▼
Next.js API route
   │
   │ API key stays here
   ▼
ElevenLabs
   │
   │ audio
   ▼
Browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API route protects our API keys and gives me somewhere to apply basic request controls. Requests are capped at 500 characters to protect quotas, and there is an ephemeral in-memory IP-based rate limit within the serverless module scope to prevent basic cost spikes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/api/tts/route.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RATE_LIMIT_MAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't pretending to be a production abuse-prevention system, but it's enough for this project. I hope…&lt;/p&gt;




&lt;h3&gt;
  
  
  7. The UI Shouldn't Have to Wait for AI
&lt;/h3&gt;

&lt;p&gt;This is probably the most important lesson from the project.&lt;/p&gt;

&lt;p&gt;A common pattern with AI interfaces is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Loading...
 ↓
AI
 ↓
Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wanted something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Application responds immediately
 ↓
Local model becomes available
 ↓
AI adds more context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If WebGPU isn't available, the deterministic parts can still work. If a question has an obvious answer, the application doesn't need to wake up a language model.&lt;/p&gt;




&lt;h3&gt;
  
  
  8. Testing the Handoff
&lt;/h3&gt;

&lt;p&gt;This kind of interface is slightly harder to test than a normal website.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User asks question
        ↓
Fallback answer appears
        ↓
Local model becomes available
        ↓
Model response streams
        ↓
Control directive detected
        ↓
Navigation occurs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used Playwright to test these interaction paths and make sure the handoff doesn't leave the interface in an awkward state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Trade-Off
&lt;/h2&gt;

&lt;p&gt;Instead of my server paying for every inference request, the visitor's machine does the work. That comes with some obvious trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model has to be downloaded.&lt;/li&gt;
&lt;li&gt;WebGPU support matters.&lt;/li&gt;
&lt;li&gt;Device memory and GPU capability matter.&lt;/li&gt;
&lt;li&gt;Performance is dependent on user's machine.&lt;/li&gt;
&lt;li&gt;Voice still relies on external services.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The goal was to reimagine a website where the visitor feels less like browsing a collection of pages and more like interacting with the persona behind it.&lt;/p&gt;

&lt;p&gt;That's ultimately what I wanted from the project.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://shahriarhaqueabirportfolio.vercel.app" rel="noopener noreferrer"&gt;https://shahriarhaqueabirportfolio.vercel.app&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/shahriarhaqueabir/ShahriarHaqueAbirPortfolio" rel="noopener noreferrer"&gt;https://github.com/shahriarhaqueabir/ShahriarHaqueAbirPortfolio&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>tutorial</category>
      <category>portfolio</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
