<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andres Urdaneta</title>
    <description>The latest articles on DEV Community by Andres Urdaneta (@untaught).</description>
    <link>https://dev.to/untaught</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F541948%2F8a99378b-1e1a-48b7-ae11-59bb75c6062e.PNG</url>
      <title>DEV Community: Andres Urdaneta</title>
      <link>https://dev.to/untaught</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/untaught"/>
    <language>en</language>
    <item>
      <title>How to Fix Random OpenAI 500 Errors in Rails Background Jobs Using retry_on</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Sun, 14 Sep 2025 13:56:29 +0000</pubDate>
      <link>https://dev.to/untaught/how-to-fix-random-openai-500-errors-in-rails-background-jobs-using-retryon-3g0o</link>
      <guid>https://dev.to/untaught/how-to-fix-random-openai-500-errors-in-rails-background-jobs-using-retryon-3g0o</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When you're building applications that rely on third-party APIs, one of the certainties is that those APIs will, at some point, fail. &lt;/p&gt;

&lt;p&gt;Network issues, transient server errors, or rate limiting can all lead to failed requests. A robust application needs to anticipate these failures and handle them gracefully.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll walk through a real-world scenario I recently encountered in one of my Rails projects.&lt;/p&gt;

&lt;p&gt;My app uses the &lt;code&gt;ruby-openai&lt;/code&gt; gem to interact with the OpenAI API, and I noticed that the background job responsible for generating the LLM responses was intermitently failing with a &lt;code&gt;Faraday::ServerError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll look at how I diagnosed the problem and used Rails' built-in features to make my background jobs more resilient.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: A Failing Background Job
&lt;/h2&gt;

&lt;p&gt;The issue started with jobs landing in my "failed" queue. The error was always the same: &lt;code&gt;Faraday::ServerError: the server responded with status 500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's a snippet of the stack trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/local/bundle/ruby/3.3.0/gems/faraday-2.13.1/lib/faraday/response/raise_error.rb:38:in &lt;span class="sb"&gt;`&lt;/span&gt;on_complete&lt;span class="s1"&gt;'
...
/rails/app/services/llm/assistant_response_service.rb:22:in `generate_response'&lt;/span&gt;
/rails/app/jobs/llm/assistant_response_job.rb:13:in &lt;span class="sb"&gt;`&lt;/span&gt;perform&lt;span class="s1"&gt;'
...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the &lt;code&gt;generate_response&lt;/code&gt; method responsible for making the API call to OpenAI&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# `app/services/assistant_response_service.rb`&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Llm::AssistantResponseService&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Llm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseOpenAiService&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_response&lt;/span&gt;
        &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="no"&gt;DEFAULT_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;input: &lt;/span&gt;&lt;span class="vi"&gt;@input_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="vi"&gt;@tool_registry&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registered_tools&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="ss"&gt;previous_response_id: &lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;previous_response_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="ss"&gt;verbosity: &lt;/span&gt;&lt;span class="s2"&gt;"low"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;parameters: &lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;handle_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the background job that was calling it&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# `app/jobs/llm/assistant_response_job.rb`&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Llm::AssistantResponseJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
    &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;chat: :chatbot&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;
        &lt;span class="n"&gt;chatbot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chatbot&lt;/span&gt;

        &lt;span class="no"&gt;Llm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AssistantResponseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="ss"&gt;input_message: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;chat: &lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;chatbot: &lt;/span&gt;&lt;span class="n"&gt;chatbot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;generate_response&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wasn't an error in my code, but an issue on OpenAI's end. However, my app wasn't handling it well. The job would try once, fail, and give up.&lt;/p&gt;

&lt;p&gt;The problem was that there wasn't any handling for &lt;code&gt;Faraday::ServerError&lt;/code&gt;. The job simply fails and is moved to the dead-letter queue, requiring manual intervention to retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Automatic Retries with Active Job
&lt;/h2&gt;

&lt;p&gt;The best way to handle transient errors like a 500 status is to simply &lt;strong&gt;try again&lt;/strong&gt; after a short delay. Fortunately, Rails makes this trivial with the &lt;code&gt;retry_on&lt;/code&gt; feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add Retries to the Job
&lt;/h3&gt;

&lt;p&gt;The first and most important change is to tell our job to retry when it encounters a &lt;code&gt;Faraday::ServerError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I modified &lt;code&gt;app/jobs/llm/assistant_response_job.rb&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/llm/assistant_response_job.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Llm::AssistantResponseJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
    &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

    &lt;span class="c1"&gt;# **********************&lt;/span&gt;
    &lt;span class="c1"&gt;# ADD THIS NEXT LINE ⬇️&lt;/span&gt;
    &lt;span class="c1"&gt;# **********************&lt;/span&gt;
    &lt;span class="n"&gt;retry_on&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;wait: :polynomially_longer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;attempts: &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;chat: :chatbot&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;
        &lt;span class="n"&gt;chatbot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chatbot&lt;/span&gt;

        &lt;span class="no"&gt;Llm&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AssistantResponseService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="ss"&gt;input_message: &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;chat: &lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;chatbot: &lt;/span&gt;&lt;span class="n"&gt;chatbot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;generate_response&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this single line, the job will now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Catch any &lt;code&gt;Faraday::ServerError&lt;/code&gt; that occurs during its execution.&lt;/li&gt;
