A Rails engine for managing and visualizing LLM prompt execution history. It provides a visual history stack UI with interactive SVG arrow connections between parent-child prompt executions, enabling users to navigate conversation trees.
- PromptExecution model - Self-referencing tree structure for tracking prompt executions with UUID identifiers
- Visual history stack - Card-based UI displaying prompt history with parent-child relationships
- Arrow visualization - Straight arrows (
↑) for adjacent cards; curved SVG arrows (via Stimulus) for non-adjacent parent-child connections - Active state highlighting - Blue border and glow effect on the currently selected card
- Responsive design - Hover effects with lift animation; arrows redraw on window resize
- Automatic integration - Controller concern, view helpers, and assets are auto-registered by the engine
- Rails generator -
prompt_navigator:modelinggenerator for creating the required migration
- Ruby >= 3.2.0
- Rails >= 8.1.2
- Stimulus (Hotwire)
Add this line to your application's Gemfile:
gem "prompt_navigator"And then execute:
$ bundle installGenerate the migration for prompt executions:
$ rails generate prompt_navigator:modeling
$ rails db:migrateThis creates the prompt_navigator_prompt_executions table with the following columns:
| Column | Type | Description |
|---|---|---|
execution_id |
string | Unique identifier (UUID), auto-generated on create |
prompt |
text | The prompt text sent to the LLM |
llm_platform |
string | The LLM platform (e.g., "openai", "anthropic") |
model |
string | The model name (e.g., "gpt-4", "claude-3") |
configuration |
string | Model configuration as JSON |
response |
text | The LLM response text |
previous_id |
integer | Foreign key to the parent execution (for building history tree) |
In your application layout (app/views/layouts/application.html.erb), add <%= yield :head %> in the <head> section to load the engine's stylesheets:
<head>
<title>Your App</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
<%= yield :head %>
</head>The HistoryManageable concern is automatically included in all controllers by the engine. It provides three methods:
initialize_history(history)- Sets the@historyinstance variable (converts to array)set_active_message_uuid(uuid)- Sets the@active_message_uuidinstance variablepush_to_history(new_state)- Appends a new state to@history
class MyController < ApplicationController
def index
# History must be ordered newest-first for arrow visualization to work correctly
initialize_history(PromptNavigator::PromptExecution.order(id: :desc))
set_active_message_uuid(params[:execution_id])
end
def create
new_execution = PromptNavigator::PromptExecution.create(
prompt: params[:prompt],
llm_platform: "openai",
model: "gpt-4",
configuration: params[:config].to_json,
response: llm_response,
previous: @current_execution
)
push_to_history(new_execution)
end
endThe history_list helper is automatically available in all views. It renders the history stack:
<%= history_list(
->(execution_id) { my_item_path(execution_id) },
active_uuid: @active_message_uuid
) %>Parameters:
card_path(first argument) - A Proc/Lambda that takes anexecution_idand returns a URL path for the card linkactive_uuid:- Theexecution_idof the currently active card (highlighted with blue border)
Alternatively, you can render the partial directly:
<%= render "prompt_navigator/history",
locals: {
active_uuid: @active_message_uuid,
card_path: ->(execution_id) { my_item_path(execution_id) }
}
%>The PromptNavigator::PromptExecution model stores prompt execution records with a self-referencing association for building conversation trees:
# Create an execution
execution = PromptNavigator::PromptExecution.create(
prompt: "Explain Ruby blocks",
llm_platform: "openai",
model: "gpt-4",
configuration: '{"temperature": 0.7}',
response: "Ruby blocks are..."
)
# Create a follow-up execution linked to the parent
follow_up = PromptNavigator::PromptExecution.create(
prompt: "Give me an example",
llm_platform: "openai",
model: "gpt-4",
response: "Here is an example...",
previous: execution # Sets previous_id to link as child
)
# Access attributes
execution.execution_id # => "a1b2c3d4-..." (auto-generated UUID)
execution.previous # => nil (root execution)
follow_up.previous # => execution (parent)PromptNavigator (Rails Engine)
├── Model
│ └── PromptExecution # Self-referencing tree model with UUID
├── Controller Concern
│ └── HistoryManageable # Provides initialize_history, set_active_message_uuid, push_to_history
├── View Helper
│ └── Helpers#history_list # Renders the history partial
├── Partials
│ ├── _history.html.erb # Main container with Stimulus controller
│ └── _history_card.html.erb # Individual card with link and arrow logic
├── JavaScript
│ └── history_controller.js # Stimulus controller for SVG curved arrows
├── Stylesheets
│ └── history.css # Modern nested CSS for cards, arrows, hover effects
└── Generator
└── modeling # Creates migration for prompt_executions table
The history component uses two types of arrows to show parent-child relationships:
- Straight arrows (
↑) - Rendered as HTML when a card's parent is the immediately adjacent card below it in the list - Curved SVG arrows - Drawn by the Stimulus controller (
history_controller.js) when a card's parent is further away (vertical gap >= 80px). These bezier curves arc to the left of the stack
Note: Arrow visualization requires the history to be ordered newest-first (e.g., order(id: :desc)). If the history is in ascending order, parent-child adjacency detection will not work correctly.
The Stimulus controller automatically redraws SVG arrows on window resize.
Make sure you have <%= yield :head %> in your application layout's <head> section.
The arrow visualization requires:
- Stimulus to be properly configured in your application
- The
history_controller.jsto be loaded (automatic with the asset pipeline) - Parent-child relationships to be set using the
previousassociation on PromptExecution records
Ensure that:
@historyis set in your controller usinginitialize_history- PromptExecution records have
execution_idvalues (automatically generated on create) - The
card_pathcallable returns valid paths
After checking out the repo, run:
$ bundle installRun the tests:
$ bin/rails testRun the linter:
$ bundle exec rubocopBug reports and pull requests are welcome on GitHub.
The gem is available as open source under the terms of the MIT License.