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!
I have an admission to make. Social share images for Pika were broken on Twitter/X, LinkedIn, and Apple Messages for months. And it made me sad. But in the past few months we got it fixed. And that made me very happy! We really love our Pika social share images. They are pretty. They are readable. They reflect the theme chosen by the blogger. They're great! When they work. One day we went to share one of our blog posts and noticed that Twitter/X wasn’t playing nicely with Pika. At the time that service had been repeatedly mucking with how it displayed links. Seemingly every week there was a new change. So we sat on it for a bit, but eventually we decided it was probably us, not them. Especially since a couple other services were also having issues. We tried many and various things to fix it. I’ll share those below so we can get to the point… The fix in our case was to make sure our server was returning the correct headers: Content-Type: text/html; charset=utf-8 We arrived at this fix when a customer pointed out that our Content-Type differed from other working services. Using the service HTTP Header Check, they shared output from our server that showed: Content-Type: */*; charset=utf-8 Most services were fine figuring this out. Twitter/X, LinkedIn, and Apple Messages were not fine figuring this out. Naturally I introduced this bug when fixing another bug. 🤦 What other things did we try? Our twenty-nine-comment thread on Basecamp proves we tried just about everything. We compared line-by-line our META tags between Pika and various other blogs (including this one right here) We tweaked those META tags about fifteen different ways for each little discrepancy that we detected We read all the documents We tried the card validator We tried LinkedIn's post inspector We tried LinkedIn's support (they sent us to their developer forums who in turn said “not our problem” and sent us to an abandoned area of Stack Overflow 🙄) We tweaked our robots.txt We played with charset=uft-8 vs charset=us-ascii
More in technology
Another day, another opportunity to rate my 2025 Apple predictions! iPad Here’s what I predicted would happen with the base iPad this year: I fully expect to see the 11th gen iPad in 2025, and I think it will come with a jump to the A17 Pro or
Home file servers can be very useful for people who work across multiple devices and want easy access to their documents. And there are a lot of DIY build guides out there. But most of them are full-fledged NAS (network-attached storage) devices and they tend to rely on single-board computers. Those take a long time […] The post A lightweight file server running entirely on an Arduino Nano ESP32 appeared first on Arduino Blog.
This weekend, a small team in Latvia won an Oscar for a film they made using free software. That’s not just cool — it’s a sign of what’s coming. Sunday night was family movie night in my home. We picked a recent movie, FLOW. I’d heard good things about it and thought we’d enjoy it. What we didn’t know was that as we watched, the film won this year’s Academy Award as best animated feature. Afterwards, I saw this post from the movie’s director, Gints Zilbalodis: We established Dream Well Studio in Latvia for Flow. This room is the whole studio. Usually about 4-5 people were working at the same time including me. I was quite anxious about being in charge of a team, never having worked in any other studios before, but it worked out. pic.twitter.com/g39D6YxVWa — Gints Zilbalodis (@gintszilbalodis) January 26, 2025 Let that sink in: 4-5 people in a small room in Latvia led by a relatively inexperienced director used free software to make a movie that as of February 2025 had earned $20m and won an Oscar. I know it’s a bit more involved than that, but still – quite an accomplishment! But not unique. Billie Eilish and her brother Phineas produced her Grammy-winning debut album When We All Fall Asleep, Where Do We Go? in their home studio. And it’s not just cultural works such as movies and albums: small teams have built hugely successful products such as WhatsApp and Instagram. As computers and software get cheaper and more powerful, people can do more with less. And “more” here doesn’t mean just a bit better (pardon the pun) – it means among the world’s best. And as services and products continue migrating from the world of atoms to the world of bits, creators’ scope of action grows. This trend isn’t new. But with AI in the mix, things are about to go into overdrive. Zilbalodis and his collaborators could produce their film because someone else built Blender; they worked within its capabilities and constraints. But what if their vision exceeded what the software can do? Just a few years ago, the question likely wouldn’t even come up. Developing software calls for different abilities. Until recently, a small team had to choose: either make the movie or build the tools. AI changes that, since it enables small teams to “hire” virtual software developers. Of course, this principle extends beyond movies: anything that can be represented symbolically is in scope. And it’s not just creative abilities, such as writing, playing music, or drawing, but also more other business functions such as scheduling, legal consultations, financial transactions, etc. We’re not there yet. But if trends hold, we’ll soon see agent-driven systems do for other kinds of businesses what Blender did for Dream Well Studio. Have you dreamed of making a niche digital product to scratch an itch? That’s possible now. Soon, you’ll be able to build a business around it quickly, easily, and without needing lots of other humans in the mix. Many people have lost their jobs over the last three years. Those jobs likely won’t be replaced with AIs soon. But job markets aren’t on track to stability. If anything, they’re getting weirder. While it’s early days, AI promises some degree of resiliency. For people with entrepreneurial drive, it’s an exciting time: we can take ideas from vision to execution faster, cheaper, and at greater scale than ever. For others, it’ll be unsettling – or outright scary. We’re about to see a major shift in who can create, innovate, and compete in the market. The next big thing might not come from a giant company, but from a small team – or even an individual – using AI-powered tools. I expect an entrepreneurial surge driven by necessity and opportunity. How will you adapt?
Gary Marcus: Hot Take: GPT 4.5 Is a Nothing Burger Half a trillion dollars later, there is still no viable business model, profits are modest at best for everyone except Nvidia and some consulting forms, there’s still basically no moat, and still no GPT-5. Any reasonable person
If you hear the term “generative art” today, you probably subconsciously add “AI” to the beginning without even thinking about it. But generative art techniques existed long before modern AI came along — they even predate digital computing altogether. Despite that long history, generative art remains interesting as consumers attempt to identify patterns in the […] The post This vending machine draws generative art for just a euro appeared first on Arduino Blog.