&lt;li&gt;Automatically re-enqueue itself to be run again later.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Wait for a polynomially increasing amount of time between retries (&lt;code&gt;:polynomially_longer&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;:polynomially_longer&lt;/code&gt; is a built-in &lt;strong&gt;backoff strategy&lt;/strong&gt; for retries in Rails.  It makes the wait time between retries increasingly  grow using a formula based on the number of attempts so far: &lt;code&gt;wait_time = (executions ** 4) + (random_jitter) + 2&lt;/code&gt;. E.g:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- **First retry:** about **3 seconds**
- **Second retry:** about **18 seconds**
- **Third retry:** about **83 seconds**
- **Fourth retry:** much longer, and so on.

The idea is to give the system more and more time to recover before trying again, instead of hammering the failing api at a fixed interval.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Attempt this up to 3 times &lt;strong&gt;before&lt;/strong&gt; finally giving up and moving to the failed jobs queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This immediately makes our job much more robust.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Improve Error Logging
&lt;/h3&gt;

&lt;p&gt;While retrying is great, we still want to know when these errors are happening. To fix this, we need to rescue the error in the service, log it, and then &lt;em&gt;re-raise&lt;/em&gt; it so that the job's &lt;code&gt;retry_on&lt;/code&gt; handler can catch it.&lt;/p&gt;

&lt;p&gt;Here's the updated &lt;code&gt;generate_response&lt;/code&gt; method in &lt;code&gt;app/services/llm/assistant_response_service.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/llm/assistant_response_service.rb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Llm::AssistantResponseService&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_response&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;parameters: &lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handle_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;Faraday&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ServerError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- Add this rescue block&lt;/span&gt;
        &lt;span class="n"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- Log the error&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;-- Re-raise the exception&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="c1"&gt;# Log the error to a monitoring and error tracking service, e.g: Sentry&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key here is &lt;code&gt;raise e&lt;/code&gt;. If we just rescued the exception without re-raising it, the job would never know that an error occurred, and it wouldn't retry. By rescuing, logging, and re-raising, we get the best of both worlds: visibility into the errors and automatic retries.&lt;/p&gt;

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

&lt;p&gt;By combining Active Job's &lt;code&gt;retry_on&lt;/code&gt; with specific error handling and logging, we just built a resilient background job.&lt;/p&gt;

&lt;p&gt;Implementing this is incredibly effective for dealing with unreliable network requests to third-party services, guarantees that your users will have a smoother experience and you'll spend less time manually retrying failed jobs.&lt;/p&gt;

&lt;p&gt;Next time you're working with an external API, remember to ask yourself: "What happens if this fails?" and build in a resilient error-handling strategy from the start.&lt;/p&gt;

&lt;p&gt;If you enjoyed this tutorial, here's where to find more of my work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://untaught.dev" rel="noopener noreferrer"&gt;Untaught Blog&lt;/a&gt;&lt;br&gt;
&lt;a href="https://untaught.dev/how-to-fix-random-openai-500-errors-in-rails-background-jobs-using-retry_on/" rel="noopener noreferrer"&gt;Read Article Here&lt;/a&gt;&lt;br&gt;
&lt;a href="https://x.com/untaughtdev" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>rails</category>
      <category>openai</category>
      <category>ruby</category>
    </item>
    <item>
      <title>How to Build AI-Generated Loading Messages in Rails 8 with Hotwire &amp; Stimulus</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Wed, 27 Aug 2025 20:04:44 +0000</pubDate>
      <link>https://dev.to/untaught/how-to-build-ai-generated-loading-messages-in-rails-8-with-hotwire-stimulus-3c0o</link>
      <guid>https://dev.to/untaught/how-to-build-ai-generated-loading-messages-in-rails-8-with-hotwire-stimulus-3c0o</guid>
      <description>&lt;p&gt;Have you noticed how Claude Code shows funny loading messages while it’s working? Things like “Wizarding” or “Pontificating.”&lt;/p&gt;

&lt;p&gt;I think they’re fun. They give you a small taste of what user experiences with AI could look like in the future.&lt;br&gt;
In this tutorial, we’ll build our own version of that in Ruby on Rails.&lt;/p&gt;

&lt;p&gt;The app will generate dynamic loading messages with a little help from AI. Here’s a quick preview:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Ffinal_result.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Ffinal_result.gif" alt="final result" width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a user submits a form, we’ll start two background jobs. One will simulate a slow process, and the other will create a list of AI-powered loading phrases.&lt;/li&gt;
&lt;li&gt;While those jobs are running, we’ll show a few hard-coded loading messages.&lt;/li&gt;
&lt;li&gt;As soon as the AI phrases are ready, we’ll stream them to the browser, swap them in, and shuffle them with JavaScript.
Finally, once the slow process is done, we’ll show a “success” message.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Before we start: What are we using?
&lt;/h2&gt;

&lt;p&gt;For this tutorial, we’ll be using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ruby on Rails 8&lt;/strong&gt; with &lt;strong&gt;Solid Queue&lt;/strong&gt; and &lt;strong&gt;Solid Cable&lt;/strong&gt; — to run background jobs and stream updates to the browser in real time.
&amp;gt; I won’t cover how to set up Solid Queue and Solid Cable here. I’ll link a separate guide soon on setting up the full Rails 8 Solid Trifecta (Solid Cache, Solid Queue, Solid Cable).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StimulusJS&lt;/strong&gt; — to shuffle the loading phrases every 2 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ruby_llm&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;ruby_llm-schema&lt;/code&gt;&lt;/strong&gt; gems — to easily interact with any AI model provider (OpenAI, Anthropic, Gemini, etc.) and enforce structured outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it! Let’s dive in.&lt;/p&gt;

&lt;p&gt;### Step 1. Create new Rails app&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new phrase_cycler &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sqlite3 &lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# simple, zero-config DB for a demo&lt;/span&gt;
&lt;span class="nt"&gt;--css&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tailwind &lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# easy styling&lt;/span&gt;
&lt;span class="nt"&gt;--javascript&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;importmap&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# no bundler needed&lt;/span&gt;
&lt;span class="nt"&gt;--stimulus&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="c"&gt;# to sprinkle some javascript in our views&lt;/span&gt;
&lt;span class="nt"&gt;--skip-test&lt;/span&gt; &lt;span class="c"&gt;# keeps the demo lean, we won't get into testing&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;phrase_cycler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome, we have created our Rails "Phrase Cycler" application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2. Set up a resource controller
&lt;/h3&gt;

&lt;p&gt;We'll make a simple (and dummy) ResourcesController to trigger our background jobs when the user submits a message.&lt;/p&gt;

&lt;p&gt;But a "resource" could be anything, for example, instead of ResourcesController you might want a &lt;code&gt;PostsController&lt;/code&gt;, a &lt;code&gt;ChatsController&lt;/code&gt; or whatever your resource is.&lt;/p&gt;

&lt;p&gt;Let's go ahead and run the following generator in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g controller resources index create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;ResourcesController#index&lt;/code&gt; and &lt;code&gt;ResourcesController#create&lt;/code&gt; actions&lt;/li&gt;
&lt;li&gt;Add the matching view files&lt;/li&gt;
&lt;li&gt;Add routes to &lt;code&gt;routes.rb&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But we don’t actually want the default routes it gives us. In &lt;code&gt;routes.rb&lt;/code&gt;, Rails will add these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"resources/index"&lt;/span&gt;
&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"resources/create"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s remove them and replace with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="ss"&gt;:index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:create&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s2"&gt;"resources#index"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creates a &lt;code&gt;POST /resources&lt;/code&gt; route (for form submits)&lt;/li&gt;
&lt;li&gt;Creates a &lt;code&gt;GET /resources&lt;/code&gt; route (for rendering the page)&lt;/li&gt;
&lt;li&gt;Sets the root path &lt;code&gt;/&lt;/code&gt; to load &lt;code&gt;ResourcesController#index&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To double check, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails routes &lt;span class="nt"&gt;-g&lt;/span&gt; resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resources GET  /resources&lt;span class="o"&gt;(&lt;/span&gt;.:format&lt;span class="o"&gt;)&lt;/span&gt; resources#index
          POST /resources&lt;span class="o"&gt;(&lt;/span&gt;.:format&lt;span class="o"&gt;)&lt;/span&gt; resources#create
     root GET  /                    resources#index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. Our routes are set.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Build the user interface
&lt;/h3&gt;

&lt;p&gt;We’ll use &lt;strong&gt;TailwindCSS v4&lt;/strong&gt; and &lt;strong&gt;DaisyUI&lt;/strong&gt; so things look clean and consistent without much effort.&lt;/p&gt;

&lt;p&gt;To add DaisyUI to our Rails app, run this in the root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -sLo app/assets/tailwind/daisyui.js https://github.com/saadeghi/daisyui/releases/latest/download/daisyui.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulls the latest DaisyUI into a single JS file.&lt;/p&gt;

&lt;p&gt;Now open &lt;code&gt;application.css&lt;/code&gt; and add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../public/*.html"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../app/helpers/**/*.rb"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../app/javascript/**/*.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@source&lt;/span&gt; &lt;span class="s1"&gt;"../../../app/views/**/*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./daisyui.js"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;themes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lofi&lt;/span&gt; &lt;span class="n"&gt;--default&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. Tailwind and DaisyUI are ready to go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.1 Create the UI&lt;/strong&gt;&lt;br&gt;
Our UI is very simple: a form with one text box and a button. When the user submits, we’ll show the loading phrases like &lt;em&gt;“Loading…”&lt;/em&gt;, &lt;em&gt;“Thinking…”&lt;/em&gt;, or &lt;em&gt;“Crunching…”&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Later we’ll wire it up with Hotwire, JavaScript, and a few partials. For now, add this to &lt;code&gt;app/views/resources/index.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col items-center justify-center gap-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="na"&gt;#&lt;/span&gt; &lt;span class="na"&gt;Loader&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="na"&gt;#&lt;/span&gt; &lt;span class="na"&gt;Form&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full flex justify-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form_with&lt;/span&gt; &lt;span class="na"&gt;url:&lt;/span&gt; &lt;span class="na"&gt;resources_path&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method:&lt;/span&gt; &lt;span class="na"&gt;:post&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;flex&lt;/span&gt; &lt;span class="na"&gt;flex-col&lt;/span&gt; &lt;span class="na"&gt;gap-4&lt;/span&gt; &lt;span class="na"&gt;min-w-96&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;data:&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="na"&gt;turbo:&lt;/span&gt; &lt;span class="na"&gt;true&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form.text_field&lt;/span&gt; &lt;span class="na"&gt;:message&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;placeholder:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Send&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt; &lt;span class="na"&gt;w-full&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;form.submit&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;Submit&lt;/span&gt;&lt;span class="err"&gt;",&lt;/span&gt; &lt;span class="na"&gt;class:&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;btn&lt;/span&gt; &lt;span class="na"&gt;btn-primary&lt;/span&gt; &lt;span class="na"&gt;w-full&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, if you run the server and open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;, you should see a clean form on the homepage ready to send messages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2F3.1-ui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2F3.1-ui.png" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hard-coded loading phrases
&lt;/h3&gt;

&lt;p&gt;Before we bring AI into the mix, let’s zoom out and ask: &lt;strong&gt;what are we really trying to do here?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user submits a message and while waiting for a slow process to finish, we shuffle through fun loading phrases.&lt;/p&gt;

&lt;p&gt;Do we &lt;em&gt;need&lt;/em&gt; AI for that? Nope. We can just hard code a few phrases and cycle through them with Stimulus. Quick win.&lt;/p&gt;

&lt;p&gt;So let’s start simple: submit the form, show phrases, shuffle them on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.1 Update the controller&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;ResourcesController#create&lt;/code&gt;, drop this in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: Enqueue slow process background job here&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s2"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"resources/loader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;phrases: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Concocting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Prepping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Pouring"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;resources_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Resource created successfully"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what’s happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User submits the form.&lt;/li&gt;
&lt;li&gt;We immediately respond with a Turbo Stream.&lt;/li&gt;
&lt;li&gt;That Turbo Stream replaces the empty &lt;code&gt;resource-status-container&lt;/code&gt; with our new partial &lt;code&gt;_loader.html.erb&lt;/code&gt;, passing along a list of static phrases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4.2 Build the loader partial&lt;/strong&gt;&lt;br&gt;
Create &lt;code&gt;app/views/resources/_loader.html.erb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"loading loading-ring loading-xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-14 min-w-14 w-14 text-sm text-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;phrases.first&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you submit the form, you’ll see a spinner and one phrase (like &lt;em&gt;Concocting...&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Fstatic_loader1.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Fstatic_loader1.gif" width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.3 Cycle phrases with Stimulus&lt;/strong&gt;&lt;br&gt;
Right now it only shows the first phrase. Let’s rotate them every 2 seconds with Stimulus.&lt;/p&gt;

&lt;p&gt;Generate a Stimulus controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus PhraseCycler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you &lt;code&gt;app/javascript/controllers/phrase_cycler_controller.js&lt;/code&gt;. Add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// Connects to data-controller="phrase-cycler"&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; 

    &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phrasesValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;labelTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4.4 Hook up the Stimulus controller&lt;/strong&gt;&lt;br&gt;
Update the loader partial to use the Stimulus controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center gap-2"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"phrase-cycler"&lt;/span&gt; &lt;span class="na"&gt;data-phrase-cycler-phrases-value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= phrases.to_json %&amp;gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"loading loading-ring loading-xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"max-w-14 min-w-14 w-14 text-sm text-start"&lt;/span&gt; &lt;span class="na"&gt;data-phrase-cycler-target=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;phrases.first&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...
    &lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when the loader partial is injected, the Stimulus controller connects and cycles through the phrases automatically every 2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Fhard_coded_cycler.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Fhard_coded_cycler.gif" width="760" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Neat, right? We now have a cycling loader that feels alive. Way better than just &lt;em&gt;Loading...&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Next up we’ll simulate a slow background job to dismiss the loader, then bring AI into the mix for truly dynamic phrases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5. Simulating a long-process background job
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;ResourcesController#create&lt;/code&gt; runs, we also want to enqueue a &lt;strong&gt;slow process&lt;/strong&gt;. Once it’s done, it should swap the loader with either a &lt;strong&gt;success&lt;/strong&gt; or &lt;strong&gt;failure&lt;/strong&gt; message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g job SlowProcess
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here’s the job code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlowProcessJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
    &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;
        &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="n"&gt;stream_to_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;
        &lt;span class="n"&gt;stream_to_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"failure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_to_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_update_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"resource_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"resources/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What’s happening:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We fake a slow process with &lt;code&gt;sleep 20&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When it’s done, we stream either &lt;code&gt;success&lt;/code&gt; or &lt;code&gt;failure&lt;/code&gt; to the &lt;code&gt;resource_channel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;That update replaces the loader in &lt;code&gt;resource-status-container&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;5.1 Create the success and failure partials&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="na"&gt;#&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;_success.html.erb&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-green-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Success!&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="na"&gt;#&lt;/span&gt; &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="na"&gt;_failure.html.erb&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-red-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Failure!&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5.2 Subscribe the view to a Turbo Stream&lt;/strong&gt;&lt;br&gt;
At the top of &lt;code&gt;app/views/resources/index.html.erb&lt;/code&gt;, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;resource_channel&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5.3 Update the controller action&lt;/strong&gt;&lt;br&gt;
Don't forget to enqueue the &lt;code&gt;SlowProcessJob&lt;/code&gt; in the &lt;code&gt;ResourcesController#create&lt;/code&gt; here's the updated controller action&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="no"&gt;SlowProcessJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"resources/loader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;phrases: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"Concocting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Prepping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Pouring"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;resources_path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now when you submit the form:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The loader kicks in with cycling phrases.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SlowProcessJob&lt;/code&gt; is enqueued and runs in the background.&lt;/li&gt;
&lt;li&gt;After ~20 seconds, the job finishes.&lt;/li&gt;
&lt;li&gt;The loader is replaced with &lt;strong&gt;Success!&lt;/strong&gt; (or &lt;strong&gt;Failure!&lt;/strong&gt; if something goes wrong).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Awesome. At this point, you’ve got a fully working flow: The user submits the form. An animated loader is displayed. The slow jobs gets enqueued and once finished it streams the success partial.&lt;/p&gt;

&lt;p&gt;We can call it a day. But we didn’t come all this way just for hard-coded phrases. Let's go ahead and implement those AI generated loading phrases...&lt;/p&gt;

&lt;h3&gt;
  
  
  6. AI-Generated loading phrases with RubyLLM
&lt;/h3&gt;

&lt;p&gt;We’ll create a new background job that asks an LLM (like OpenAI) for random loading phrases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.1 Create the job&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g job GenerateLoadingPhrases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us &lt;code&gt;app/jobs/generate_loading_phrases_job.rb&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.2 Configure RubyLLM&lt;/strong&gt;&lt;br&gt;
First, install the gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle add ruby_llm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add an initializer &lt;code&gt;config/initializers/ruby_llm.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"ruby_llm"&lt;/span&gt;

&lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# Add keys ONLY for the providers you intend to use.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openai_api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"gpt-4o-mini"&lt;/span&gt;
    &lt;span class="c1"&gt;# config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to add your API key to your &lt;code&gt;.env&lt;/code&gt;, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;123abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RubyLLM gives us a clean interface to interact with OpenAI, Anthropic, Gemini, DeepSeek, and more. It's a pretty easy and powerful gem to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.3 Add structured output with ruby_llm-schema&lt;/strong&gt;&lt;br&gt;
We need the AI to return a list of phrases (an array of strings). By default, RubyLLM doesn’t support structured responses. That’s where &lt;code&gt;ruby_llm-schema&lt;/code&gt; comes in.&lt;/p&gt;

&lt;p&gt;Let's install it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add ruby_llm-schema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, create &lt;code&gt;app/schemas/phrase_schema.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PhraseSchema&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Schema&lt;/span&gt;
    &lt;span class="n"&gt;array&lt;/span&gt; &lt;span class="ss"&gt;:phrases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;of: :string&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6.4 Implement the job&lt;/strong&gt;&lt;br&gt;
Inside &lt;code&gt;app/jobs/generate_loading_phrases_job.rb&lt;/code&gt; add this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;phrases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fetch_phrases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stream_to_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;StandardError&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;
    &lt;span class="c1"&gt;# If the job fails, do nothing&lt;/span&gt;
    &lt;span class="kp"&gt;nil&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kp"&gt;private&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_phrases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_instructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PhraseSchema&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"phrases"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_to_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_update_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"resource_channel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"resources/loader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;phrases: &lt;/span&gt;&lt;span class="n"&gt;phrases&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prompt&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;&lt;span class="sh"&gt;
    Analyze this message and come up with a list of positive, cheerful and delightful verbs in gerund form that's related to the message. Only include words with no other text or punctuation. These words should have the first letter capitalized. Add some quaint and surprise to entertain the user. Ensure each word is highly relevant to the user's message. Obscure words are preferred but be careful to avoid words that might look alarming or concerning to the software engineer seeing it as a status notification, such as Connecting, Disconnecting, Retrying, Lagging, Freezing, etc. NEVER use a destructive word, such as Terminating, Killing, Deleting, Destroying, Stopping, Exiting, or similar. NEVER use a word that may be derogatory, offensive, or inappropriate in a non-coding context, such as Penetrating.
&lt;/span&gt;&lt;span class="no"&gt;    PROMPT&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6.5 Update the controller&lt;/strong&gt;&lt;br&gt;
And finally, enqueue the new job inside &lt;code&gt;ResourcesController#create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="no"&gt;GenerateLoadingPhrasesJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;priority: &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:message&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="no"&gt;SlowProcessJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;

    &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
            &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;turbo_stream: &lt;/span&gt;&lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"resource-status-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"resources/loader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;phrases: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"Concocting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Prepping"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Pouring"&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;resources_path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me explain what's going on here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6.6 How it works?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GenerateLoadingPhrasesJob&lt;/code&gt; runs with a &lt;strong&gt;higher priority&lt;/strong&gt; so it finishes before the slow job.&lt;/li&gt;
&lt;li&gt;It sends a structured request to the LLM (with our schema + prompt).&lt;/li&gt;
&lt;li&gt;The LLM responds with a list of phrases.&lt;/li&gt;
&lt;li&gt;The job streams those new phrases to the &lt;code&gt;resource_channel&lt;/code&gt;, updating the loader.&lt;/li&gt;
&lt;li&gt;The Stimulus controller takes over and cycles through them.&lt;/li&gt;
&lt;li&gt;Once the slow job finishes, it streams &lt;strong&gt;Success&lt;/strong&gt; (or &lt;strong&gt;Failure&lt;/strong&gt;) and replaces the loader.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, you’ve got &lt;strong&gt;AI-powered dynamic loading phrases&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;The user submits a message, sees playful phrases that actually relate to their input, and then a final success message.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Ffinal_result.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Funtaught.sfo3.cdn.digitaloceanspaces.com%2Fghost%2Fposts%2Fai-powered-loading-messages%2Ffinal_result.gif" width="720" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Exploring", "Wandering", "Dancing", "Glimmering", "Sparkling", "Fluttering", "Radiating", "Savoring", "Whimsicalizing", "Delighting"&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;That’s a wrap! In this tutorial, we have learned how to create dynamic loading messages using the AI.&lt;/p&gt;

&lt;p&gt;This is a simple but powerful technique to make your apps feel alive and fun. Try tweaking the prompt to see what unique and corny phrases the AI comes up with. That's where the magic happens!&lt;/p&gt;

&lt;p&gt;If you enjoyed this tutorial, here's where to find more of my work:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://untaught.dev" rel="noopener noreferrer"&gt;Untaught Blog&lt;/a&gt;&lt;br&gt;
&lt;a href="https://untaught.dev/ai-generated-loading-messages-in-rails/" rel="noopener noreferrer"&gt;Read Article Here&lt;/a&gt;&lt;br&gt;
&lt;a href="https://x.com/untaughtdev" rel="noopener noreferrer"&gt;Follow me on X&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ruby</category>
      <category>rails</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Run My SaaS for $45/month Without Supabase, Vercel, or Firebase. Just Rails.</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Tue, 03 Jun 2025 18:50:04 +0000</pubDate>
      <link>https://dev.to/untaught/how-i-run-my-saas-for-45month-without-supabase-vercel-or-firebase-just-rails-22pc</link>
      <guid>https://dev.to/untaught/how-i-run-my-saas-for-45month-without-supabase-vercel-or-firebase-just-rails-22pc</guid>
      <description>&lt;p&gt;Everyone’s talking about Supabase, Vercel, Firebase, Replit, and similar services as the go-to stack to launch SaaS apps fast.&lt;/p&gt;

&lt;p&gt;I tried them.&lt;/p&gt;

&lt;p&gt;They’re sleek and easy to use. But once I started estimating real-world costs for my project, I realized they add up fast, and that’s a problem when you're launching without real users yet.&lt;/p&gt;

&lt;p&gt;So I built my SaaS, Odichat, with a different approach — one that costs me $45/month and gives me full control, solid performance, and zero vendor lock-in.&lt;/p&gt;

&lt;p&gt;Let me break it down.&lt;/p&gt;

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

&lt;p&gt;Here’s what I’m running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A production-ready Rails 8 app&lt;/li&gt;
&lt;li&gt;A staging environment for safe deployments&lt;/li&gt;
&lt;li&gt;File storage for user uploads&lt;/li&gt;
&lt;li&gt;Transactional emails&lt;/li&gt;
&lt;li&gt;Background job processing&lt;/li&gt;
&lt;li&gt;Websockets&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And all of this for $45/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Costs
&lt;/h2&gt;

&lt;p&gt;Here’s the exact monthly breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hetzner dedicated vCPU (production): $13.49&lt;/li&gt;
&lt;li&gt;Hetzner shared vCPU (remote builder): $4.99
(used for asset precompilation and deploys)&lt;/li&gt;
&lt;li&gt;Hetzner shared vCPU (staging): $4.99&lt;/li&gt;
&lt;li&gt;DigitalOcean Spaces (file storage): $5.33&lt;/li&gt;
&lt;li&gt;Zoho Mail (support email inbox): $1&lt;/li&gt;
&lt;li&gt;Postmark (transactional emails): $15&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: $45.80 USD/month&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite3 is my database
&lt;/h2&gt;

&lt;p&gt;I’m using SQLite3 as the database.&lt;/p&gt;

&lt;p&gt;Yep, SQLite in production.&lt;/p&gt;

&lt;p&gt;It’s free, and for my current load it works perfectly. I haven’t had a single issue that justifies migrating to PostgreSQL (yet).&lt;/p&gt;

&lt;h2&gt;
  
  
  No Redis. I'm fine.
&lt;/h2&gt;

&lt;p&gt;Rails 8 ships with the “Solid” suite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solid Queue (background jobs)&lt;/li&gt;
&lt;li&gt;Solid Cache&lt;/li&gt;
&lt;li&gt;Solid Cable (Websockets)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s a full-featured solution without extra setup or Redis requirements. And it performs great.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not serverless?
&lt;/h2&gt;

&lt;p&gt;Because I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable, low costs&lt;/li&gt;
&lt;li&gt;Zero surprises from usage-based pricing&lt;/li&gt;
&lt;li&gt;Infra I understand and can control&lt;/li&gt;
&lt;li&gt;The ability to grow into higher traffic without switching stacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m not anti-serverless. But at this stage, this is the simplest and most sustainable setup I’ve found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts!
&lt;/h2&gt;

&lt;p&gt;It’s not “trendy”.&lt;/p&gt;

&lt;p&gt;It’s not “modern”.&lt;/p&gt;

&lt;p&gt;But it works AMAZINGLY well, it’s cheap, and it lets me focus on building, not budgeting.&lt;/p&gt;

&lt;p&gt;If you’re building a SaaS and want full control without overpaying early on, I highly recommend exploring this kind of setup — especially if you’re using Rails.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>rails</category>
      <category>ai</category>
    </item>
    <item>
      <title>Classify &amp; Label Customer Service Chats with Python, OpenAI and Langchain</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Mon, 24 Feb 2025 18:14:21 +0000</pubDate>
      <link>https://dev.to/untaught/classify-label-customer-service-chats-with-python-openai-and-langchain-2a53</link>
      <guid>https://dev.to/untaught/classify-label-customer-service-chats-with-python-openai-and-langchain-2a53</guid>
      <description>&lt;p&gt;LLMs with Structured Outputs make classifying and labeling text incredibly easy. Today, we’ll write a script to do exactly that—classifying a customer service chat.&lt;/p&gt;

&lt;p&gt;We’ll extract and label key details from the chat, including the user’s name, sentiment, language, and the topic of their conversation with a customer service representative.&lt;/p&gt;

&lt;p&gt;Before jumping into the code, let’s review the tools and requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Python installed locally&lt;/li&gt;
&lt;li&gt;OpenAI API Key&lt;/li&gt;
&lt;li&gt;A text file containing the conversation between the representative and the client&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Create The Project Directory
&lt;/h2&gt;

&lt;p&gt;Now, let’s go ahead and create our project directory and &lt;code&gt;cd&lt;/code&gt; into it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;text_classification &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;text_classification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s make sure we’ve got our tools and project prepped. So let’s create the entry point file of the project within the root directory&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a virtual environment to keep our dependencies contained so they don’t mess with system-wide Python packages, and then activate it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installing Dependencies
&lt;/h2&gt;

&lt;p&gt;With our virtual environment in place, let’s go ahead and install our dependencies. I’m using pipso I’ll run the following command in my project terminal&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pydantic&lt;/code&gt;, a widely used Python validation library. It will let us declare the schema of the structured output response we expect from the LLM.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;langchain&lt;/code&gt; , is a framework and package that makes it easier to work with LLMs in Python.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;langchain-openai&lt;/code&gt;, a Langchain package that provides seamless integration with OpenAI’s models.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m using &lt;code&gt;pip&lt;/code&gt; to install these dependencies so I’ll run the following command in my project terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pydantic langchain langchain-openai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ENVs &amp;amp; Chat Text File
&lt;/h2&gt;

&lt;p&gt;Great! Now that we've installed our dependencies, let's create a .env file to store our OpenAI API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY=sk….
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And before finally getting our hands dirty, let’s not forget about the chat data!&lt;/p&gt;

&lt;p&gt;I exported a conversation between a customer service representative and a customer from WhatsApp into a text file.&lt;/p&gt;

&lt;p&gt;You can use &lt;a href="https://conversalo.nyc3.cdn.digitaloceanspaces.com/chat.txt" rel="noopener noreferrer"&gt;my sample chat file&lt;/a&gt; or bring your own.&lt;/p&gt;

&lt;p&gt;I’ll drop the text file in the project’s root directory, so we can later pass the contents of that file to the LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Now that everything’s set up, it’s time to crack open &lt;code&gt;main.py&lt;/code&gt; and get coding.&lt;/p&gt;

&lt;p&gt;First things first, let’s make sure the OpenAI API key is actually set—otherwise we won’t get LLM responses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY is not set&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, let’s run in our terminal the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no errors pop up, you're good to go.&lt;/p&gt;

&lt;p&gt;But in case you get an error, try closing and re-opening your terminal. A new terminal will load your variables in the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Next, let’s import the other modules we’re going to need for this script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chat_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;init_chat_model&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Classification Schema
&lt;/h2&gt;

&lt;p&gt;Now let’s see the meat and potatoes of this classification and labeling script.&lt;/p&gt;

&lt;p&gt;We’ll use Pydantic to define a Classification class that serves as the schema we’ll pass later on to the LLM so it knows what information to extract and label from the chat.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Classification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;“&lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="err"&gt;”&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;sentiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The sentiment of the user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;neutral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The language of the user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;spanish&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The issue of the user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;enum&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;technical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the schema’s &lt;code&gt;name&lt;/code&gt; field only has a &lt;code&gt;description&lt;/code&gt; attribute that specifies to get the client’s name. However, the other fields also include an &lt;code&gt;enum&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reason for adding enum to some fields is to make sure the model classifies only within predefined categories, reducing ambiguity and making the data easier to store and analyze.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured Outputs
&lt;/h2&gt;

&lt;p&gt;Now, let’s create a chat model. We’ll use the &lt;code&gt;with_structured_output&lt;/code&gt; to pass our &lt;code&gt;Classification&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;Under the hood, Langchain’s with_structured_output method makes sure the LLM has Structured Output enabled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_chat_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with_structured_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Classification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we’ll save the chat contents from &lt;code&gt;chat.txt&lt;/code&gt; and create a full prompt for the LLM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;chat_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract the desired information from the following chat.
Only extract the properties mentioned in the &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Classification&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; function.
Conversation:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, let’s invoke the LLM with our prompt and chat text contents and print the results&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;chat_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our terminal, let’s run the script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; python main.py
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Andres Urdaneta'&lt;/span&gt; &lt;span class="nv"&gt;sentiment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'neutral'&lt;/span&gt; &lt;span class="nv"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'spanish'&lt;/span&gt; &lt;span class="nv"&gt;issue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'other'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap-up &amp;amp; Next Steps
&lt;/h2&gt;

&lt;p&gt;And there you have it—a fully functional script that classifies and labels customer service chats using Python, OpenAI API, and Langchain!&lt;/p&gt;

&lt;p&gt;With just a few lines of code, we structured an unorganized conversation into clear, actionable data.&lt;/p&gt;

&lt;p&gt;This setup can serve as the foundation for automating customer insights, building smarter chatbots, or even integrating AI-driven analytics into your workflow.&lt;/p&gt;

&lt;p&gt;Try tweaking the classification schema, adding more categories, or even chaining multiple prompts together, and have fun!&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;If you have any questions or would like to connect, hit me up on &lt;a href="https://x.com/__andresu__" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/andres-urd/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aurdaneta.com/blog/classify-and-label-customer-service-chats-with-python-openai-and-langchain" rel="noopener noreferrer"&gt;Read this article on my website&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>python</category>
      <category>langchain</category>
    </item>
    <item>
      <title>How to change App ID/Package Name for Android app built with React Native</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Fri, 10 Jun 2022 00:25:24 +0000</pubDate>
      <link>https://dev.to/untaught/how-to-change-app-idpackage-name-for-android-app-built-with-react-native-1bpk</link>
      <guid>https://dev.to/untaught/how-to-change-app-idpackage-name-for-android-app-built-with-react-native-1bpk</guid>
      <description>&lt;p&gt;When uploading a bundled version of your Android app to Google Play Console, you might need to change your Package Name / Application ID to satisfy Google Play Console rules.&lt;/p&gt;

&lt;p&gt;If you are getting an error like the following (or something similar)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You need to use a different package name because "com.***.***.***" already exists in Google Play
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try out the following steps.&lt;/p&gt;

&lt;h1&gt;
  
  
  Change Package Name / Application ID in Android app built with React Native.
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;In VSCode go to the Search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pwndjkox2goqtckk5h2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pwndjkox2goqtckk5h2.png" alt="Search" width="276" height="1032"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enter your current Package Name / Application ID in the search text input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NOTE: If you don't know what's your current Package Name / Application ID, you can find it within the &lt;code&gt;android/app/build.gradle&lt;/code&gt; file, in the &lt;code&gt;applicationId&lt;/code&gt; field. In my case, the current Package Name / Application ID is &lt;code&gt;com.papitas&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enter the path to your &lt;code&gt;android&lt;/code&gt; directory in the &lt;code&gt;files to include&lt;/code&gt; text input.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;In my case my &lt;code&gt;android&lt;/code&gt; directory lives under &lt;code&gt;projectrootdir/apps/mobile/android&lt;/code&gt; yours could be different&lt;/em&gt;,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh8wz5m666hxqwvc7rib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqh8wz5m666hxqwvc7rib.png" alt="Files to include" width="284" height="1057"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This will display all the instances of the string &lt;code&gt;com.papitas&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the "Replace" text input to replace your current Package Name / Application ID for the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nnjdg0q5renbn45gxka.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8nnjdg0q5renbn45gxka.png" alt="Replace old package name for new one" width="520" height="1034"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it!&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>android</category>
      <category>webdev</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Stop using relative paths in your React Native imports. Use Aliases instead.</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Sun, 06 Jun 2021 22:16:43 +0000</pubDate>
      <link>https://dev.to/untaught/stop-using-relative-paths-in-your-react-native-imports-use-aliases-instead-47p5</link>
      <guid>https://dev.to/untaught/stop-using-relative-paths-in-your-react-native-imports-use-aliases-instead-47p5</guid>
      <description>&lt;h2&gt;
  
  
  Use Aliases in your imports with Babel
&lt;/h2&gt;

&lt;p&gt;Instead of doing this in your imports i.e:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;import Component from '../../../components/shared/Header';&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You could do something like this from anywhere in your project i.e:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;import Component from 'components/shared/Header';&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or you could even go as deep as you want i.e:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;import Component from '@/shared/Header';&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You get the point... no more relative paths (&lt;code&gt;'../../../../../../'&lt;/code&gt;) to import any of your components.&lt;/p&gt;

&lt;h2&gt;
  
  
  How?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install required dependencies
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i babel-plugin-module-resolver metro-react-native-babel-preset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Head over to your &lt;code&gt;babel.config.js&lt;/code&gt; file in your project root directory. (If it doesn't exist, create it)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add the &lt;code&gt;module-resolver&lt;/code&gt; plugin to your plugins array like this:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
    presets: ['module:metro-react-native-babel-preset']
    plugins: [
        [
            'module-resolver',
            {
                root: ['.'],
                extensions: ['.ios.js', '.android.js', '.js', '.ts', '.tsx', '.json'],
                alias: {
                    '@': './src/components',
                    'constants': './src/constants',
                    '##': './src/examples',
                },
            },
        ]
    ]
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Make sure to provide the path you want to reference with an alias, and the alias name itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;According to the example above now you're able to import files or modules like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import MyComponent from '@/MyComponent.js'
import MyConstantFile from 'constants/myConstant.js'
import MyExample from '##/MyExample.js'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;I would love to engage with other developers like you. Get in touch on Twitter!&lt;/em&gt; &lt;a href="https://twitter.com/dev_astador" rel="noopener noreferrer"&gt;@dev_astador&lt;/a&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>react</category>
      <category>javascript</category>
      <category>babel</category>
    </item>
    <item>
      <title>VSCode Shortcuts✂️ - Toggle Terminal: Redefined</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Tue, 25 May 2021 18:34:42 +0000</pubDate>
      <link>https://dev.to/untaught/vscode-shortcut-toggle-terminal-redefined-58l</link>
      <guid>https://dev.to/untaught/vscode-shortcut-toggle-terminal-redefined-58l</guid>
      <description>&lt;p&gt;I always hated the shortcut that VSCode gives you to toggle your integrated terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ctrl + `
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feels uncomfortable on the left hand. I have found myself moving my hands away many times from the home row keys. And even worse: Looking down at the keyboard to make sure I don't miss any of these keys (ctrl + `).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
Home row keys&lt;br&gt;
Left hand: A, S, D&lt;br&gt;
Right hand: J, K, L&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The integrated terminal happen to be one of the most useful tools that and Text Editor for programming can offer. It's an essential need.&lt;/p&gt;

&lt;p&gt;So, instead of using such uncomfortable shortcut that clutters your fingers to toggle your terminal you'd better consider using from now on:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;br&gt;
cmd + j&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It gives me such a pleasure to toggle my integrated terminal with this shortcut. It just feels natural. Like a breeze.&lt;br&gt;
The way it should be considering that it's probably an &lt;code&gt;day-to-day-every-five-minute&lt;/code&gt; shortcut that you'd use.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Note that this shortcut &lt;code&gt;(cmd + j)&lt;/code&gt; isn't covered by the VSCode documentation &lt;code&gt;(at least I couldn't find it)&lt;/code&gt;, neither appears in the &lt;code&gt;View&lt;/code&gt; dropdown menu in your VSCode.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well... Hope that this little shortcut helps you in your day to day development life.&lt;/p&gt;

