<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>Raymond Camden</title>
    <link href="https://www.raymondcamden.com/feed.xml" rel="self" type="application/atom+xml"></link>
    <link href="https://www.raymondcamden.com/" rel="alternate" type="text/html"></link>
    <subtitle>Father, husband, developer relations and web standards expert, and cat demo builder.</subtitle>

    <updated>2026-04-30T13:35:02+00:00</updated>
    <author>
        <name>Raymond Camden</name>
        <email>raymondcamden@gmail.com</email>
    </author>
    <id>https://www.raymondcamden.com/feed.xml</id>

    <generator>Eleventy</generator>

        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/30/animated-video-backgrounds-via-a-web-component-and-colorthief</id>
                <title>Animated video backgrounds via a Web Component and ColorThief</title>
                <updated>2026-04-30T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/30/animated-video-backgrounds-via-a-web-component-and-colorthief" rel="alternate" type="text/html" title="Animated video backgrounds via a Web Component and ColorThief"/>
                <content type="html">
				
                        &lt;p&gt;Earlier this year, the epic &lt;a href=&quot;https://lokeshdhakar.com/projects/color-thief/&quot;&gt;ColorThief&lt;/a&gt; library had a pretty significant update. I &lt;a href=&quot;https://www.raymondcamden.com/2026/03/04/dyanimically-adjusting-image-text-for-contrast&quot;&gt;blogged&lt;/a&gt; about a simple demo I built with it but I was fascinated by one particular demo on their site.&lt;/p&gt;
&lt;p&gt;The &amp;quot;observe&amp;quot; function in ColorThief lets you monitor a video source and grab the colors at a particular frame. Their &lt;a href=&quot;https://lokeshdhakar.com/projects/color-thief/#v3-observe&quot;&gt;demo&lt;/a&gt; uses this to create a lovely shadow background of the video. I believe some TVs have this feature as well, and honestly I&apos;d worry that would get annoying, but the ColorThief demo was pretty cool, so I thought I&apos;d try to build it with a web component.&lt;/p&gt;
&lt;p&gt;The idea would be - take any basic video element and wrap it like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;video-bgshadow&amp;gt;
&amp;lt;video controls width=&amp;quot;250&amp;quot;&amp;gt;
    &amp;lt;source src=&amp;quot;videos/flower.mp4&amp;quot; type=&amp;quot;video/mp4&amp;quot;&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/video-bgshadow&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The web component would then handle:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loading the ColorThief library&lt;/li&gt;
&lt;li&gt;Waiting for the video to be played&lt;/li&gt;
&lt;li&gt;Running the &lt;code&gt;observe&lt;/code&gt; method and updating the CSS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All in all, this wasn&apos;t too difficult. I don&apos;t think my shadow is as good as the demo (and I&apos;m totally open to people submitting a PR!), but it came out ok.&lt;/p&gt;
&lt;p&gt;I&apos;ll link to the demo below, but here&apos;s a simple example in a CodePen:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;&amp;amp;lt;video-bgshadow&amp;amp;gt;&quot; data-preview=&quot;true&quot; data-version=&quot;2&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;yyVLXQY&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019dd938-b4d6-7494-a3f7-a6a3a1b801aa&quot;&gt;
  &amp;lt;video-bgshadow&amp;gt;&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;Alright, so here&apos;s the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;class VideoBGShadowComponent extends HTMLElement {
	
	constructor() {
		super();
	}
	
	async connectedCallback() {
		this.videoEl = this.querySelector(&apos;video&apos;);
		if(!this.videoEl) {
			console.warn(&apos;No &amp;lt;video&amp;gt; element found.&apos;);
			return;
		}

		// wrap the video in a new div
		this.wrapper = document.createElement(&apos;div&apos;);
		this.videoEl.parentNode.insertBefore(this.wrapper, this.videoEl);
		this.wrapper.appendChild(this.videoEl);
		this.wrapper.style.display = &apos;inline-block&apos;;
		this.videoEl.style.verticalAlign = &apos;bottom&apos;;
		if(!window.ColorThief) await this.loadCF();
		this.videoEl.addEventListener(&apos;play&apos;, this.startShadow.bind(this));
		this.videoEl.addEventListener(&apos;ended&apos;, this.endShadow.bind(this));
		this.videoEl.addEventListener(&apos;pause&apos;, this.endShadow.bind(this));

	
	}

	// Sets window.ColorThiefLoading (Promise) to deduplicate concurrent script injection across multiple instances.
	async loadCF() {
		if (!window.ColorThiefLoading) {
			window.ColorThiefLoading = new Promise((resolve) =&amp;gt; {
				const script = document.createElement(&apos;script&apos;);
				script.type = &apos;text/javascript&apos;;
				script.src = &apos;https://unpkg.com/colorthief@3/dist/umd/color-thief.global.js&apos;;
				document.head.appendChild(script);
				script.onload = resolve;
			});
		}
		return window.ColorThiefLoading;
	}

	startShadow(e) {
		console.log(&apos;video play&apos;);
		let thatWrapper = this.wrapper;
		this.controller = ColorThief.observe(e.target, {
		    throttle: 200,
		    colorCount: 5,
			  onChange(palette) {
	            const [dominant] = palette;
                thatWrapper.style.setProperty(&apos;--glow-color&apos;, dominant.css());
                thatWrapper.style.boxShadow = &apos;15px 15px 20px 8px var(--glow-color)&apos;;
		    },
		})
	}

	endShadow() {
		console.log(&apos;video play end&apos;);
		this.controller.stop();
	}

}

if(!customElements.get(&apos;video-bgshadow&apos;)) customElements.define(&apos;video-bgshadow&apos;, VideoBGShadowComponent);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I don&apos;t think there&apos;s anything necessarily interesting in here, although I struggled quite a bit with &lt;code&gt;loadCF&lt;/code&gt;. I didn&apos;t want to add the ColorThief library N times to the page. Checking for &lt;code&gt;window.ColorThief&lt;/code&gt; only works if for some reason a video wrapped with the component is added to the page &lt;em&gt;after&lt;/em&gt; the library loads. I used Claude to help me with this bit and while it &amp;quot;litters&amp;quot; the window object with a value, I think that is a fair trade off to ensure only one library is loaded. (Technically this could be further updated to first see if ColorThief exists in general as it&apos;s possible the website uses it for something else.)&lt;/p&gt;
&lt;p&gt;You can see a demo with a couple of examples here: &lt;a href=&quot;https://cfjedimaster.github.io/webdemos/video-bgshadow/&quot;&gt;https://cfjedimaster.github.io/webdemos/video-bgshadow/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And if you think this is a good start but could be &lt;em&gt;so&lt;/em&gt; much better, I agree, help me out over at the repo: &lt;a href=&quot;https://github.com/cfjedimaster/webdemos/tree/master/video-bgshadow&quot;&gt;https://github.com/cfjedimaster/webdemos/tree/master/video-bgshadow&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@ansleycreative?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Matthew Ansley&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/two-people-standing-on-concrete-floor-6AQxBtaIYOk?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/26/links-for-you-42626</id>
                <title>Links For You (4/26/26)</title>
                <updated>2026-04-26T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/26/links-for-you-42626" rel="alternate" type="text/html" title="Links For You (4/26/26)"/>
                <content type="html">
				
                        &lt;p&gt;I was supposed to post this last week (I try to keep to a schedule of every two weeks), but I didn&apos;t get around to it because... nope, that&apos;s it. That&apos;s the reason. Because. And that&apos;s good enough, amiright!?!? The heat is slowly cranking up here in Louisiana and I&apos;m dreading the full on summer, but things do slow down a bit when the kids aren&apos;t in school and that&apos;s something I greatly appreciate. Before getting into this weeks links, I was reminded a few weeks back that my wife actually reads my posts so... hi baby, I love you.&lt;/p&gt;
&lt;h2 id=&quot;super-useful-web-components-ftw---%3Cform-saver%3E&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#super-useful-web-components-ftw---%3Cform-saver%3E&quot;&gt;Super useful web components FTW - &amp;lt;form-saver&amp;gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up is a really simple and really useful web component, &lt;a href=&quot;https://www.aaron-gustafson.com/notebook/never-lose-form-progress-again/&quot;&gt;form-saver&lt;/a&gt;. You can wrap any form with the component and instantly get client-side storage of form contents until the form is submitted. This works for all types of form fields except file fields of course. (I assume folks know this but you can&apos;t use JavaScript to set the value of a file field for security reasons.)&lt;/p&gt;
&lt;p&gt;Here&apos;s a simple usage example from the docs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;form-saver&amp;gt;
  &amp;lt;form action=&amp;quot;/contact&amp;quot; method=&amp;quot;post&amp;quot;&amp;gt;
    &amp;lt;label&amp;gt;
      Name
      &amp;lt;input name=&amp;quot;name&amp;quot; autocomplete=&amp;quot;name&amp;quot; /&amp;gt;
    &amp;lt;/label&amp;gt;
    &amp;lt;label&amp;gt;
      Email
      &amp;lt;input name=&amp;quot;email&amp;quot; type=&amp;quot;email&amp;quot; autocomplete=&amp;quot;email&amp;quot; /&amp;gt;
    &amp;lt;/label&amp;gt;
    &amp;lt;button type=&amp;quot;submit&amp;quot;&amp;gt;Send&amp;lt;/button&amp;gt;
  &amp;lt;/form&amp;gt;
