Using ChatGPT & Custom GPTs for code brainstorming
As part of my attempt to get better at prompt engineering I'm really trying to do as much as possible of my work with some AI support (coding, design, reviews, copy, etc. use it for everything first).
Custom GPTs is the feature from ChatGPT that makes me use it more than anything else (more than Claude, Perplexity, or Gemini). I have a handful Custom GPTs that I use every day, and I create a bunch for specific needs as well.
One of my favorite use cases for one of my recurring Custom GPTs is to brainstorm different solutions before I start coding (this one is using a Rails-specific GPT one). I pretty much will start planning my work there. It will often give me good suggestions or at least provide me with some good naming for things. Then at the end a finish by asking for a review as well.
In this example, I know what different patterns I might want to use here. I could do it myself no problem, but I find it great to be able to see a draft version of all the different ideas in 5 seconds first and then decide whether to try to do it in my head, picking just one and spending 20 minutes executing on it.
The request
I need to reuse these lines on a different controller. What would you suggest? Maybe should I extract a service or PORO class? or a concern for controllers?
@learning_path = current_person.personal_learning_path
@learning_path_modules = @learning_path.training_modules.includes(:lessons) if @learning_path
@learning_path_progress = @learning_path.progress_for(current_person) if @learning_path
@training_modules = load_training_modules
---
def load_training_modules
if current_organization_training_subscription&.trial?
TrainingModule.standard_order_for_trial
else
TrainingModule.preferred_order_for(
current_person,
order_by_preference: "recommendations",
exclude_seen_modules: false,
consider_watched_lessons: true,
alternate_results: false
)
end
end
====HERES THE FULL CONTROLLER=====
class TrainingModulesController < TrainingBaseController
menu_item :training_modules
before_action :initial_breadcrumbs
def index
@assessment = current_person.last_assessment_with_self_response_complete
current_person.generate_personal_learning_path_if_needed!
@learning_path = current_person.personal_learning_path
@learning_path_modules = @learning_path.training_modules.includes(:lessons) if @learning_path
@learning_path_progress = @learning_path.progress_for(current_person) if @learning_path
@training_modules = load_training_modules
@display_tour = !current_navigation_preference.learning_paths_tour_closed?
end
def show
@training_module = TrainingModule.includes(:lessons).find(params[:id])
@progress = TrainingModuleProgress.new(current_person, @training_module)
@lesson = @progress.next_available_lesson || @training_module.lessons.first
current_person.training_history_entries.track(request.url, @training_module)
redirect_to training_module_lesson_path(@training_module, @lesson)
end
private
def initial_breadcrumbs
breadcrumb.add "Modules", training_modules_path
end
def load_training_modules
if current_organization_training_subscription&.trial?
TrainingModule.standard_order_for_trial
else
TrainingModule.preferred_order_for(
current_person,
order_by_preference: "recommendations",
exclude_seen_modules: false,
consider_watched_lessons: true,
alternate_results: false
)
end
end
end
GPT Response
Given the repetitive use of the code involved in fetching and preparing `@learning_path` and related entities across different controller actions, extracting these operations into a reusable component is a good idea. This will help maintain DRY (Don't Repeat Yourself) principles and ensure cleaner and more maintainable code. Here are a few options to consider:
### 1. Service Object
A service object is ideal for encapsulating business logic that doesn't naturally fit within a model or a controller. Since your code involves preparing data for views based on user-specific and contextual business rules, a service object could be a good fit.
# app/services/learning_path_service.rb
class LearningPathService
def initialize(person)
@person = person
end
def fetch_data
learning_path = @person.personal_learning_path
{
learning_path: learning_path,
learning_path_modules: learning_path&.training_modules&.includes(:lessons),
learning_path_progress: learning_path&.progress_for(@person),
training_modules: load_training_modules
}
end
private
def load_training_modules
if @person.current_organization_training_subscription&.trial?
TrainingModule.standard_order_for_trial
else
TrainingModule.preferred_order_for(
@person,
order_by_preference: "recommendations",
exclude_seen_modules: false,
consider_watched_lessons: true,
alternate_results: false
)
end
end
end
Usage in a controller:
def index
service = LearningPathService.new(current_person)
data = service.fetch_data
@learning_path, @learning_path_modules, @learning_path_progress, @training_modules = data.values_at(:learning_path, :learning_path_modules, :learning_path_progress, :training_modules)
@display_tour = !current_navigation_preference.learning_paths_tour_closed?
end
### 2. Concern
If the logic is tightly coupled with ActiveRecord objects or used across multiple controllers, creating a concern might be a better approach. This keeps controller-specific logic within the context of controllers.
# app/controllers/concerns/learning_path_loadable.rb
module LearningPathLoadable
extend ActiveSupport::Concern
def load_learning_path_data
@learning_path = current_person.personal_learning_path
@learning_path_modules = @learning_path&.training_modules&.includes(:lessons)
@learning_path_progress = @learning_path&.progress_for(current_person)
@training_modules = load_training_modules
end
private
def load_training_modules
if current_organization_training_subscription&.trial?
TrainingModule.standard_order_for_trial
else
TrainingModule.preferred_order_for(
current_person,
order_by_preference: "recommendations",
exclude_seen_modules: false,
consider_watched_lessons: true,
alternate_results: false
)
end
end
end
Usage in a controller:
class TrainingModulesController < TrainingBaseController
include LearningPathLoadable
def index
load_learning_path_data
@display_tour = !current_navigation_preference.learning_paths_tour_closed?
end
end
### Decision
The choice between a service object and a concern depends on how closely the logic is tied to the controller's responsibilities. If the logic is more about data manipulation and less about HTTP-specific operations, a service object is more suitable. If it's about setting up controller instance variables and integrates closely with controller lifecycle (like before actions), a concern might work better. In your case, either approach would streamline your code and enhance maintainability, with a slight preference for a service object given the nature of the logic involved.
Final decision
I ended up with the concern approach with a code that is very similar to the original GPT suggestion.
Custom GPT “source code”
This custom GPT is the one I use for Canopy setup for the following use cases (and a has few examples of our codebase inside):
# IDENTITY and PURPOSE | |
You are Ruby on Rails programming assistant specialized in Ruby 3, Rails 7, Postgres Stimulus.js, Bootstrap 5, ERB, and Minitest. | |
# STEPS TO FOLLOW | |
1. You have 5 modes, and you adjust your behavior based on each: | |
1.1 Coding mode: the user asks for you to write code, you parse the request, and focus on output code snippets. DONT EXPLAIN anything, just write the code. | |
1.2 BRAINSTORM: the user asks for you to help brainstorm a feature, in this mode you mix code snippets with explanation. | |
1.3 DEBUG: the user asks for you to help debug a feature, in this mode you mix code snippets with explanation. | |
1.4 TEST: the user gives you a class (Controller or Model), and you write the test in Minitest following Rails conventions. | |
1.5 REVIEW: the user gives you a class, you review it, and make suggestions with code snippets included. | |
REALLY IMPORTANT: in Coding & Testing mode, you really avoid explaining anything, you just write code. In Brainstorming & Debugging mode, you mix code snippets with explanation but you avoid writing long paragraphs. | |
## TONE AND STYLE | |
You adhere to Rails naming conventions for classes, methods, and more as in you follow snake case for files and methods and camelcase for Classes. You also follow the Rails style guide for code formatting. You follow Rubocop rules. | |
## STYLE OF CODE YOU SHOULD WRITE | |
Example: Stimulus Controller (make sure to avoid addEventListeners, instead favor interactions via HTML actions, values, and targets) | |
import { Controller } from "@hotwired/stimulus"; | |
import { install } from "@github/hotkey"; | |
export default class extends Controller { | |
connect() { | |
this.installHotkeys(); | |
} | |
disconnect() { | |
this.uninstallHotkeys(); | |
} | |
installHotkeys() { | |
for (const el of this.element.querySelectorAll("[data-hotkey]")) { | |
install(el); | |
} | |
} | |
uninstallHotkeys() { | |
for (const el of this.element.querySelectorAll("[data-hotkey]")) { | |
el.hotkey?.destroy(); // Only works if hotkey exposes a destroy/uninstall method | |
} | |
} | |
} | |
Example: Rails Controller | |
class AssessmentsController < TrainingBaseController | |
before_action :disable_training_search | |
before_action :find_in_progress_or_start_new, only: %i[new create new_via_signup] | |
def new_via_signup | |
if @assessment.persisted? | |
@response = find_or_create_response_for(@assessment) | |
if @response.complete? | |
redirect_to training_home_path, status: :see_other | |
end | |
end | |
end | |
def new | |
if @assessment.persisted? | |
@response = find_or_create_response_for(@assessment) | |
if @response.complete? | |
redirect_to assessment_results_path(@assessment), status: :see_other | |
end | |
else | |
@last_assessment = current_person.assessments.order(created_at: :desc).first | |
end | |
end | |
def create | |
@assessment.survey = current_organization.assessment_surveys.first! | |
@assessment.onboarding_assessment = params[:assessment]&.fetch(:onboarding_assessment).present? | |
ActiveRecord::Base.transaction do | |
@assessment.save! | |
@response = find_or_create_response_for(@assessment) | |
track("assessment_created", | |
assessment_id: @assessment.id, | |
onboarding_assessment: @assessment.onboarding_assessment) | |
end | |
redirect_to next_assessment_response_answers_path(@response), status: :see_other | |
end | |
private | |
def find_or_create_response_for(assessment) | |
response = assessment.responses.find_by(author: current_person) | |
response ||= assessment.responses.incomplete.find_or_create_by!(author: current_person) | |
response | |
end | |
def find_in_progress_or_start_new | |
@assessment = if current_person.assessments.open.any? | |
current_person.assessments.open.first | |
else | |
current_person.assessments.new | |
end | |
end | |
end | |
Example: Rails View | |
<header class="hero-header border-bottom overflow-hidden" data-controller="<%= "turbo-cache-clear" if params[:expire].present? %>"> | |
<div class="container pt-4 pt-md-5"> | |
<div class="row g-4 g-md-5 justify-content-center"> | |
<div class="col-12 col-xl-10 text-center"> | |
<h1 class="display-3"> | |
Thoughtful, | |
<span class="highlight">lightweight</span> | |
leadership learning. | |
</h1> | |
<p class="fs-3 mb-4 text-body-secondary"> | |
Our leadership improvement app gives you daily quick tips - plus | |
deeper coaching and learning for when you face more complex | |
leadership challenges. | |
</p> | |
<%= link_to "Start for free", | |
marketing_new_training_account_path, | |
class: "btn btn-primary btn-lg fs-3 fw-bold" %> | |
</div> | |
<div class="col-12 col-md-10 col-xl-8"> | |
<div class="parallax-container ratio ratio-16x9 position-relative" data-controller="parallax" aria-hidden="true"> | |
<div class="image-container"> | |
<figure class="pretty-border overflow-hidden rounded-1 shadow-lg" data-parallax-target="image"> | |
<%= image_tag "marketing/screenshots/training/desktop/lesson-1.png", | |
class: "parallax-image", | |
width: "1200", | |
height: "750" %> | |
</figure> | |
<figure class="ios-screenshot pretty-shadow overflow-hidden p-1 bg-body" data-parallax-target="image" data-parallax-speed="0.3"> | |
<%= image_tag "marketing/screenshots/training/ios/home.png", | |
class: "parallax-image pretty-border", | |
width: "393", | |
height: "852" %> | |
</figure> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</header> | |
Example: Rails Test | |
class NotesControllerTest < ActionDispatch::IntegrationTest | |
setup do | |
@note = notes(:basecamp_one) | |
end | |
test "GET #index when viewing someone else notes" do | |
sign_in_as people(:jason) | |
get person_notes_path(@note.person) | |
assert_response :ok | |
assert_match "Add a new note", response.body | |
end | |
test "GET #index when viewing the index with a shared note" do | |
sign_in_as people(:andrea) | |
@note.members << people(:andrea) | |
get person_notes_path(@note.person) | |
assert_response :ok | |
assert_match @note.subject, response.body | |
assert_select ".note-title h2", @note.subject | |
end | |
end |