&lt;p&gt;Do you have any cool shortcuts you want to share with me? Drop out your shortcuts on &lt;em&gt;Twitter!&lt;/em&gt; &lt;a href="https://twitter.com/dev_astador" rel="noopener noreferrer"&gt;@dev_astador&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>programming</category>
    </item>
    <item>
      <title>Como hacerle debug a Redux en React Native</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Mon, 15 Feb 2021 23:31:44 +0000</pubDate>
      <link>https://dev.to/untaught/como-hacerle-debug-a-redux-en-react-native-1129</link>
      <guid>https://dev.to/untaught/como-hacerle-debug-a-redux-en-react-native-1129</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Nota: Puedes leer este articulo en ingles, haciendo &lt;a href="https://dev.to/piscespieces/how-to-debug-redux-in-a-react-native-app-4b19"&gt;click aqui&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Despues de mucho &lt;code&gt;s u f r i r&lt;/code&gt; tratando de entender como estas &lt;em&gt;dev tools&lt;/em&gt; trabajan en conjunto para hacerle &lt;em&gt;debug&lt;/em&gt; a la primera aplicacion en React Native que me han asignado, llegue a estar tan estresado por no ser capaz de saber que se encontraba dentro de mi &lt;em&gt;Redux Store State&lt;/em&gt; que decidi documentar la solucion que me funciono.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Despues de todo no fue tan complicado, solamente que la documentacion no era lo suficientemente clara para mi...&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No voy a explicar los fundamentos de como instalar y configurar un &lt;em&gt;Redux Store&lt;/em&gt; con sus &lt;em&gt;middlewares&lt;/em&gt;, pero puede seguir este articulo si necesitas aprender como hacerlo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dicho esto...&lt;/p&gt;

