More from Good Enough
The second half of 2024 was definitely an inflection point in the world of software. Large Language Models (LLMs) and generative AI started to permeate products everywhere, from chatbots to operating systems, and at times it felt like everyone was taking part in a race to integrate some AI feature or other into their product. This seems to have been particularly true in the world of customer support. Whole businesses seem to have pivoted, turning AI into their central feature as if their very lives depended on it. Some taglines from well-known companies leave no doubt: The best AI Agent and AI-first Customer Service Platform Try our new AI integration! AI-first service. ... and I can see the appeal for some businesses. But personally, I hate talking to bot or AI customer service tools. Is there anything more frustrating than carefully explaining your issue, then inexplicably being railroaded through some set of pointless questions or regurgitated knowledge-base articles, desperately hoping that if you can only jump through all these hoops like a good little boy, you might be able to eventually get in touch with an actual person who can actually read and understand your question and actually help you at the end of the tortuous process? It makes my blood boil! And as these big players double down on AI, it feels clearer than ever that they are really only focussed on customers who are so big that they don’t need to care how frustrating their support processes are. Companies for whom support is a cost centre they are trying to minimise. Jelly takes a different position. Jelly is about connecting actual people having actual conversations — support requests, questions, and all other kinds of collaboration. We're a small company, too. We know that the communication between us and our customers, existing or potential, will be one of the biggest factors in our success. There's no way we want an AI agent representing us in those vital conversations. Our bet is that there are thousands of other small companies and groups who neither need nor want an AI agent sitting between them and the people they want to communicate with. If you've been looking for a way for your team to share an inbox and work together to talk to your users, customers, clients, collaborators, and anyone else -- try Jelly. It's the simplest, most elegant, most humane way to work on email as a team.
While building Pika’s Stream of posts layout, we had need to add the capability to manage excerpts in the Pika editor. These excerpts would be used to show a small portion of your post in a post stream while offering a “continue reading” link for readers to click to read the rest of your post. To add this capability we had to dig into extending the base open source library for our editor, Tiptap. First the Tiptap part Below is the full code of the extension. Primarily the extension detects if the user types {{ excerpt }}, {{ more }}, or WordPress’s <!--more--> and replaces that text with: <div data-type="excerpt" class="post-excerpt" contenteditable="false" draggable="true">↑ Excerpt ↑</div> With that HTML, we use CSS to style things like so: This extension is smart enough to know if an excerpt already exists in the editor, in that case disallowing another excerpt being created. The extension: import { Node, InputRule, nodeInputRule, mergeAttributes } from '@tiptap/core' /** * Look for on a line by itself, with all the whitespace permutations */ export const excerptInputRegex = /^\s*{{\s*excerpt\s*}}\s*$/i /** * Look for on a line by itself, with all the whitespace permutations */ export const moreInputRegex = /^\s*{{\s*more\s*}}\s*$/i /** * Look for classic WordPress <!--more--> tag */ export const wpMoreInputRegex = /^\s*<*![-—]+\s*more\s*[-—]+>\s*$/i const excerptText = '↑ Excerpt ↑' /** * Used to detect if an excerpt already exists in the editor */ const hasExistingExcerpt = (state) => { let hasExcerpt = false state.doc.descendants(node => { if (node.type.name === 'excerpt') { hasExcerpt = true return false // stop traversing } }) return hasExcerpt } /** * Disable excerpt button in toolbar if excerpt already exists in * the editor. Note that we use Tiptap with the Rhino editor for * Rails, which explains the rhino-editor selectors. Rhino: * https://rhino-editor.vercel.app/ */ const setExcerptButtonState = (editor) => { const button = editor.view.dom.closest('rhino-editor').querySelector('rhino-editor button[data-excerpt]') if (button) { button.classList.toggle('toolbar__button--disable', hasExistingExcerpt(editor.state)) button.disabled = hasExistingExcerpt(editor.state) } } /** * This custom InputRule allows us to make a singleton excerpt node * that short-circuits if an excerpt node already exists. */ export function excerptInputRule(config) { return new InputRule({ find: config.find, handler: ({ state, range, match }) => { if (hasExistingExcerpt(state)) { return } const delegate = nodeInputRule({ find: config.find, type: config.type, }) delegate.handler({ state, range, match }) }, }) } export const Excerpt = Node.create({ name: 'excerpt', group: 'block', content: 'inline+', inline: false, isolating: true, atom: true, draggable: true, selectable: true, onCreate() { setExcerptButtonState(this.editor) }, onUpdate() { setExcerptButtonState(this.editor) }, parseHTML () { return [{ tag: 'div[data-type="excerpt"]' }] }, renderHTML ({ HTMLAttributes }) { return ['div', mergeAttributes({ 'data-type': 'excerpt', class: 'post-excerpt' }, HTMLAttributes), excerptText] }, /** * Add insertExcerpt command that we can call from our custom * toolbar buttons. This command checks for an existing excerpt * before inserting a new one. */ addCommands() { return { insertExcerpt: () => ({ state, commands }) => { if (hasExistingExcerpt(state)) { return false } return commands.insertContent({ type: this.name, content: [{ type: 'text', text: excerptText }] }) }, } }, /** * Set up various detection for {{ excerpt }} etc text. */ addInputRules() { return [ excerptInputRule({ find: excerptInputRegex, type: this.type, }), excerptInputRule({ find: moreInputRegex, type: this.type, }), excerptInputRule({ find: wpMoreInputRegex, type: this.type, }), ] }, }) Now for the Rails part This will obviously need modified depending on your Tiptap environment. In our case, using the Rhino editor with Rails, here’s what we do… app/javascript/controllers/extensions/excerpt.js is where our extensions directory lives. We use importmaps to manage our JavaScript, so we need to pin our extensions directory there: pin_all_from "app/javascript/extensions", under: "extensions" We have a Stimulus controller for all of our Rhino enhancements. We need to import our extension there: import { Excerpt } from "extensions/excerpt" To add the extension, we do this in the function that we use to do all of our Rhino initialization (we pass the Rhino editor into this function): initializeEditor(rhino) { // snip rhino.addExtensions(Excerpt) // snip } And we add one function, which we will later call with our new Rhino toolbar button. This function calls the insertExcerpt command that we defined in our extension. insertExcerpt() { this.element.editor.chain().focus().insertExcerpt().run() } And finally here’s the button we add to our Rhino toolbar. Notice the data-action="click->rhino#insertExcerpt", which is calling the function above: <button slot="after-increase-indentation-button" class="rhino-toolbar-button toolbar__button--excerpt" type="button" data-role-tooltip="rhino-insert-excerpt" data-action="click->rhino#insertExcerpt" data-excerpt data-role="toolbar-item" tabindex="-1"> <role-tooltip class="toolbar__tooltip" id="rhino-insert-excerpt" part="toolbar__tooltip toolbar__tooltip--create-excerpt" exportparts="base:toolbar__tooltip__base, arrow:toolbar__tooltip__arrow"> Create Excerpt </role-tooltip> <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" part="toolbar__icon" viewBox="0 0 24 24" width="24" height="24" style="pointer-events: none;"> <path d="SNIP"/> </svg> </button> This is by no means a drop-in extension, but hopefully it helps someone else who is wanting to add this excerpt functionality to their Tiptap editor.
When Good Enough was in its infancy as a truly American LLC (formed in Delaware and representing one or two people who were only semi-serious about a business), it was fun to play around with building websites. Shawn and I were truly just playing and exploring, more than anything reminding ourselves that building software could be a satisfying activity. After a year of goofing around we were still enjoying it, but we were also running up against our limitations. Some things we were okay at, but many of our skills just weren’t that impressive. So began the journey to Good Enough’s next phase: a collective of Good Enough people. We could make some cool, if janky, web toys alone, but with a few more people to play with… Along came Lettini and Patrick and James and Cade. Each of us with a different set of skills and a different set of weaknesses. Things definitely did become a lot more interesting once we teamed up! When my weaknesses got in the way, there was someone else to step into that gap and show me how it’s done. Hopefully others agree that I’m able to help them in some of the areas where I have a little more experience. 🤞 That’s enough reading for you; now it’s time to listen. Lettini, James, and I were recently asked to have a conversation on the IndieRails podcast. We are very thankful to Jeremy and Jess for this opportunity to talk about some of Good Enough’s short history. And luckily for you, we hardly talk about Rails at all! Throughout our lovely discussion, the power of a team filled with complimentary skills kept resurfacing in my head. This experience cannot be recreated as a solo dev or by working on some project in my garage. The times where our skills don’t overlap makes this whole Good Enough experiment lovely and worthwhile. To my teammates, I thank you. You complete me!
Yesterday, Lettini took a chance and posted about Jelly on Hacker News, a discussion site notorious for it's mercurial population of tech-maybe-too-saavy experts. Jelly is a tough sell for some of them, those with the technical skill to pipe email at a low level through custom-built filters running on their own cloud servers. I'm not going to lie to you. I was pretty nervous. And yet... Jelly at the top of Hacker News last night. At the time of writing, we've had over 100 comments and 281 "points" We got a really lovely response! It was also a great opportunity for us to practice talking about Jelly, about why we built it, what it stands for, and why people should consider it over other tools or workflows. It gave us an opportunity to talk about our philosophy on pricing: For us, affordability is part of the product itself. We’re specifically building this not to hoover up every dollar on the table, but to serve smaller groups that have been left out in the cold by "bigger" tools, and who get screwed by per-seat pricing. We believe there are enough teams who fit this profile to be profitable. There’s a difference between making profit and maximizing profit. the capitalists will call us crazy, but we're not here to maximize profit. This really resonated: I love this. Seriously. This is such a refreshing perspective! I've always wondered if there's room for craftsmen to build quality products for smaller groups. Your focus on simple, well-designed software really resonates with me. Thanks for showing us a viable path. A lot of people really got the product and the design choices we've been making: I'm really liking the UX there! In sports-speak there's the "Whose got the ball" method to identify who is managing a topic...and the way this is executed - from what i saw in the video - seems really straight-forward to help answer that. I really like the way this landing page is designed. And I think it really highlights one of the sales points, which is that you are decent and reasonable. Good stuff. I'm going to send this around to some people. Of course, there were plenty of people offering their home-brewed alternatives that cover some of what Jelly does, setting up filters and forwarding and even using labels to "claim" messages. It's fascinating to see how other people have approached this, and the existence of so many different "solutions" demonstrates, to me, that this is a problem that really exists in the world, and that really needs a Good Enough solution that works for people whether they are tech-saavy or not. Anyway. Go try Jelly. It's approved by the smart folks at Hacker News. What are you waiting for?
More in technology
Introduction Selecting the RAM Opening up Replacing the RAM Reassembly References Introduction I do virtually all of my hobby and home computing on Linux and MacOS. The MacOS stuff on a laptop and almost all Linux work a desktop PC. The desktop PC has Windows on it installed as well, but it’s too much of a hassle to reboot so it never gets used in practice. Recently, I’ve been working on a project that requires a lot of Spice simulations. NGspice works fine under Linux, but it doesn’t come standard with a GUI and, more important, the simulation often refuse to converge once your design becomes a little bit bigger. Tired of fighting against the tool, I switched to LTspice from Analog Devices. It’s free to use and while it support Windows and MacOS in theory, the Mac version is many years behind the Windows one and nearly unusuable. After dual-booting into Windows too many times, a Best Buy deal appeared on my BlueSky timeline for an HP laptop for just $330. The specs were pretty decent too: AMD Ryzen 5 7000 17.3” 1080p screen 512GB SSD 8 GB RAM Full size keyboard Windows 11 Someone at the HP marketing departement spent long hours to come up with a suitable name and settled on “HP Laptop 17”. I generally don’t pay attention to what’s available on the PC laptop market, but it’s hard to really go wrong for this price so I took the plunge. Worst case, I’d return it. We’re now 8 weeks later and the laptop is still firmly in my possession. In fact, I’ve used it way more than I thought I would. I haven’t noticed any performance issues, the screen is pretty good, the SSD larger than what I need for the limited use case, and, surprisingly, the trackpad is the better than any Windows laptop that I’ve ever used, though that’s not a high bar. It doesn’t come close to MacBook quality, but palm rejection is solid and it’s seriously good at moving the mouse around in CAD applications. The two worst parts are the plasticy keyboard and the 8GB of RAM. I can honestly not quantify whether or not it has a practical impact, but I decided to upgrade it anyway. In this blog post, I go through the steps of doing this upgrade. Important: there’s a good chance that you will damage your laptop when trying this upgade and almost certainly void your warranty. Do this at your own risk! Selecting the RAM The laptop wasn’t designed to be upgradable and thus you can’t find any official resources about it. And with such a generic name, there’s guaranteed to be multiple hardware versions of the same product. To have reasonable confidence that you’re buying the correct RAM, check out the full product name first. You can find it on the bottom: Mine is an HP Laptop 17-cp3005dx. There’s some conflicting information about being able to upgrade the thing. The BestBuy Q&A page says: The HP 17.3” Laptop Model 17-cp3005dx RAM and Storage are soldered to the motherboard, and are not upgradeable on this model. This is flat out wrong for my device. After a bit of Googling around, I learned that it has a single 8GB DDR4 SODIMM 260-pin RAM stick but that the motherboard has 2 RAM slots and that it can support up to 2x32GB. I bought a kit with Crucial 2x16GB 3200MHz SODIMMs from Amazon. As I write this, the price is $44. Opening up Removing the screws This is the easy part. There are 10 screws at the bottom, 6 of which are hidden underneath the 2 rubber anti-slip strips. It’s easy to peel these stips loose. It’s als easy to put them back without losing the stickiness. Removing the bottom cover The bottom cover is held back by those annoying plastic tabs. If you have a plastic spudger or prying tool, now is the time to use them. I didn’t so I used a small screwdriver instead. Chances are high that you’ll leave some tiny scuffmarks on the plastic casing. I found it easiest to open the top lid a bit, place the laptop on its side, and start on the left and right side of the keyboard. After that, it’s a matter of working your way down the long sides at the front and back of the laptop. There are power and USB connectors that are right against the side of the bottom panel so be careful not to poke with the spudger or screwdriver inside the case. It’s a bit of a jarring process, going back and forth and making steady improvement. In addition to all the clips around the board of the bottom panel, there are also a few in the center that latch on to the side of the battery. But after enough wiggling and creaking sounds, the panel should come loose. Replacing the RAM As expected, there are 2 SODIMM slots, one of which is populated with a 3200MHz 8GDB RAM stick. At the bottom right of the image below, you can also see the SSD slot. If you don’t enjoy the process of opening up the laptop and want to upgrade to a larger drive as well, now would be the time for that. New RAM in place! It’s always a good idea to test the surgery before reassembly: Success! Reassembly Reassembly of the laptop is much easier than taking it apart. Everything simply clicks together. The only minor surprise was that both anti-slip strips became a little bit longer… References Memory Upgrade for HP 17-cp3005dx Laptop Upgrading Newer HP 17.3” Laptop With New RAM And M.2 NVMe SSD Different model with Intel CPU but the case is the same.
Today marks day 13 of using the iPhone 16e as my primary phone, and after this review goes live, I'll be moving my eSIM back to the 16 Pro that I use day to day. I intended to use this phone for a month before going back to
Forget GB Railways and GB Energy... how about GB Drones?