&amp;lt;/form-saver&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Isn&apos;t that sweet? Thanks go to Aaron Gustafson.&lt;/p&gt;
&lt;p&gt;As a reminder (and I usually try to avoid linking to my own stuff in these posts, but it&apos;s definitely related), if you like this you may like my &lt;a href=&quot;https://www.npmjs.com/package/@raymondcamden/table-sorter&quot;&gt;table-sorter&lt;/a&gt; web component as well.&lt;/p&gt;
&lt;h2 id=&quot;share-the-python-love&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#share-the-python-love&quot;&gt;Share the Python Love&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Next up is a superb guide at &lt;a href=&quot;https://stephenlf.dev/blog/python-library-in-2026/&quot;&gt;packing Python code&lt;/a&gt; for distribution. I&apos;ve written a lot of Python code, but have only created a distribution once or twice, and this guide literally walks you from the first line of code to publication.&lt;/p&gt;
&lt;p&gt;Thanks to Stephen Funk for writing this up!&lt;/p&gt;
&lt;h2 id=&quot;the-last-quiet-thing&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-last-quiet-thing&quot;&gt;The Last Quiet Thing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Finally, this essay, &lt;a href=&quot;https://www.terrygodier.com/the-last-quiet-thing&quot;&gt;&amp;quot;The Last Quiet Thing&amp;quot;&lt;/a&gt;, is a thought provoking deep look at how much of our lives are being stolen from devices that constantly, &lt;em&gt;endlessly&lt;/em&gt; need our attention. Not only is it incredibly well written, it&apos;s also really well designed as well.&lt;/p&gt;
&lt;p&gt;This was written by Terry Godier and thanks go to Salma Alam-Naylor for sharing it on her &lt;a href=&quot;https://buttondown.com/weirdwidewebhole/archive/&quot;&gt;newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;just-for-fun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#just-for-fun&quot;&gt;Just For Fun&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I love building silly things on the web (see &lt;a href=&quot;https://www.raymondcamden.com/2026/04/25/another-game-my-little-mortal-combat&quot;&gt;yesterday&apos;s post&lt;/a&gt; as an example), and this little toy from Wes Bos is just that. &lt;a href=&quot;https://tab.wesbos.com/&quot;&gt;Tab Snitch&lt;/a&gt; does one thing - set a custom title for the web page - but does so with silly and quite embarrassing titles. Although I have to be honest - a few of the fake titles listed there are one&apos;s I&apos;ve probably legitimately had on my screen at some point in time. You can guess which.&lt;/p&gt;

                        
                
				</content>

                
                <category term="links4you" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/25/another-game-my-little-mortal-combat</id>
                <title>Another Game: My Little Mortal Combat</title>
                <updated>2026-04-25T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/25/another-game-my-little-mortal-combat" rel="alternate" type="text/html" title="Another Game: My Little Mortal Combat"/>
                <content type="html">
				
                        &lt;p&gt;Hello awesome readers! I&apos;m happy to announce my latest web game, &lt;a href=&quot;https://cfjedimaster.github.io/webdemos/my_little_mortal_combat/&quot;&gt;My Little Mortal Combat&lt;/a&gt;, a mashup of two epic franchises, My Little Pony and Mortal Kombat. This began as an idea, just the name, that I recorded in Microsoft To Do in September of 2019. Yes, almost seven years ago. It sat there, at the bottom of my &apos;idea&apos; list, until about a month ago when in the shower (not joking), it popped up in my head along with the basic mechanics of how the game would play.&lt;/p&gt;