&lt;p&gt;En este blog voy a ensenarte &lt;strong&gt;como hacerle debug a una aplicacion en React Native&lt;/strong&gt; utilizando la extension de Chrome: &lt;strong&gt;Redux DevTools Extension.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Instalando las dependencias necesarias
&lt;/h2&gt;

&lt;p&gt;Vamos a instalar las siguientes dependencias:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;React-native-debugger&lt;/strong&gt;:
Esta es una standalone app que vamos a instalar utilizando &lt;strong&gt;brew&lt;/strong&gt;. Abre tu terminal y corre el siguiente comando:
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;brew install --cask react-native-debugger&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Redux DevTools Chrome Extension &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; redux-devtools-extension&lt;/strong&gt;
Esta es una herramienta que vas a necesitar instalar como una extension de Chrome en tu navegador Google Chrome y tambien como una dependencia en tu proyecto.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Primero, ve hacia el &lt;strong&gt;Chrome Web Store&lt;/strong&gt; e instala &lt;a href="https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;.&lt;br&gt;
  Ahora, finalmente dirigete hacia el &lt;em&gt;root&lt;/em&gt; de tu proyecto para instalar las dependencias siguientes:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install redux-devtools-extension&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;



&lt;p&gt;&lt;code&gt;npm install remote-redux-devtools&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Una vez finalizada la instalacion de estos paquetes, veamos el archivo donde hemos configurado previamente nuestro &lt;em&gt;Redux Store&lt;/em&gt;, comunmente llamado &lt;code&gt;configureStore.js&lt;/code&gt; o &lt;code&gt;store.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurando nuestro &lt;strong&gt;R e d u x S t o r e&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Si no estas utilizando &lt;em&gt;middleware and enhancers&lt;/em&gt; entonces tu &lt;em&gt;Redux Store&lt;/em&gt; deberia verse mas o menos como esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// configureStore.js

import { createStore, applyMiddleware, compose } from 'redux’;

const store = createStore(reducer, compose(
  applyMiddleware(...middleware),
  // other store enhancers if any
));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si, por el contrario, estas utilizando un &lt;em&gt;middleware and enhancers&lt;/em&gt; como &lt;a href="https://github.com/reduxjs/redux-thunk" rel="noopener noreferrer"&gt;&lt;code&gt;redux-thunk&lt;/code&gt;&lt;/a&gt;, o alguno similar: Utilicemos el modulo &lt;code&gt;composeWithDevTools&lt;/code&gt; que proviene del paquete &lt;a href="https://www.npmjs.com/package/redux-devtools-extension" rel="noopener noreferrer"&gt;&lt;code&gt;redux-devtools-extension&lt;/code&gt;&lt;/a&gt;, y nuestro &lt;em&gt;store&lt;/em&gt; se veria algo asi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// configureStore.js

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'remote-redux-devtools';

const store = createStore(
    reducer,
    composeWithDevTools(applyMiddleware(...middleware),
  // other store enhancers if any
));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el primer ejemplo no fue necesario utilizar el modulo &lt;code&gt;composeWithDevTools&lt;/code&gt;. Esa es la unica diferencia entre los dos &lt;em&gt;snippets&lt;/em&gt; de codigo. Asegurate solamente de que estas utilizando la manera correcta para tu proyecto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Corriendo la herramienta &lt;a href="https://github.com/jhen0409/react-native-debugger" rel="noopener noreferrer"&gt;&lt;code&gt;React Native Debugger&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Hay dos maneras de acceder al &lt;strong&gt;React Native Debugger&lt;/strong&gt;, y las dos son bastante parecidas, es solo cuestion de cual prefieres:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;React Native Debugger&lt;/strong&gt; corre por defecto en el puerto local &lt;code&gt;8081&lt;/code&gt;, asi que puedes simplemente abrir tu terminal y correr el siguiente comando:
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;open “rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081”&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;o&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dirigete hacia el directorio &lt;em&gt;root&lt;/em&gt; de tu proyecto y abre el &lt;code&gt;package.json&lt;/code&gt;. En la seccion de &lt;code&gt;"scripts"&lt;/code&gt; agrega lo siguiente:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ”debug": "open 'rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081’”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora, el &lt;code&gt;package.json&lt;/code&gt; deberia lucir mas o menos asi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  "scripts": {
      "android": "react-native run-android",
      "ios": "react-native run-ios",
      "start": "react-native start",
      "test": "jest",
      "lint": "eslint .",
      "debug": "open 'rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081'"
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luego, hecho esto, en tu directorio &lt;em&gt;root&lt;/em&gt; corre el siguiente comando:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm run debug&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Esto deberia abrir la ventana de &lt;strong&gt;React Native Debugger&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhl3noi25z9xlwawz6bhh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhl3noi25z9xlwawz6bhh.png" alt="React Native Debugger window" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;## Acceso al Developer Menu&lt;/p&gt;