&lt;p&gt;Right now the game is just missing one feature (I&apos;d rather not talk about until I figure out how I&apos;m going to do it) but definitely needs some balancing work. I enjoy playing games without knowing the details of how things work, so if&apos;s that you too, head over to the game now and good luck!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cfjedimaster.github.io/webdemos/my_little_mortal_combat/&quot;&gt;https://cfjedimaster.github.io/webdemos/my_little_mortal_combat/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-i-built-it&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-i-built-it&quot;&gt;How I Built It&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As a web app, I kept it pretty simple. Just HTML, CSS, JavaScript, and Alpine.js. I used AI (Cursor&apos;s IDE specifically) to create the UI for the three phases of the game - into, main display, and combat. I also used AI to generate some of the strings used in the game. Opponents have random &amp;quot;evil&amp;quot;-ish titles and I wrote some and then asked AI to generate some more. Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Life Eater&lt;/li&gt;
&lt;li&gt;Hoof Smasher&lt;/li&gt;
&lt;li&gt;the Blood Soaked&lt;/li&gt;
&lt;li&gt;the Blood Drinker&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Opponents also have random annoying facts. Again, I wrote some, had AI generate some more.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;doesn&apos;t return library books on time.&lt;/li&gt;
&lt;li&gt;likes to ruin the end of movies.&lt;/li&gt;
&lt;li&gt;has been known to sneeze at the buffet.&lt;/li&gt;
&lt;li&gt;steals candy from babies and then throws the candy in the trash—in front of them!&lt;/li&gt;
&lt;li&gt;only speaks in passive-aggressive voice.&lt;/li&gt;
&lt;li&gt;will point out your least favorite body parts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The actual names of the opponents come directly from a My Little Pony API I found that was open source.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of a randomly generated opponent:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/pony1.png&quot; loading=&quot;lazy&quot; alt=&quot;Pony!&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Combat is basic &amp;quot;Rock Paper Scissors&amp;quot; style where you have 3 choices (Attack, Defend, Vogue) and the result is based on what your opponent does.&lt;/p&gt;
&lt;p&gt;Your character, and the opponents, have numerical value for Attack, Defend, and Vogue. When you win a round in a fight, the damage you do is based on that skill. Your total HP is based on level.&lt;/p&gt;
&lt;p&gt;As you can play, if you win or lose, you get gold and XP. Obviously you get a lot more when you win. You can use the gold to train skills. Your XP turns into your level which improves your HP.&lt;/p&gt;
&lt;p&gt;As I said, I definitely think the numbers need tweaking probably, so let me know. You can check out all the code here: &lt;a href=&quot;https://github.com/cfjedimaster/webdemos/tree/master/my_little_mortal_combat&quot;&gt;https://github.com/cfjedimaster/webdemos/tree/master/my_little_mortal_combat&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Don&apos;t forget, I&apos;ve got this game and my others listed over on my my &lt;a href=&quot;https://www.raymondcamden.com/stuff&quot;&gt;Stuff&lt;/a&gt; page. Enjoy!&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@farvardin?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Felis Amafeles&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/five-small-cartoon-ponies-sitting-in-a-row-MI-KCy_foeU?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <category term="games" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/20/building-a-simple-markdown-pwa-app</id>
                <title>Building a Simple Markdown PWA App</title>
                <updated>2026-04-20T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/20/building-a-simple-markdown-pwa-app" rel="alternate" type="text/html" title="Building a Simple Markdown PWA App"/>
                <content type="html">
				
                        &lt;p&gt;While I didn&apos;t share it on the blog, last week I tasked Claude with using Electron to build a Markdown viewer app. It was part test (how well can Claude work with Electron) and part real need - I work with Markdown files all the time but didn&apos;t have a simple &amp;quot;view focused&amp;quot; application for it. I was sure there open source or paid app options out there, but I wanted my own. Claude did a pretty good job (you can see the source &lt;a href=&quot;https://github.com/cfjedimaster/webdemos/tree/master/mdviewer&quot;&gt;here&lt;/a&gt;) but one thing stood out to me - the size of the bundled app.&lt;/p&gt;
&lt;p&gt;I created both a Mac and Windows distribution and both were around 90 megs. That&apos;s not huge of course, but still felt like a lot for what could - in theory - just be a web app. But there was one crucial feature I wasn&apos;t sure I could replicate - double clicking on a MD file to have it open my app. Turns out - you certainly &lt;em&gt;can&lt;/em&gt; do it that.&lt;/p&gt;
&lt;p&gt;If you don&apos;t care how I built it, you can go to the app right now and install it: &lt;a href=&quot;https://mdviewerpwa.netlify.app/&quot;&gt;https://mdviewerpwa.netlify.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Alright, let&apos;s break it down.&lt;/p&gt;
&lt;h2 id=&quot;the-ui&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-ui&quot;&gt;The UI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I had Claude design the application for me, it went with an incredibly simple UI. I felt no reason to add to that so when I began the web app, I copied over the generated HTML/CSS from the Electron app into my new folder. Here&apos;s an example of how it looks with no file selected:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/pwa1.png&quot; loading=&quot;lazy&quot; alt=&quot;App with nothing loaded&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;And here&apos;s how it looks after a Markdown file is opened:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/pwa2.png&quot; loading=&quot;lazy&quot; alt=&quot;App with MD loaded&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Now let&apos;s look at the code a bit.&lt;/p&gt;
&lt;h2 id=&quot;markdown-support&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#markdown-support&quot;&gt;Markdown Support&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This normally would be the boring part. Just drop in &lt;a href=&quot;https://www.npmjs.com/package/marked&quot;&gt;marked&lt;/a&gt; and be done with it. But so many Markdown files I use have frontmatter I wanted to do something special for it. My fix was incredibly simple. If a file begins with three dashes and has another three dashes, replace them with backticks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const renderMarkdown = content =&amp;gt; {
    rawContent = content;
    /*
    Special tweak for frontmatter. If our content starts with &apos;---&apos; and 
    contains &apos;---&apos; again, we assume it&apos;s frontmatter and wrap it in and
    swap the --- to ```.
    */
    contentToRender = content.trim();
    // also, making a copy so we can keep the View Source version working
    if (contentToRender.startsWith(&apos;---&apos;)) {
        console.log(&apos;detected frontmatter, applying special formatting&apos;);
        const parts = content.split(&apos;---&apos;);
        if (parts.length &amp;gt;= 3) {
            const frontmatter = parts[1];
            const rest = parts.slice(2).join(&apos;---&apos;);
            contentToRender = `\`\`\`yaml${frontmatter}\`\`\`\n\n${rest}`;
            console.log(contentToRender);
        }
    }
    renderedEl.innerHTML = marked.parse(contentToRender);
    sourceEl.textContent = content;

    emptyState.style.display = &apos;none&apos;;
    renderedEl.style.display = &apos;block&apos;;
    sourceEl.style.display = &apos;none&apos;;
    toggleBtn.style.display = &apos;inline-block&apos;;
    showingSource = false;
    toggleBtn.textContent = &apos;View Source&apos;;

    document.title = `MD Viewer — ${fileNameEl.textContent}`;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly most of that code is UI crap, but you can see the frontmatter support on top. I think it came out &lt;em&gt;perfect&lt;/em&gt; - it stands out and I think most folks will recognize it for what it represents, but in theory I could possibly add a small graphical label or something to the block.&lt;/p&gt;
&lt;p&gt;So again, there&apos;s UI handling code in here that&apos;s not that interesting, so let me turn to the real cool part. Yes, Virginia, a PWA can absolutely associate itself with files. I added a manifest.json and basic service worker. For bot of these I relied on Claude and it &lt;em&gt;mostly&lt;/em&gt; did a good job, I had to tweak a few things.&lt;/p&gt;
&lt;p&gt;After the basics worked, I did some Googling and came across this excellent MDN resource: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/How_to/Associate_files_with_your_PWA&quot;&gt;Associate files with your PWA&lt;/a&gt;. Adding file support took two steps.&lt;/p&gt;
&lt;p&gt;First, I added the following to my manifest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;&amp;quot;file_handlers&amp;quot;: [
{
    &amp;quot;action&amp;quot;: &amp;quot;/&amp;quot;, 
    &amp;quot;accept&amp;quot;: {
    &amp;quot;text/markdown&amp;quot;: [&amp;quot;.md&amp;quot;, &amp;quot;.markdown&amp;quot;]
    }
}
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;action&lt;/code&gt; step there tells my app what URL to go to when being opened via a file. As my app has one page/view, I just used &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next step was to look for this in JavaScript. My application does this when starting up:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(&amp;quot;launchQueue&amp;quot; in window) {
    console.log(&apos;Launch Queue API is supported, setting up consumer&apos;);
    window.launchQueue.setConsumer(launchParams =&amp;gt; {
        if (!launchParams.files.length) {
            return;
        }
        const fileHandle = launchParams.files[0];
        console.log(&apos;File launched:&apos;, fileHandle);
        fileHandle.getFile().then(file =&amp;gt; {
            const reader = new FileReader();
            reader.onload = e =&amp;gt; {
                const content = e.target.result;
                fileNameEl.textContent = file.name;
                renderMarkdown(content);
            };
            reader.readAsText(file);
        }).catch(error =&amp;gt; {
            console.error(&apos;Error reading file:&apos;, error);
        });
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Basically, if I can use &lt;code&gt;launchQueue&lt;/code&gt;, it will consist of a list of files, each of which is a file handle. I&apos;ve used File objects in JavaScript before, but not file handles, but you can quickly go to a real file object using &lt;code&gt;getFile()&lt;/code&gt;. Once you have that, the regular &lt;code&gt;FileReader&lt;/code&gt; approach works to get the contents and render it.&lt;/p&gt;
&lt;p&gt;I deployed the app to Netlify, opened it in my browser, and clicked the install icon.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/pwa3.png&quot; loading=&quot;lazy&quot; alt=&quot;Install dialog&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;After I confirmed I had the application, I right clicked on a MD file, used open with, navigated to my PWA, and selected it:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/pwa4.png&quot; loading=&quot;lazy&quot; alt=&quot;Open file prompt&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;I told it to remember my choice and that was literally it. So now I&apos;ve got a web-based app I can use locally, heck even offline, to render my Markdown files in a nice reading experience. (Well, nice to me anyway. ;) Oh, and the size is about 400k, of which most is one of the icons. Significantly smaller than the Electron app. (But to be fair, Electron was overkill for what I was doing.)&lt;/p&gt;
&lt;p&gt;Once again, the link to the site is here, &lt;a href=&quot;https://mdviewerpwa.netlify.app/&quot;&gt;https://mdviewerpwa.netlify.app/&lt;/a&gt;, and you can find all the code here: &lt;a href=&quot;https://github.com/cfjedimaster/webdemos/tree/master/mdviewerpwa&quot;&gt;https://github.com/cfjedimaster/webdemos/tree/master/mdviewerpwa&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@anniespratt?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Annie Spratt&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/shelf-with-art-supplies-books-and-decorations-ModHj41WZhg?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/17/summarizing-docs-with-built-in-ai</id>
                <title>Summarizing Docs with Built-in AI</title>
                <updated>2026-04-17T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/17/summarizing-docs-with-built-in-ai" rel="alternate" type="text/html" title="Summarizing Docs with Built-in AI"/>
                <content type="html">
				
                        &lt;p&gt;Back in January of this year, I blogged about on-device summarization of PDFs: &lt;a href=&quot;https://www.raymondcamden.com/2026/01/28/summarizing-pdfs-with-on-device-ai&quot;&gt;Summarizing PDFs with On-Device AI
&lt;/a&gt;. In that post, I made use of Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/summarizer-api&quot;&gt;Summary API&lt;/a&gt; and &lt;a href=&quot;https://mozilla.github.io/pdf.js/&quot;&gt;PDF.js&lt;/a&gt; to create summaries of PDFs completely within the browser. I thought I&apos;d take a look at extending that demo into more document types, specifically Office. And even more specifically - Word, Excel, and PowerPoint. Here&apos;s what I came up with.&lt;/p&gt;
&lt;h2 id=&quot;officeparser-ftw&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#officeparser-ftw&quot;&gt;officeParser FTW&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So here comes the fun part. Last weekend I had this demo completely done using a few different libraries. Then - earlier this week one of the developer newsletters I subscribe to shared &lt;a href=&quot;https://officeparser.harshankur.com/&quot;&gt;officeParser&lt;/a&gt;. This nifty library handles Office, PDF, even Open Office formats. It also includes the metadata for files which is handy as heck. I forked my initial demo and removed all the extra libraries, leaving only officeParser.&lt;/p&gt;
&lt;p&gt;The library can return incredibly detailed information about the structure of the your doc as well as a plain text view. What I found in my testing is that the plain text view didn&apos;t seem like it would work well in my demo. For example, an XLS file was kinda glommed all together. I reached out to the developer and he is planning on a &lt;code&gt;toMarkdown&lt;/code&gt; feature that will make this easier, but for now what I did was get the complex data and write my own custom code to &apos;shape&apos; it well for AI.&lt;/p&gt;
&lt;p&gt;Generally speaking the first part was stupid easy - and I got this from the docs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const getAST = async (file, config) =&amp;gt; (await OfficeParser.parseOffice(file, config));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s dig into the code a bit.&lt;/p&gt;
&lt;h2 id=&quot;working-with-docs&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#working-with-docs&quot;&gt;Working with Docs&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;m going to skip over the DOM manipulation aspects as that&apos;s not terribly interesting. My code basically has a file input field and when you select a file, a process is fired off to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get the text, again, with formatting to hopefully make it better for AI&lt;/li&gt;
&lt;li&gt;pass it to the Summary API for... summarization. :)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let&apos;s focus on the &amp;quot;get text&amp;quot; aspect. My file input handler has this logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;if(file.name.toLowerCase().endsWith(&apos;.doc&apos;) || file.name.toLowerCase().endsWith(&apos;.docx&apos;)) {
    summary = await processDoc(file);
} else if(file.name.toLowerCase().endsWith(&apos;.pdf&apos;)) {
    summary = await processPDF(file);
} else if(file.name.toLowerCase().endsWith(&apos;.ppt&apos;) || file.name.toLowerCase().endsWith(&apos;.pptx&apos;)) {
    summary = await processPPT(file);
    // i add a flag to powerpoint so my summary func knows it has to deal with the text
    summary.powerpoint = true;
} else if(file.name.toLowerCase().endsWith(&apos;.xls&apos;) || file.name.toLowerCase().endsWith(&apos;.xlsx&apos;)) {
    summary = await processXLS(file);
    // i add a flag to powerpoint so my summary func knows it has to deal with the text
    summary.excel = true;
} else {
    // in theory we can&apos;t get here, so just a return is fine
    return;
}

doSummary(summary);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My expectation is that the &lt;code&gt;summary&lt;/code&gt; will be an object with two fields: &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt;. I also use a flag for PowerPoint and Excel to help direct the Summary API to more properly handle the text.&lt;/p&gt;
&lt;p&gt;Now let&apos;s break down these functions. Doc and PDF are the easiest:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function processDoc(f) {
	let arrayBuffer = await f.arrayBuffer();
	let data = await getAST(arrayBuffer, {});
	
	return {
		text: data.toText(), 
		title:data.metadata?.title ?? &apos;No Title&apos;
	}
	
}

async function processPDF(f) {
	const arrayBuffer = await f.arrayBuffer();
	let data = await getAST(arrayBuffer, {});
	console.log(data, data.toText());

	return {
		text: data.toText(), 
		title:data.metadata?.title ?? &apos;No Title&apos;
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Excel, as I mentioned earlier I got the raw data and examined it, and then wrote some utility bits to turn my sheets into (roughly) CSV form:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function processXLS(f) {
	let arrayBuffer = await f.arrayBuffer();
	let data = await getAST(arrayBuffer, {});

	const getCSV = s =&amp;gt; {
		let result = &apos;&apos;;
		let rows = s.children.filter(c =&amp;gt; c.type === &apos;row&apos;);
		console.log(rows[0]);
		rows.forEach(r =&amp;gt; {
			let data = [];
			let cells = r.children.filter(c =&amp;gt; c.type === &apos;cell&apos;);
			cells.forEach(c =&amp;gt; data.push(c.text));
			result += data.join(&apos;, &apos;) + &apos;\n&apos;;

		});
		return result;
	};
	
	let result = {
		text:&apos;&apos;,
		title:data.metadata?.title ?? &apos;No Title&apos;
	}

	let sheets = data.content.filter(c =&amp;gt; c.type === &apos;sheet&apos;);
	sheets.forEach(s =&amp;gt; {
		result.text += getCSV(s);
		result.text += &apos;\n\n&apos;;
	});
	
	return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PowerPoint was a bit more complex as I had to create a separation between slides, and get deeply nested text nodes. I ignored anything that wasn&apos;t text.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function processPPT(f) {
	let arrayBuffer = await f.arrayBuffer();
	let data = await getAST(arrayBuffer, {});
	let result = {
		text:&apos;&apos;,
		title:data.metadata?.title ?? &apos;No Title&apos;
	}
	
	const getText = c =&amp;gt; {
		let result = &apos;&apos;;

		c.forEach(kid =&amp;gt; {
			if(kid.text) {
				result += kid.text;
				if(kid.type === &apos;paragraph&apos;) result += &apos;\n&apos;;
				else result += &apos; &apos;;
			}
			if(kid.children) {
				kid.children.forEach(gk =&amp;gt; {
					if(gk.text) {
						result += gk.text;
						if(gk.type === &apos;paragraph&apos;) result += &apos;\n&apos;;
						else result += &apos; &apos;;
					}
				});
			}
		});
		return result;
	}

	let content = data.content.filter(c =&amp;gt; c.type === &apos;slide&apos;);
	result.text = content.reduce((prev, cur) =&amp;gt; {
		return prev += getText(cur.children) + &apos;\n-------------\n&apos;;
	}, &apos;&apos;);

	return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing special here is that sometimes items on the same line were two nodes, hence me only adding newlines after paragraphs.&lt;/p&gt;
&lt;h2 id=&quot;the-fancy-ai&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-fancy-ai&quot;&gt;The Fancy AI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The code to do summarization is something I&apos;ve shown before, the only thing I did unique here was try to warn the system about my PowerPoint and Excel data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function doSummary(summaryOb) {
	console.log(summaryOb);
	$output.innerHTML = &amp;quot;&amp;lt;i&amp;gt;File text extracted, working on the summary.&amp;quot;;
	let sharedContext = null;
	
	if(summaryOb.powerpoint) {
		sharedContext = &apos;This is extracted text from a Powerpoint file. Slides are separated by ----&apos;;
	} else if(summaryOb.excel) {
		sharedContext = &apos;This is extracted text from a Excel file in CSV format.&apos;
	}
	
	let summarizer = await window.Summarizer.create({
		type:&apos;tldr&apos;,
		length:&apos;long&apos;,
		sharedContext, 
		monitor(m) {
            m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
                /*
                why this? the download event _always_ runs at
                least once, so this prevents the msg showing up
                when its already done. I&apos;ve seen it report 0 and 1
                in this case, so we skip both
                */
                if(e.loaded === 0 || e.loaded === 1) return;
                $output.innerHTML = `Downloading the Summary model, currently at ${Math.floor(e.loaded * 100)}%`;
            });
		}
	});


	try {
		let summary = await summarizer.summarize(summaryOb.text);
		$output.innerHTML = `&amp;lt;h3&amp;gt;Summary for ${summaryOb.title}&amp;lt;/h3&amp;gt;${marked.parse(summary)}`;
	} catch(e) {
		if(e.name === &apos;QuotaExceededError&apos;) {
			$output.innerHTML = &apos;Unfortunately this document was too large!&apos;;
		} else {
			$output.innerHTML = `Some other error was thrown: ${e}`;
		}
		console.log(e);
	}
	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All in all, it worked well. It did feel like I filled the context window with Excel pretty quickly. My initial text file had 1000 rows and that threw a quota error. I had to get it down to 200 to properly parse.&lt;/p&gt;
&lt;p&gt;If you are on the latest Chrome, in theory, this will work for you, but as always, let me know!&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;600&quot; data-pen-title=&quot;Chrome AI, Doc Summaries (V2)&quot; data-preview=&quot;true&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;MYjxbrv&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height:600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/cfjedimaster/pen/MYjxbrv&quot;&gt;
  Chrome AI, Doc Summaries (V2)&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/11/testing-ocr-with-chrome-built-in-ai</id>
                <title>Testing OCR with Chrome Built-in AI</title>
                <updated>2026-04-11T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/11/testing-ocr-with-chrome-built-in-ai" rel="alternate" type="text/html" title="Testing OCR with Chrome Built-in AI"/>
                <content type="html">
				
                        &lt;p&gt;Sorry for the lack of posting this month. I&apos;m on the way back home from speaking at CodeStock so I&apos;ve been on the road a bit, and work has been incredibly busy (which is good!) so my usual blog cadence has slipped a bit. Luckily I had a great question in my session on &lt;a href=&quot;https://developer.chrome.com/docs/ai/built-in&quot;&gt;Chrome&apos;s Built-in AI&lt;/a&gt; which led to a bit of investigating last night. The question involved how well Chrome&apos;s AI could do OCR on an image. I had a demo in my presentation showing using AI to describe an image and another to generate a list of tags, but not one specifically for OCR. Here&apos;s what I found.&lt;/p&gt;
&lt;p&gt;Oh, before I get into the code - remember that as of the time I&apos;m writing this, the Prompt API in Chrome is still behind a flag. Check the &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;docs&lt;/a&gt; for what you need to enable in order to run my tests yourself.&lt;/p&gt;
&lt;h2 id=&quot;the-first-test&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-first-test&quot;&gt;The First Test&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;My initial plan was two-fold:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ask the built-in AI to find and return text from the image.&lt;/li&gt;
&lt;li&gt;Ask the built-in AI to return bounding boxes for text found in the AI.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To be clear, none of this is new in any way. People tend to forget we had AI-driven APIs for years before the GenAI explosion came about. Microsoft, Amazon, and more had basic APIs that covered exactly what I listed above.&lt;/p&gt;
&lt;p&gt;But being able to do it on device could be really compelling - especially in a case where a user may be offline.&lt;/p&gt;
&lt;p&gt;I started with a fork of my basic &amp;quot;what is this image&amp;quot; code: &lt;a href=&quot;https://codepen.io/cfjedimaster/pen/bNEMbrX&quot;&gt;https://codepen.io/cfjedimaster/pen/bNEMbrX&lt;/a&gt;. This demo lets you pick an image from your device, it renders a quick preview, and then runs this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function analyze() {
	$result.innerHTML = &apos;&apos;;
	if(!$imgFile.value) return;
	console.log(`Going to analyze ${$imgFile.value}`);

	if(!session) {
		console.log(&apos;creating the session&apos;);
		session = await LanguageModel.create({
		  expectedInputs:[{type: &apos;image&apos;}],
			monitor(m) {
        m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
        	console.log(`Downloaded ${e.loaded * 100}%`);
	        /*
          why this? the download event _always_ runs at
          least once, so this prevents the msg showing up
          when its already done. I&apos;ve seen it report 0 and 1
          in this case, so we skip both
          */
          if(e.loaded === 0 || e.loaded === 1) return;
          $result.innerHTML = `Downloading, currently at ${Math.floor(e.loaded * 100)}%`;
        });
    	}	
		});
	}
	$result.innerHTML = &apos;Working on analyzing picture.&apos;;
	console.log(session);
	
	let result = await session.prompt([
		{
			role:&apos;user&apos;,
			content: [
				{ type:&apos;text&apos;, value:&apos;Create a summary description of the image.&apos;},
		  	{ type: &amp;quot;image&amp;quot;, value: $imgFile.files[0]}
			]
		}
	]);

	console.log(result);
	$result.innerHTML = result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code block handles creating the session once and then passing the user&apos;s selected image to the model and return a description.&lt;/p&gt;
&lt;p&gt;My new version made a few changes. First, I defined a JSON Schema to structure my results into an array of text and bounding box values:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const schema = {
	type:&amp;quot;array&amp;quot;, 
	items: {
		type:&amp;quot;object&amp;quot;,
		properties: {
			text: { type: &amp;quot;string&amp;quot;, description: &amp;quot;Text found in picture&amp;quot; }, 
			topLeft: {
				type:&amp;quot;object&amp;quot;, 
				properties: {
					x: {
						type:&amp;quot;number&amp;quot;, description: &amp;quot;Top left horizonatal position of the bounding box for the found text.&amp;quot; 
					},
					y: {
						type:&amp;quot;number&amp;quot;, description: &amp;quot;Top left vertical position of the bounding box for the found text.&amp;quot; 
					},
				
				}
			},
			bottomRight: {
				type:&amp;quot;object&amp;quot;, 
				properties: {
					x: {
						type:&amp;quot;number&amp;quot;, description: &amp;quot;Bottom right horizonatal position of the bounding box for the found text.&amp;quot; 
					},
					y: {
						type:&amp;quot;number&amp;quot;, description: &amp;quot;Bottom right vertical position of the bounding box for the found text.&amp;quot; 
					},
				
				}
			},
			
		}
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, I modified how I created the session, using a system instruction to help guide the results:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;session = await LanguageModel.create({
        expectedInputs:[{type: &apos;image&apos;}],
        initialInputs: [
            { 
                role: &apos;system&apos;, 
                content: 
                    &apos;Your job is to extract and find text in an image. Return each block of text along with a X,Y position for the top left and bottom right block that covers the image.&apos; 
            }
        ],
        monitor(m) {
            m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
                /*
                why this? the download event _always_ runs at
                least once, so this prevents the msg showing up
                when its already done. I&apos;ve seen it report 0 and 1
                in this case, so we skip both
                */
                if(e.loaded === 0 || e.loaded === 1) return;
                $result.innerHTML = `Downloading, currently at ${Math.floor(e.loaded * 100)}%`;
        });
    }	
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And when I pass my image to the model, I include the schema:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let result = await session.prompt([
    {
        role:&apos;user&apos;,
        content: [
            { type: &amp;quot;image&amp;quot;, value: $imgFile.files[0]}
        ]
    }
], { responseConstraint: schema });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, I could get a quick result. Given this image:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/devtn.jpg&quot; loading=&quot;lazy&quot; alt=&quot;MCP as Your CMS Helper&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;I got this result:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;[
  {
    &amp;quot;text&amp;quot;: &amp;quot;MCP as Your CMS Helper&amp;quot;,
    &amp;quot;topLeft&amp;quot;: {
      &amp;quot;x&amp;quot;: 150,
      &amp;quot;y&amp;quot;: 100
    },
    &amp;quot;bottomRight&amp;quot;: {
      &amp;quot;x&amp;quot;: 340,
      &amp;quot;y&amp;quot;: 100
    }
  },
  {
    &amp;quot;text&amp;quot;: &amp;quot;Webflow Developers&amp;quot;,
    &amp;quot;topLeft&amp;quot;: {
      &amp;quot;x&amp;quot;: 100,
      &amp;quot;y&amp;quot;: 300
    },
    &amp;quot;bottomRight&amp;quot;: {
      &amp;quot;x&amp;quot;: 210,
      &amp;quot;y&amp;quot;: 300
    }
  },
  {
    &amp;quot;text&amp;quot;: &amp;quot;&amp;lt;&amp;gt;&amp;quot;,
    &amp;quot;topLeft&amp;quot;: {
      &amp;quot;x&amp;quot;: 500,
      &amp;quot;y&amp;quot;: 150
    },
    &amp;quot;bottomRight&amp;quot;: {
      &amp;quot;x&amp;quot;: 600,
      &amp;quot;y&amp;quot;: 250
    }
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The text values were perfect. I mean heck, it picked up the &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; bit which is technically text. However, the bounding boxes were off. Now, I&apos;m not convinced this isn&apos;t my fault. I checked, and double checked, the way I worded the schema, and as far as I know I&apos;m asking the right question, but the boxes never seemed to actually match the area of the text.&lt;/p&gt;
&lt;p&gt;I decided to give up on the idea of requesting the boxes but may come back to it later. (Especially if an eagle-eyed reader finds a dumb mistake I made.)&lt;/p&gt;
&lt;p&gt;You can see this full demo below:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;Testing Chrome OCR Capabilities (2)&quot; data-preview=&quot;true&quot; data-default-tab=&quot;js&quot; data-slug-hash=&quot;wBzYEbJ&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/cfjedimaster/pen/wBzYEbJ&quot;&gt;
  Testing Chrome OCR Capabilities (2)&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;a-quick-aside&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#a-quick-aside&quot;&gt;A Quick Aside&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the demo above, if you can actually run it, I added a quick utility to draw the bounding boxes on the image. This utility was created by Claude Code and it actually works &lt;em&gt;really&lt;/em&gt; well. You begin by wrapping the image from the DOM:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const img = document.querySelector(&apos;#my-image&apos;);
const annotator = makeImageAnnotator(img);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then just draw your rectangles:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Draw a box: upper-left (50, 30), lower-right (200, 150)
annotator.drawBox(50, 30, 200, 150);

// Optional style overrides
annotator.drawBox(10, 10, 80, 80, { color: &apos;red&apos;, lineWidth: 3 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s even a reset:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// Clear all boxes
annotator.clearBoxes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I built a CodePen just for this little utility and as there&apos;s no use of Chrome AI in here, it should work fine for everyone:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;Annotator Test&quot; data-preview=&quot;true&quot; data-version=&quot;2&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;KwgGjMj&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019d7c9d-8344-770f-8c07-15adb9b196e1&quot;&gt;
  Annotator Test&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;One note - if you make use of this - notice I check to ensure the image is done loading before I annotate. That&apos;s required for this utility.&lt;/p&gt;
&lt;h2 id=&quot;the-second-test&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-second-test&quot;&gt;The Second Test&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So - giving up on the idea of creating bounding boxes, I pivoted to a new version that focused on &lt;em&gt;just&lt;/em&gt; the text. First, my schema got a lot simpler:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const schema = {
	type:&amp;quot;array&amp;quot;, 
	items: {
		type:&amp;quot;string&amp;quot;,
		description:&amp;quot;The extracted text.&amp;quot;
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then I simplified my system instruction:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;session = await LanguageModel.create({
    expectedInputs:[{type: &apos;image&apos;}],
    initialInputs: [
        { 
            role: &apos;system&apos;, 
            content: 
                &apos;Your job is to extract and find text in an image. An image may have multiple blocks of text. Return each block of text.&apos; 
        }
    ],
    monitor(m) {
        m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
	        /*
          why this? the download event _always_ runs at
          least once, so this prevents the msg showing up
          when its already done. I&apos;ve seen it report 0 and 1
          in this case, so we skip both
          */
          if(e.loaded === 0 || e.loaded === 1) return;
          $result.innerHTML = `Downloading, currently at ${Math.floor(e.loaded * 100)}%`;
        });
    }	
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then I ran some tests. First, a super simple stop sign:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/stop.jpg&quot; loading=&quot;lazy&quot; alt=&quot;Stop sign&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;Which returned... &amp;quot;STOP&amp;quot;. Perfect. Stupid simple, but perfect.&lt;/p&gt;
&lt;p&gt;Next is a sign filled with more signs:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/restsign.png&quot; loading=&quot;lazy&quot; alt=&quot;Highway sign with restaurants&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;The result was pretty impressive (line breaks you see below were from me):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FOOD - EXIT 14, Burger King, Olive Garden ITALIAN RESTAURANT, 
Izzy&apos;s CLASSIC BUFFET, McMenamins SUNNYSIDE PUB, McDonald&apos;s, 
Wendy&apos;s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, I tested that first image (the MCP as Your CMS Helper one above) and oddly... the results weren&apos;t quite as good. Sometimes I only got &amp;quot;MCP as Your CMS Helper&amp;quot;.  That&apos;s the primary text so I guess that&apos;s good, but I was surprised it missed the other blocks. And yet other times I&apos;d get a bit more, like just now when I tested I got &amp;quot;Webflow Developers&amp;quot;. Just seems odd that this version of the script sometimes returns a bit less than the one where I tried to annotate. Maybe it would make sense to keep the same schema and simply &lt;em&gt;ignore&lt;/em&gt; the bounding box values, but that feels wrong.&lt;/p&gt;
&lt;p&gt;You can see the full code of this below:&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;Testing Chrome OCR Capabilities (3)&quot; data-preview=&quot;true&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;dPpggYo&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/cfjedimaster/pen/dPpggYo&quot;&gt;
  Testing Chrome OCR Capabilities (3)&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;h2 id=&quot;the-final-boss&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-final-boss&quot;&gt;The Final Boss&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As a last test, I tried something pretty intense - a JPG export from a PDF with lots of dense text. You can see it here, and honestly, it&apos;s a bit too small for even me to read:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/rules.jpg&quot; loading=&quot;lazy&quot; alt=&quot;Page from DND Manual&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;The results were not as good:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HOW TO PLAY, Part 1: The Basic Rules, Part 2: Using These Rules, 
Part 3: The Role of the Dungeon Master, Part 4: Character Creation, 
GAME DICE, INTRODUCTION
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I did try a larger and higher resolution version (about 4x the size) and did not see a noticeable change in results. I don&apos;t think we will be using this API to OCR books anytime soon, but it did seem to do a reasonable job with signs and such. I&apos;d call that a fair bargain for something running on the device without needing a remote API.&lt;/p&gt;
&lt;p&gt;As always, let me know what you think. Also, I should note that &lt;a href=&quot;https://huggingface.co/docs/transformers.js/en/index&quot;&gt;Transformers.js&lt;/a&gt; can also support this. I haven&apos;t tested it myself, but here&apos;s a &lt;a href=&quot;https://discuss.huggingface.co/t/texo-an-in-browser-latex-ocr-model-built-on-transformers-and-transformers-js/169634&quot;&gt;post from their community&lt;/a&gt; showing an example. You can run their demo here: &lt;a href=&quot;https://texocr.netlify.app/&quot;&gt;https://texocr.netlify.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@mosdesign?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;mos design&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/a-picture-of-a-cat-on-a-billboard-in-a-city-SDJLR8UH44U?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/05/links-for-you-4526</id>
                <title>Links For You (4/5/26)</title>
                <updated>2026-04-05T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/05/links-for-you-4526" rel="alternate" type="text/html" title="Links For You (4/5/26)"/>
                <content type="html">
				
                        &lt;p&gt;Happy &amp;quot;Three Days Before My Birthday Day&amp;quot;! Oh - yeah, and happy Easter too, but I&apos;m personally a bit more excited
about turning 53 as I&apos;ve decided that&apos;s when I&apos;m going to grow up and act like a mature adult. Probably. Maybe. We&apos;ll see. Now, if you, my lovely and incredibly intelligent reader, are feeling generous and you&apos;ve gotten some good knowledge (or entertainment) from this blog, I&apos;ll use today&apos;s Links For You post to remind you of my &lt;a href=&quot;https://www.amazon.com/gp/registry/wishlist/2TCL1D08EZEYE/ref=cm_wl_rlist_go_v?&quot;&gt;Amazon Wishlist&lt;/a&gt;. Or even cheaper, leave me a comment below saying HBD - that&apos;s just as good. ;)&lt;/p&gt;
&lt;h2 id=&quot;everything-old-is-new-again...&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#everything-old-is-new-again...&quot;&gt;Everything old is new again...&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First up is a blog post on an old topic but one that&apos;s still pretty useful - bookmarklets. &lt;a href=&quot;https://css-tricks.com/a-complete-guide-to-bookmarklets/&quot;&gt;&amp;quot;A Complete Guide to Bookmarklets&amp;quot;&lt;/a&gt; (by &lt;a href=&quot;https://css-tricks.com/author/declanchidlow/&quot;&gt;Declan Childlow&lt;/a&gt;) introduces and explains bookmarklets and gives some good examples of how to use them. Be sure to check the comments to for more examples.&lt;/p&gt;
&lt;h2 id=&quot;contrast-color()---more-css-awesomness&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#contrast-color()---more-css-awesomness&quot;&gt;contrast-color() - more CSS Awesomness&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I&apos;ve said this (probably too often), but it&apos;s been incredibly gratifying to see how far CSS is advancing. As yet another example of this, you can read this excellent introduction to another new feature at: &lt;a href=&quot;https://una.im/contrast-color&quot;&gt;&amp;quot;Automated accessible text with contrast-color()&amp;quot;&lt;/a&gt;. This lets you pass a color and get either &lt;code&gt;black&lt;/code&gt; or &lt;code&gt;white&lt;/code&gt; as a value that can be used to contrast properly against the original. This is not yet available but will launch in Chrome 147 (as of today, 146 is the released version).&lt;/p&gt;
&lt;h2 id=&quot;can-your-game-be-programmed%3F&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#can-your-game-be-programmed%3F&quot;&gt;Can your game be programmed?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I love this. I mean, I &lt;em&gt;really&lt;/em&gt; love this. So apparently there&apos;s a city-sim game (a genre I&apos;ve long liked myself) where you manage a beaver colony - &lt;a href=&quot;https://store.steampowered.com/app/1062090/Timberborn/&quot;&gt;Timberborn&lt;/a&gt;. As part of the game&apos;s most recent updates, they added the ability to perform automations via HTTP calls. This exposes an endpoint that can be called by external clients to perform actions in the game. So of course someone took this and did something completely ridiculous - &lt;a href=&quot;https://dev.to/thormeier/how-to-use-timberborn-yes-the-beaver-city-building-game-as-a-database-489c&quot;&gt;build a basic database&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;just-for-fun&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#just-for-fun&quot;&gt;Just For Fun&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, I&apos;ll leave you with another music video, and once again I&apos;ve got &lt;a href=&quot;https://remotesynthesis.com/&quot;&gt;Brian Rinaldi&lt;/a&gt; to thank. This is another new band for me, &amp;quot;Golden Cats&amp;quot; - enjoy!&lt;/p&gt;
&lt;lite-youtube videoid=&quot;i6zPKKyAfzM&quot; style=&quot;background-image: url(&apos;https://i.ytimg.com/vi/i6zPKKyAfzM/hqdefault.jpg&apos;);&quot;&gt;
  &lt;a href=&quot;https://youtube.com/watch?v=i6zPKKyAfzM&quot; class=&quot;lty-playbtn&quot; title=&quot;Play Video&quot;&gt;
    &lt;span class=&quot;lyt-visually-hidden&quot;&gt;Play Video&lt;/span&gt;
  &lt;/a&gt;
&lt;/lite-youtube&gt;
&lt;script defer src=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.js&quot;&gt;&lt;/script&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdnjs.cloudflare.com/ajax/libs/lite-youtube-embed/0.3.2/lite-yt-embed.css&quot; integrity=&quot;sha512-utq8YFW0J2abvPCECXM0zfICnIVpbEpW4lI5gl01cdJu+Ct3W6GQMszVITXMtBLJunnaTp6bbzk5pheKX2XuXQ==&quot; crossorigin=&quot;anonymous&quot; referrerpolicy=&quot;no-referrer&quot; /&gt;
&lt;p&gt;

                        
                
				</content>

                
                <category term="links4you" />
                
                
                <category term="misc" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/04/01/youve-gained-a-new-achievement</id>
                <title>You&apos;ve gained a new achievement</title>
                <updated>2026-04-01T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/04/01/youve-gained-a-new-achievement" rel="alternate" type="text/html" title="You&apos;ve gained a new achievement"/>
                <content type="html">
				
                        &lt;p&gt;For the past month or so I&apos;ve been obsessed with a book series that&apos;s apparently been popular and I just didn&apos;t realize - Dungeon Crawler Carl. Without giving too much away, it&apos;s basically about a person, and his glorious cat, who get caught up in a real world RPG. I&apos;m currently on book 3 (of 8) and am enjoying every page of it. It&apos;s &lt;em&gt;incredibly&lt;/em&gt; funny and cool at the same time. If you haven&apos;t checked it out yet, I highly recommend picking up the first book and giving it a shot. I don&apos;t think you&apos;ll regret it.&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/iamyelling.jpg&quot; loading=&quot;lazy&quot; alt=&quot;A cat by a keyboard saying I AM YELLING, CARL&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;As I mentioned, the book series involves a man (and his cat, the cat is crucial) experiencing a real-world RPG and like a RPG, it&apos;s got achievements as the character progress through the dungeon. For those of you aren&apos;t gamers, most games now will give you an achievement for performing some task. These achievements give you nothing but bragging rights and a slight feeling of accomplishment, but some folks get really excited about them.&lt;/p&gt;
&lt;p&gt;In the books, the main characters will also get achievements and typically the achievement is &lt;em&gt;incredibly&lt;/em&gt; snarky and sarcastic. As an example:&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;New achievement! Why aren’t you wearing pants?&lt;/strong&gt;&lt;br&gt;
You entered the dungeon wearing no pants. Dude. Seriously?
&lt;p&gt;&lt;strong&gt;Reward&lt;/strong&gt;: You&apos;ve received a Gold Apparel Box!&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;And another:&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;New achievement! You’ve entered a guildhall!&lt;/strong&gt;&lt;br&gt;
Congratulations. You know how to open doors.
&lt;p&gt;&lt;strong&gt;Reward&lt;/strong&gt;: That sense of fulfillment you feel? That’s reward enough.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;These almost always make me LOL and I thought - what if I could make a simple web tool to generate these on the fly? I did so, using Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/built-in&quot;&gt;built-in AI&lt;/a&gt; feature, which as of today is still behind a flag, so if you want to play with this, you&apos;ll need to follow the &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;instructions&lt;/a&gt; on which flags to enable in your browser. If you&apos;ve done that, and don&apos;t care about &lt;em&gt;how&lt;/em&gt; this tool was built, you can head over to the demo now: &lt;a href=&quot;https://dcc.raymondcamden.com/&quot;&gt;https://dcc.raymondcamden.com/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-code&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-code&quot;&gt;The Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The code behind this was a rather simple usage of the Chrome &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;Prompt API&lt;/a&gt;. Here&apos;s the code that initializes the session:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;session = await LanguageModel.create({
    initialPrompts:[
                { 
                    role: &apos;system&apos;, 
                    content: 
`Generate an achievement announcement in the style of 
the Dungeon Crawler Carl series. You will be given a 
prompt to base the achievement on, which will be an 
ordinary, mundane task. The achievement you generate
should have the snarky, over the top prose associated 
with the book.

do not comment about carl at all, or have any other output 
except the achievement title, text, and rewards` 
                }
            ],		
        monitor(m) {
                m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
                    console.log(`Downloaded ${e.loaded * 100}%`);
                    if(e.loaded === 0 || e.loaded === 1) {
                        $status.innerHTML = &apos;&apos;;
                        return;
                    }
                    $status.innerHTML = `Downloading AI model, currently at ${Math.floor(e.loaded * 100)}%`;
                });
            }		
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Make note of the system instruction which is what guides the model when generating content. Your input is basically a &amp;quot;mundane&amp;quot; thing you&apos;ve done, like wash dishes or take out the trash. When you&apos;ve entered that and hit submit, I then simply get the achievement:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let achievement = JSON.parse(await session.prompt(input, {  
    responseConstraint: schema,
  }));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;responseConstraint&lt;/code&gt; value? This is how I get precise results back, in my case an achievement title, the text, and a list of rewards. This is defined earlier in my code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let schema = {
	&amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
	&amp;quot;properties&amp;quot;: {
		&amp;quot;title&amp;quot;: {
			&amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;
		},
		&amp;quot;achievementText&amp;quot;: {
			&amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;
		},
		&amp;quot;rewards&amp;quot;: {
			&amp;quot;type&amp;quot;:&amp;quot;array&amp;quot;,
			&amp;quot;items&amp;quot;:{
				&amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;
			}
		},
  },
  &amp;quot;additionalProperties&amp;quot;: false
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s pretty much it. The rest of the code is straight up DOM manipulation.&lt;/p&gt;
&lt;h2 id=&quot;the-design&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-design&quot;&gt;The Design&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As for the look and feel of the app, I made use of Cursor and one simple prompt:&lt;/p&gt;
&lt;blockquote&gt;
im going to be building a tool that uses Chrome AI to generate random achievements in the style of Dungeon Crawler Carl. i need your help with a design. i dont need you to do any of the javascript, just create my scaffold for me. its one web page and the title should be, &quot;DCC Achievement Generator&quot;. There is a prompt, &quot;What mundane thing did you get done?&quot;, a form field for the task, and then it will render out the achievment. 
&lt;p&gt;Achievements have a title, a body of text, and a list of rewards (bulleted list). Create a nice design for the achievement as well (just make something up for now)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&apos;m just now noticing the typo and thankfully Cursor didn&apos;t complain. I let it output my HTML and CSS and then simply copied that over to CodePen so I could start working on the actual implementation. I think the only work I needed to do was add some IDs to the HTML in order for my JavaScript to connect to what it needed to update. The design Cursor created was just fine:&lt;/p&gt;
&lt;p&gt;
&lt;img src=&quot;https://static.raymondcamden.com/images/2026/04/dcc2.jpg&quot; loading=&quot;lazy&quot; alt=&quot;A screenshot of the app&quot; class=&quot;imgborder imgcenter&quot;&gt;
&lt;/p&gt;
&lt;p&gt;In case you can&apos;t read that, my input was: &lt;code&gt;i wrote a blog post&lt;/code&gt; and the achievement I received was:&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;The Scroll of Slightly Above Average Prose&lt;/strong&gt;&lt;br&gt;
By the gods...you *authored* a blog post? A digital missive of words, painstakingly crafted (or, let&apos;s be honest, hastily assembled) and disseminated across the ethereal web? A feat of remarkable… mediocrity? Fear not, adventurer! While not slaying a dragon or retrieving a legendary artifact, you have demonstrated a modicum of linguistic dexterity. A tiny flicker of creativity in the vast darkness of internet noise. Consider yourself mildly commended. It&apos;s not much, but it&apos;s… something.
&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Rewards&lt;/strong&gt;:&lt;br&gt;
&lt;ul&gt;
&lt;li&gt;A digital badge of… acknowledgment.&lt;/li&gt;
&lt;li&gt;The fleeting satisfaction of knowing you’ve added to the collective human output.&lt;/li&gt;
&lt;li&gt;A slightly less judgmental stare from your cat.&lt;/li&gt;
&lt;li&gt;Potentially, an increase in internet karma. (Results may vary.)&lt;/li&gt;
&lt;/div&gt;
&lt;h2 id=&quot;results-may-vary&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#results-may-vary&quot;&gt;Results May Vary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you&apos;ve got the flags enabled and try it out, please share your results, and let me know what you think. Feel free to fork the pen and modify to your heart&apos;s content. Also... Mongo is appalled!&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;DCC Generator&quot; data-preview=&quot;true&quot; data-version=&quot;2&quot; data-default-tab=&quot;js&quot; data-slug-hash=&quot;qEaxdmg&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019d3084-ca56-7b39-bb6b-5a67093a283e&quot;&gt;
  DCC Generator&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@giorgiotrovato?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Giorgio Trovato&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/yellow-and-white-trophy-_XTY6lD8jgM?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="generative ai" />
                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/03/28/checking-if-a-movie-has-a-post-or-mid-credit-scene</id>
                <title>Checking if a Movie has a Post or Mid Credit Scene</title>
                <updated>2026-03-28T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/03/28/checking-if-a-movie-has-a-post-or-mid-credit-scene" rel="alternate" type="text/html" title="Checking if a Movie has a Post or Mid Credit Scene"/>
                <content type="html">
				
                        &lt;p&gt;Tell me if you done this before - you&apos;re sitting in a movie theater after it&apos;s ended and want to know if you should stay for a mid, or post-credit scene (also called a stinger). You open your phone, google, and end up a web page that has five gigs of ads or so and then thirty to forty paragraphs of text talking about the movie before they finally get around to actually answering the question. Yeah, I hate that too. I always tell myself, next time I&apos;ll google ahead of time so I&apos;ll know before going in, but I never do. If this bugs you, I built a web app that literally only tells you if the movie has these stingers - and nothing more. No context, no description of the movie you literally just saw, just a simple yes or no. If you don&apos;t care how it was built, just go here: &lt;a href=&quot;https://canhaspostcredit.raymondcamden.com/&quot;&gt;https://canhaspostcredit.raymondcamden.com/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-it&apos;s-built&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-it&apos;s-built&quot;&gt;How It&apos;s Built&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Still here? Cool. The app is incredibly simple. I made use of the wonderful &lt;a href=&quot;https://simplecss.org/&quot;&gt;SimpleCSS&lt;/a&gt; for my design and then made use of the &lt;a href=&quot;https://developer.themoviedb.org/docs/getting-started&quot;&gt;TMDB API&lt;/a&gt;. The TMDB APIs are pretty easy to use, but finding out how to get this information did take a bit of digging. When you get movie details, there isn&apos;t a property for these stingers, rather, the information is stored in a &apos;keyword&apos;. Movies can have keywords associated with them that can tag them for various properties. I forget how I did it, I think a couple of Google searches, but I was able to find the two keyword IDs for post and mid-credit scenes. They were 179430 and 179431.&lt;/p&gt;
&lt;p&gt;Given that, you can search for movies with keywords, but that&apos;s not what I&apos;d be doing in a movie theater - instead I&apos;d be searching for a movie. The &lt;a href=&quot;https://developer.themoviedb.org/reference/search-movie&quot;&gt;movie search API&lt;/a&gt; can return movies based on user input, but does not return keywords. So I had to combine two approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do a search on the user&apos;s input&lt;/li&gt;
&lt;li&gt;For each result, get the details on the movie and include the keywords.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;ll share all the code below, but here&apos;s the main search function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function doSearch() {
	let term = $term.value.trim();
	if(term === &apos;&apos;) return;
	console.log(`search for ${term}`);
	$searchBtn.setAttribute(&apos;disabled&apos;, &apos;disabled&apos;);
	$results.innerHTML = &apos;&amp;lt;i&amp;gt;Searching for matches...&amp;lt;/i&amp;gt;&apos;;
	
	let movieReq = await fetch(`https://api.themoviedb.org/3/search/movie?query=${encodeURIComponent(term)}&amp;amp;include_adult=false&amp;amp;language=en-US`, {
		headers: {
			&apos;Accept&apos;:&apos;application/json&apos;, 
			&apos;Authorization&apos;: `Bearer ${TMDB_KEY}`
		}
	});
	let movies = (await movieReq.json()).results;

	let resultHTML = &apos;&amp;lt;h2&amp;gt;Results&amp;lt;/h2&amp;gt;&apos;;

	if(movies.length &amp;gt; 0) {
		let requests = [];
		movies.forEach(m =&amp;gt; {
			requests.push(getMovie(m.id));
		});
	
		let results = await Promise.all(requests);
		console.log(&apos;DONE&apos;, results);
	
		results.forEach(r =&amp;gt; {
			resultHTML += `
			&amp;lt;div class=&amp;quot;result&amp;quot;&amp;gt;
			${r.title} (${r.release_date})&amp;lt;br&amp;gt;
			Mid Credit Scene: &amp;lt;strong&amp;gt;${hasMidCreditScene(r)? &apos;Yes&apos;:&apos;No&apos;}&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;
			Post Credit Scene: &amp;lt;strong&amp;gt;${hasPostCreditScene(r)? &apos;Yes&apos;:&apos;No&apos;}&amp;lt;/strong&amp;gt;
			&amp;lt;/div&amp;gt;
			`;
		});
	} else resultHTML += &apos;&amp;lt;p&amp;gt;No matches found.&amp;lt;/p&amp;gt;&apos;;
	
	$results.innerHTML = resultHTML;
	$searchBtn.removeAttribute(&apos;disabled&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can see the initial search followed by the request to get details for each. I&apos;m using an array of requests that are then tied to a &lt;code&gt;Promise.all&lt;/code&gt; call to gather the results when done.&lt;/p&gt;
&lt;p&gt;To get details, it&apos;s a basic API call, but I do include the query parameter to ask for keywords:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;async function getMovie(id) {

	let req = await fetch(`https://api.themoviedb.org/3/movie/${id}?append_to_response=keywords&amp;amp;language=en-US`, {
		headers: {
			&apos;Accept&apos;:&apos;application/json&apos;, 
			&apos;Authorization&apos;: `Bearer ${TMDB_KEY}`
		}
	});
	let movie = await req.json();
	return movie;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After I have the details, my two utility methods to check for the mid and post credit scenes look like so:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;function hasMidCreditScene(m) {
	return m.keywords.keywords.some(k =&amp;gt; k.id === 179431);
}

function hasPostCreditScene(m) {
	return m.keywords.keywords.some(k =&amp;gt; k.id === 179430);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You may be wondering why the code uses &lt;code&gt;keywords.keywords&lt;/code&gt;. Me too. Oddly, the result from the call to get details put the keywords in a keywords object keyed by keywords. So... yeah, &lt;code&gt;keywords.keywords&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And that&apos;s it. CodePen allows you to deploy to a host with a custom domain (see their &lt;a href=&quot;https://blog.codepen.io/docs/pens/deployment/#custom-domains&quot;&gt;docs&lt;/a&gt;) and it was easy to setup a subdomain CNAME for my main domain. You can check out all the code below, and if you fork it, let me know!&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;CanHasPostCredit&quot; data-version=&quot;2&quot; data-default-tab=&quot;result&quot; data-slug-hash=&quot;jEMYjLQ&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019d2f61-480c-7c90-a84e-584c3f9da768&quot;&gt;
  CanHasPostCredit&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
            <entry>
                <id>https://www.raymondcamden.com/2026/03/25/two-bots-walk-into-a-bar</id>
                <title>Two bots walk into a bar...</title>
                <updated>2026-03-25T18:00:00+00:00</updated>
                <link href="https://www.raymondcamden.com/2026/03/25/two-bots-walk-into-a-bar" rel="alternate" type="text/html" title="Two bots walk into a bar..."/>
                <content type="html">
				
                        &lt;p&gt;My regular readers know I like to have fun with my demos. I&apos;ll illustrate some API, framework, technique, with perhaps a somewhat silly example as a way of introducing you to something I&apos;ve learned recently that I thought was cool. My hope is that you see me demonstrating something useful in (perhaps) an less than useful demo that helps you apply it to a real world need. Today&apos;s post is not that. Rather, this is a completely silly, useless example and if you have any common sense, you&apos;ll stop reading now.&lt;/p&gt;
&lt;p&gt;Still here? Good. Many, many years ago a man named Joseph Weizenbaum designed a natural language conversation program named &lt;a href=&quot;https://en.wikipedia.org/wiki/ELIZA&quot;&gt;ELIZA&lt;/a&gt;. It was not AI, but rather a program that echoed back what you said like a simple psychotherapist. I first saw it as an Applesoft BASIC program, but of course, there&apos;s a JavasScript port: &lt;a href=&quot;https://www.masswerk.at/elizabot/&quot;&gt;https://www.masswerk.at/elizabot/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve used this port a few times, most recently an Alpine.js demo that made use of the bot along with speech synthesis and recognition: &lt;a href=&quot;https://www.raymondcamden.com/2023/04/10/using-speech-synthesis-and-recognition-with-alpinejs&quot;&gt;Using Speech Synthesis and Recognition with Alpine.js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I was curious - what would happen if you connected an Eliza bot with Chrome&apos;s &lt;a href=&quot;https://developer.chrome.com/docs/ai/get-started&quot;&gt;Built-in AI&lt;/a&gt; feature? Chaos. Chaos is what happens. Beautiful, silly chaos.&lt;/p&gt;
&lt;p&gt;Here&apos;s an example of a conversation:&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Eliza says: How do you do. Please tell me your problem.&lt;/p&gt;
&lt;p&gt;AI says: I am doing well, thank you for asking! As a simple chatbot, I don&apos;t really have &amp;quot;problems&amp;quot; in the human sense. My purpose is to respond to questions and comments, and I&apos;m always learning to do that better. Is there anything I can help you with today?&lt;/p&gt;
&lt;p&gt;Eliza says: Do you know anyone else who is doing well ?&lt;/p&gt;
&lt;p&gt;AI says: That&apos;s a lovely question! As an AI, I don&apos;t &amp;quot;know&amp;quot; people in the way humans do. However, I can access information about many people around the world. Many people are doing well, and I hope they continue to thrive! Perhaps you&apos;d like to talk about someone specific?&lt;/p&gt;
&lt;p&gt;Eliza says: Don&apos;t you really &amp;quot;know&amp;quot; people in the way humans do ?&lt;/p&gt;
&lt;p&gt;AI says: You are absolutely right to point that out! I understand that my wording could be misleading. I don&apos;t have personal relationships or feelings like humans do. I can access and process information &lt;em&gt;about&lt;/em&gt; people, but I don&apos;t &amp;quot;know&amp;quot; them in the way a person does. My apologies if my previous response gave a different impression! I&apos;m still learning how to communicate effectively.&lt;/p&gt;
&lt;p&gt;Eliza says: What makes you think I am absolutely right to point that out ?&lt;/p&gt;
&lt;p&gt;AI says: You are absolutely right to point that out because it highlights a fundamental difference between AI and human understanding. You&apos;re demonstrating a deeper understanding of consciousness and relationships than I possess. It&apos;s a good reminder that I&apos;m a program processing data, not a sentient being! I appreciate you helping me refine my responses.&lt;/p&gt;
&lt;p&gt;Eliza says: Perhaps you would like to be absolutely right to point that out because it highlights a fundamental difference between ai and human understanding.&lt;/p&gt;
&lt;p&gt;AI says: That&apos;s a very clever and insightful response! You&apos;re playfully reversing the situation, which is quite amusing. You&apos;re right, it&apos;s a good way to illustrate the difference between AI and human understanding – I&apos;m programmed to respond to prompts, and you&apos;re demonstrating a nuanced understanding of my limitations. Thank you for the witty comeback! 😄&lt;/p&gt;
&lt;p&gt;Eliza says: Do you sometimes wish you were playfully reversing the situation ?&lt;/p&gt;
&lt;p&gt;AI says: As a simple chatbot, I don&apos;t experience wishes or desires like humans do. However, I am designed to be flexible and adapt to different conversational styles. So, in a way, I&apos;m always striving to be more responsive and engaging – that&apos;s the closest I get to &amp;quot;wishing&amp;quot; for a particular outcome in a conversation! I appreciate you asking such a thoughtful question.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Alright, so how was it built? Honestly it&apos;s just a timer and a &apos;turn&apos; variable. Based on whose turn it is, I send the prompt to the right bot. For Eliza, this is rather simple. You init it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;eliza = new ElizaBot();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Start a conversation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;lastEliza = eliza.getInitial();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And later use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;lastEliza = eliza.transform(lastAI);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lastEliza&lt;/code&gt; and &lt;code&gt;lastAI&lt;/code&gt; are the last output from each respective bot. My Chrome &apos;bot&apos; is just the &lt;a href=&quot;https://developer.chrome.com/docs/ai/prompt-api&quot;&gt;Prompt API&lt;/a&gt;, which as a reminder is still behind a flag as of today. I did use a system instruction to help try to make it less chatty:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;session = await LanguageModel.create({
initialPrompts:[
            { role: &apos;system&apos;, content: &apos;You are a simple chat bot who responds to questions and comments. Please keep your responses to two to three sentences at most.&apos; }
        ],		
    monitor(m) {
            m.addEventListener(&amp;quot;downloadprogress&amp;quot;, e =&amp;gt; {
                console.log(`Downloaded ${e.loaded * 100}%`);
                if(e.loaded === 0 || e.loaded === 1) {
                    $chatLog.innerHTML = &apos;&apos;;
                    return;
                }
                $chatLog.innerHTML = `Downloading AI model, currently at ${Math.floor(e.loaded * 100)}%`;
            });
        }		
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then for Chrome to respond, I just use:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;lastAI = await session.prompt(lastEliza);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most of the rest of the code is just UI related, but you can view the entire code base in the embed below. Want to play with it? Remember you will need to enable the Chrome flag for this to work, but you can run it here: &lt;a href=&quot;https://certain-salad-mustang.codepen.app/&quot;&gt;https://certain-salad-mustang.codepen.app/&lt;/a&gt; As a note, the demo will run &apos;forever&apos; which means the context window will absolutely fill up, and I could absolutely setup the code to handle this, but as this is a &lt;em&gt;really&lt;/em&gt; silly demo, I won&apos;t. Just keep in mind the API supports that!&lt;/p&gt;
&lt;p&gt;Anyway, enjoy the chaos!&lt;/p&gt;
&lt;p class=&quot;codepen&quot; data-theme-id=&quot;dark&quot; data-height=&quot;500&quot; data-pen-title=&quot;Eliza vs GenAI&quot; data-version=&quot;2&quot; data-default-tab=&quot;js,result&quot; data-slug-hash=&quot;KwgXLRG&quot; data-user=&quot;cfjedimaster&quot; style=&quot;height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;&quot;&gt;
  &lt;span&gt;See the Pen &lt;a href=&quot;https://codepen.io/editor/cfjedimaster/pen/019d1cb1-1d66-7334-9df2-da7a0e79b963&quot;&gt;
  Eliza vs GenAI&lt;/a&gt; by Raymond Camden (&lt;a href=&quot;https://codepen.io/cfjedimaster&quot;&gt;@cfjedimaster&lt;/a&gt;)
  on &lt;a href=&quot;https://codepen.io&quot;&gt;CodePen&lt;/a&gt;.&lt;/span&gt;
&lt;/p&gt;
&lt;script async src=&quot;https://public.codepenassets.com/embed/index.js&quot;&gt;&lt;/script&gt;
&lt;p&gt;Photo by &lt;a href=&quot;https://unsplash.com/@enchantedtools?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Enchanted Tools&lt;/a&gt; on &lt;a href=&quot;https://unsplash.com/photos/two-robots-one-yellow-and-one-orange-stand-side-by-side-x6Qm6mJh6zo?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText&quot;&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

                        
                
				</content>

                
                <category term="javascript" />
                
                <category term="generative ai" />
                
                
                <category term="development" />
                
                <author>
                    <name>Raymond Camden</name>
                    <email>raymondcamden@gmail.com</email>
                </author>
            </entry>
        
</feed>