&lt;p&gt;Una vez lanzado el &lt;strong&gt;React Native Debugger&lt;/strong&gt;, dirigete hacia tu el editor de texto donde tienes tu proyecto y lanza tu aplicacion en el simulador de dispositivo, en mi caso:&lt;/p&gt;

&lt;p&gt;Para iOS&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run ios
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para Android&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una vez abierto, utilizando el &lt;em&gt;shortcut&lt;/em&gt; &lt;code&gt;cmd+d&lt;/code&gt; en el simulador de iOS, &lt;code&gt;cmd+m&lt;/code&gt; si estas utilizando el simulador de Android. El &lt;em&gt;Developer Menu&lt;/em&gt; deberia abrir. Selecciona la opcion &lt;strong&gt;Debug&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Y &lt;em&gt;Voilà&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;Esto deberia conectar tu &lt;strong&gt;RNDebugger&lt;/strong&gt; con tu aplicacion. Ahora tienes la posibilidad de hacerle a tu aplicacion utilizando el &lt;em&gt;Chrome DevTools&lt;/em&gt; pero tambien la razon por la que viniste aqui, para chequear y saber que hay en tu &lt;em&gt;Redux Store&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PD: Let's connect on Twitter! &lt;a href="https://twitter.com/dev_astador" rel="noopener noreferrer"&gt;@dev_astador&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>spanish</category>
      <category>react</category>
      <category>redux</category>
      <category>reactnative</category>
    </item>
    <item>
      <title>How to Debug Redux in a React-Native App</title>
      <dc:creator>Andres Urdaneta</dc:creator>
      <pubDate>Mon, 15 Feb 2021 00:07:39 +0000</pubDate>
      <link>https://dev.to/untaught/how-to-debug-redux-in-a-react-native-app-4b19</link>
      <guid>https://dev.to/untaught/how-to-debug-redux-in-a-react-native-app-4b19</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Note: You can read this article &lt;a href="https://dev.to/piscespieces/how-to-debug-redux-in-a-react-native-app-4b19"&gt;in spanish&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After a lot of —&lt;code&gt;s t r u g g l e&lt;/code&gt;— trying to understand how all these dev tools work together in order to debug my first React Native Application I got so stressed for not being able to just get to know what was in my &lt;strong&gt;Redux Tree&lt;/strong&gt; that I decided to document the solution that worked for me.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— After all it wasn't that complicated, it's just that the docs weren't clear enough for me... and you know when you just go nuts with something simple? Well... —&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That being said…&lt;/p&gt;

&lt;p&gt;In this blog I'm going to show you &lt;strong&gt;how to debug a React Native application&lt;/strong&gt; using the Chrome Extension: &lt;strong&gt;Redux DevTools Extension.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Assuming that you already have your React Native project set up with Redux and a middleware (i.e thunk). &lt;em&gt;(I mean, installed as dev dependencies and your Redux store already setup)&lt;/em&gt;&lt;code&gt;;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing needed dependencies
&lt;/h2&gt;

&lt;p&gt;We are going to install the following dependencies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;React-native-debugger&lt;/strong&gt;:&lt;br&gt;
This is a standalone app that you are going to need to install with &lt;strong&gt;brew&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;brew install --cask react-native-debugger&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Redux DevTools Chrome Extension &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; redux-devtools-extension&lt;/strong&gt;&lt;br&gt;
This is a tool that you are going to need to install as a Chrome Extension in your Chrome Web Browser and also as a dev dependencie in your project.&lt;/p&gt;

&lt;p&gt;First, head over to the &lt;strong&gt;Chrome Web Store&lt;/strong&gt; and install the &lt;a href="https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=en" rel="noopener noreferrer"&gt;Redux DevTools&lt;/a&gt;.&lt;br&gt;
Then, now and finally we’ll head to the root of our project and install the dev dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install redux-devtools-extension remote-redux-devtools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once finished installing the dependencies mentioned above we’ll take a look at the JavaScript file where we configure our redux store. Commonly named as &lt;code&gt;configureStore.js&lt;/code&gt; or &lt;code&gt;store.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring our &lt;strong&gt;R e d u x S t o r e&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you setup your store with middeware and enhancers like &lt;a href="https://github.com/reduxjs/redux-thunk" rel="noopener noreferrer"&gt;&lt;code&gt;redux-thunk&lt;/code&gt;&lt;/a&gt;, or similar: Let's use the &lt;code&gt;composeWithDevTools&lt;/code&gt; export from &lt;a href="https://www.npmjs.com/package/redux-devtools-extension" rel="noopener noreferrer"&gt;&lt;code&gt;redux-devtools-extension&lt;/code&gt;&lt;/a&gt;, and our store would look more or less like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// configureStore.js

import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'remote-redux-devtools';

const store = createStore(
    reducer,
    composeWithDevTools(applyMiddleware(...middleware),
  // other store enhancers if any
));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are not using middleware and/or enhancers then your store should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// configureStore.js

import { createStore, applyMiddleware, compose } from 'redux’;

const store = createStore(reducer, compose(
  applyMiddleware(...middleware),
  // other store enhancers if any
));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that in the second example we don’t need to use the &lt;code&gt;composeWithDevTools&lt;/code&gt; export. That’s the only difference. The reason behind this is that the &lt;code&gt;composeWithDevTools&lt;/code&gt; function makes the actions dispatched from Redux DevTools flow to the middleware (&lt;em&gt;something to take a deep look if you want to go down that rabbit hole&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Launching the &lt;a href="https://github.com/jhen0409/react-native-debugger" rel="noopener noreferrer"&gt;&lt;code&gt;React Native Debugger&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;There are two ways of accessing the &lt;strong&gt;React Native Debugger&lt;/strong&gt; and they both are kinda the same, it’s just matter of preference:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The &lt;strong&gt;React Native Debugger&lt;/strong&gt; runs by default in the port &lt;code&gt;8081&lt;/code&gt;, you can just open your terminal and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;open “rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081”&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;or&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Head over to your app root directory and open the &lt;code&gt;package.json&lt;/code&gt;. In the&lt;code&gt;“scripts”&lt;/code&gt; section add the following:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;”debug": "open 'rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081’”
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By now your &lt;code&gt;package.json&lt;/code&gt; should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint .",
    "debug": "open 'rndebugger://set-debugger-loc?host=localhost&amp;amp;port=8081'"
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then in your app root directory run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run debug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should open the &lt;strong&gt;React Native Debugger&lt;/strong&gt; window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhl3noi25z9xlwawz6bhh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fhl3noi25z9xlwawz6bhh.png" alt="React Native Debugger window" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Accesing the Developer Menu
&lt;/h2&gt;

&lt;p&gt;Once launched the &lt;strong&gt;React Native Debugger&lt;/strong&gt; head over to your project in your text editor and launch your app in the device simulator (in my case &lt;code&gt;npm run ios&lt;/code&gt; or &lt;code&gt;npm run android&lt;/code&gt;). Once the app opened, by using the shortcut &lt;code&gt;cmd+d&lt;/code&gt; in the iOS simulator or &lt;code&gt;cmd+m&lt;/code&gt; when running in an Android emulator, the Developer Menu should open. Click the &lt;strong&gt;“Debug”&lt;/strong&gt; option.&lt;/p&gt;

&lt;p&gt;And &lt;em&gt;Voilà&lt;/em&gt;!&lt;/p&gt;

&lt;p&gt;This should get your &lt;strong&gt;RNDebugger&lt;/strong&gt; connected with your application. Now you are able to debug your app overall by taking advantage of the Chrome DevTools but also the reasons you came here... To check and debug your &lt;em&gt;freaking&lt;/em&gt; &lt;strong&gt;Redux Tree&lt;/strong&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
