prompt_navigator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +210 -0
- data/Rakefile +6 -0
- data/app/assets/config/prompt_navigator_manifest.js +2 -0
- data/app/assets/stylesheets/prompt_navigator/application.css +15 -0
- data/app/assets/stylesheets/prompt_navigator/history.css +76 -0
- data/app/controllers/concerns/prompt_navigator/history_manageable.rb +19 -0
- data/app/controllers/prompt_navigator/application_controller.rb +4 -0
- data/app/helpers/prompt_navigator/application_helper.rb +4 -0
- data/app/javascript/controllers/history_controller.js +123 -0
- data/app/jobs/prompt_navigator/application_job.rb +4 -0
- data/app/mailers/prompt_navigator/application_mailer.rb +6 -0
- data/app/models/prompt_navigator/application_record.rb +5 -0
- data/app/models/prompt_navigator/prompt_execution.rb +13 -0
- data/app/views/layouts/prompt_navigator/application.html.erb +17 -0
- data/app/views/prompt_navigator/_history.html.erb +16 -0
- data/app/views/prompt_navigator/_history_card.html.erb +18 -0
- data/config/routes.rb +2 -0
- data/lib/generators/prompt_navigator/modeling/modeling_generator.rb +19 -0
- data/lib/generators/prompt_navigator/modeling/templates/db/migrate/20260129073026_create_prompt_navigator_prompt_executions.rb +17 -0
- data/lib/prompt_navigator/engine.rb +38 -0
- data/lib/prompt_navigator/helpers.rb +7 -0
- data/lib/prompt_navigator/version.rb +3 -0
- data/lib/prompt_navigator.rb +10 -0
- data/lib/tasks/prompt_navigator_tasks.rake +4 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 206bf5dde601862fee9a65dc6fd8b8af2324a4e3ae8a952ef30be31b1c650028
|
|
4
|
+
data.tar.gz: 32724a12d078a3f9bba48c6ce120010c2f7d1b2d41158d373d8bdc03c17cc754
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 51872a8c58c3dee2aa6ed61c4622b4eee4bfe0fa81f4a593f7ab4be01d2bcb5beebb94aeba3a5a959ed8b000610bf3cd213b8ab668870866a2ed0af4e83dc3c1
|
|
7
|
+
data.tar.gz: a36897e24af8beb7718bfe5693ca69ce746f57905bf823dd3a0ecbf13142984339f5e81b4b7852d0fed83d77515ed6ba1aa839dabd15fdd3df1da05b5cfa46d3
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright dhq_boiler
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# PromptNavigator
|
|
2
|
+
|
|
3
|
+
Rails engine for managing prompt execution history with visual interface.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Visual history stack with parent-child relationships
|
|
8
|
+
- Modern CSS with nested syntax
|
|
9
|
+
- Stimulus-powered interactive arrows
|
|
10
|
+
- Automatic asset pipeline integration
|
|
11
|
+
- Active state highlighting
|
|
12
|
+
- Responsive design with hover effects
|
|
13
|
+
- Built-in model for tracking prompt executions
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "prompt_navigator"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
And then execute:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
$ bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Generate the migration for prompt executions:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ rails generate prompt_navigator:modeling
|
|
33
|
+
$ rails db:migrate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This will create the `prompt_navigator_prompt_executions` table with the following fields:
|
|
37
|
+
|
|
38
|
+
- `execution_id` - Unique identifier (UUID) for each prompt execution
|
|
39
|
+
- `prompt` - The prompt text
|
|
40
|
+
- `llm_platform` - The LLM platform used (e.g., "openai", "anthropic")
|
|
41
|
+
- `model` - The model name (e.g., "gpt-4", "claude-3")
|
|
42
|
+
- `configuration` - Model configuration settings
|
|
43
|
+
- `response` - The LLM response
|
|
44
|
+
- `previous_id` - Reference to the parent execution (for history tree)
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
### Basic Setup
|
|
49
|
+
|
|
50
|
+
In your application's layout file (`app/views/layouts/application.html.erb`), make sure you have `<%= yield :head %>` in the `<head>` section:
|
|
51
|
+
|
|
52
|
+
```erb
|
|
53
|
+
<head>
|
|
54
|
+
<title>Your App</title>
|
|
55
|
+
<%= csrf_meta_tags %>
|
|
56
|
+
<%= csp_meta_tag %>
|
|
57
|
+
|
|
58
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
59
|
+
<%= javascript_importmap_tags %>
|
|
60
|
+
|
|
61
|
+
<%= yield :head %>
|
|
62
|
+
</head>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Include the history partial in your view:
|
|
66
|
+
|
|
67
|
+
```erb
|
|
68
|
+
<%= render 'prompt_navigator/history', locals: { active_uuid: @current_execution_id, card_path: ->(execution_id) { my_path(execution_id) } } %>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Asset Pipeline Configuration
|
|
72
|
+
|
|
73
|
+
The engine automatically configures the asset pipeline to include:
|
|
74
|
+
- `prompt_navigator/history.css` - Styles for the history component
|
|
75
|
+
- `controllers/history_controller.js` - Stimulus controller for arrow drawing
|
|
76
|
+
|
|
77
|
+
The assets are automatically precompiled and made available to your application. For Rails 7+ with importmap, the CSS will be loaded via `stylesheet_link_tag` when you render the history partial, and the Stimulus controller will be automatically registered.
|
|
78
|
+
|
|
79
|
+
### Controller Setup
|
|
80
|
+
|
|
81
|
+
Include `HistoryManageable` concern in your controller:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class MyController < ApplicationController
|
|
85
|
+
include HistoryManageable
|
|
86
|
+
|
|
87
|
+
def index
|
|
88
|
+
# Initialize history with prompt executions
|
|
89
|
+
initialize_history(PromptNavigator::PromptExecution.all)
|
|
90
|
+
|
|
91
|
+
# Set the active execution ID (optional)
|
|
92
|
+
set_active_message_uuid(params[:execution_id])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def create
|
|
96
|
+
# Add new execution to history
|
|
97
|
+
new_execution = PromptNavigator::PromptExecution.create(
|
|
98
|
+
prompt: params[:prompt],
|
|
99
|
+
llm_platform: "openai",
|
|
100
|
+
model: "gpt-4",
|
|
101
|
+
configuration: params[:config].to_json,
|
|
102
|
+
response: llm_response,
|
|
103
|
+
previous: @current_execution
|
|
104
|
+
)
|
|
105
|
+
push_to_history(new_execution)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Using PromptExecution Model
|
|
111
|
+
|
|
112
|
+
The `PromptNavigator::PromptExecution` model has the following attributes and methods:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
execution = PromptNavigator::PromptExecution.create(
|
|
116
|
+
prompt: "Your prompt text",
|
|
117
|
+
llm_platform: "openai",
|
|
118
|
+
model: "gpt-4",
|
|
119
|
+
configuration: "{\"temperature\": 0.7}",
|
|
120
|
+
response: "LLM response text",
|
|
121
|
+
previous: parent_execution # Optional: for building history tree
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Access fields
|
|
125
|
+
execution.execution_id # UUID, automatically generated
|
|
126
|
+
execution.prompt # The prompt text
|
|
127
|
+
execution.llm_platform # LLM platform used
|
|
128
|
+
execution.model # Model name
|
|
129
|
+
execution.configuration # Configuration JSON
|
|
130
|
+
execution.response # LLM response
|
|
131
|
+
execution.previous # Parent execution (belongs_to association)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Using the Helper Method
|
|
135
|
+
|
|
136
|
+
The helper methods are automatically included in your views. You can use the `history_list` helper:
|
|
137
|
+
|
|
138
|
+
```erb
|
|
139
|
+
<%= history_list(->(execution_id) { my_item_path(execution_id) }, active_uuid: @current_execution_id) %>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Customizing the Card Path
|
|
143
|
+
|
|
144
|
+
The `card_path` parameter should be a callable (Proc or lambda) that takes an execution_id and returns a path:
|
|
145
|
+
|
|
146
|
+
```erb
|
|
147
|
+
<%= render 'prompt_navigator/history',
|
|
148
|
+
locals: {
|
|
149
|
+
active_uuid: @current_execution_id,
|
|
150
|
+
card_path: ->(execution_id) { my_item_path(execution_id) }
|
|
151
|
+
}
|
|
152
|
+
%>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Or using the helper:
|
|
156
|
+
|
|
157
|
+
```erb
|
|
158
|
+
<%= history_list(->(execution_id) { my_item_path(execution_id) }, active_uuid: @current_execution_id) %>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Troubleshooting
|
|
162
|
+
|
|
163
|
+
### Styles not loading
|
|
164
|
+
|
|
165
|
+
Make sure you have `<%= yield :head %>` in your application layout's `<head>` section.
|
|
166
|
+
|
|
167
|
+
### Arrows not appearing
|
|
168
|
+
|
|
169
|
+
The arrow visualization requires:
|
|
170
|
+
1. Stimulus to be properly configured in your application
|
|
171
|
+
2. The `history_controller.js` to be loaded (automatic with importmap)
|
|
172
|
+
3. Parent-child relationships to be properly set using the `previous` association in your PromptExecution records
|
|
173
|
+
|
|
174
|
+
### History not displaying
|
|
175
|
+
|
|
176
|
+
Ensure that:
|
|
177
|
+
1. `@history` instance variable is set in your controller using `initialize_history`
|
|
178
|
+
2. Your PromptExecution records have `execution_id` values (automatically generated on create)
|
|
179
|
+
3. The `card_path` callable is correctly defined and returns valid paths
|
|
180
|
+
|
|
181
|
+
## Requirements
|
|
182
|
+
|
|
183
|
+
- Rails >= 8.1.2
|
|
184
|
+
- Stimulus (Hotwired)
|
|
185
|
+
|
|
186
|
+
## Installation
|
|
187
|
+
|
|
188
|
+
Add this line to your application's Gemfile:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
gem "prompt_navigator"
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
And then execute:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
$ bundle
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or install it yourself as:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
$ gem install prompt_navigator
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
Contribution directions go here.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.history-stack {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 8px;
|
|
6
|
+
padding-left: 32px; /* space for arrows */
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.history-card {
|
|
10
|
+
background: #fff;
|
|
11
|
+
border: 1px solid #ddd;
|
|
12
|
+
border-radius: 8px;
|
|
13
|
+
padding: 8px 10px;
|
|
14
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
15
|
+
position: relative;
|
|
16
|
+
z-index: 1;
|
|
17
|
+
transition:
|
|
18
|
+
box-shadow 0.15s ease,
|
|
19
|
+
transform 0.15s ease;
|
|
20
|
+
|
|
21
|
+
&.is-active {
|
|
22
|
+
border-color: #007bff;
|
|
23
|
+
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
28
|
+
transform: translateY(-2px);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.history-card-link {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: auto 1fr auto;
|
|
35
|
+
grid-gap: 8px;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
color: #333;
|
|
38
|
+
align-items: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.history-card-number {
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: #555;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.history-card-prompt {
|
|
48
|
+
font-size: 12px;
|
|
49
|
+
line-height: 1.3;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
display: -webkit-box;
|
|
52
|
+
-webkit-line-clamp: 2;
|
|
53
|
+
-webkit-box-orient: vertical;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.history-arrows {
|
|
57
|
+
position: absolute;
|
|
58
|
+
top: 0;
|
|
59
|
+
left: 0;
|
|
60
|
+
width: 100%;
|
|
61
|
+
height: 100%;
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
overflow: visible;
|
|
64
|
+
z-index: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.history-straight-arrow {
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
align-items: center;
|
|
71
|
+
height: 8px;
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 10px;
|
|
74
|
+
color: #555;
|
|
75
|
+
line-height: 1;
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptNavigator
|
|
4
|
+
module HistoryManageable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
def initialize_history(history)
|
|
8
|
+
@history = history.to_a
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def set_active_message_uuid(uuid)
|
|
12
|
+
@active_message_uuid = uuid
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def push_to_history(new_state)
|
|
16
|
+
@history << new_state
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Draw arrows connecting child cards to their parent card vertically.
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = ["svg", "cards"]
|
|
6
|
+
|
|
7
|
+
// Private fields
|
|
8
|
+
#drawArrowsBound
|
|
9
|
+
#markerId = "history-arrow-head"
|
|
10
|
+
#startX = 32
|
|
11
|
+
#curveOffset = 40
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
this.#drawArrows()
|
|
15
|
+
this.#drawArrowsBound = this.#drawArrows.bind(this)
|
|
16
|
+
window.addEventListener("resize", this.#drawArrowsBound)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnect() {
|
|
20
|
+
window.removeEventListener("resize", this.#drawArrowsBound)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ================= Private methods =================
|
|
24
|
+
#drawArrows() {
|
|
25
|
+
// Skip if svg target is missing (template variant without arrow canvas)
|
|
26
|
+
if (!this.hasSvgTarget) return
|
|
27
|
+
const svg = this.svgTarget
|
|
28
|
+
|
|
29
|
+
this.#clearSvg(svg)
|
|
30
|
+
const cardMap = this.#buildCardMap()
|
|
31
|
+
const bbox = this.#setupSvgDimensions(svg)
|
|
32
|
+
|
|
33
|
+
this.#ensureArrowMarker(svg)
|
|
34
|
+
|
|
35
|
+
// Iterate over Stimulus targets instead of querySelectorAll
|
|
36
|
+
for (const card of this.cardsTargets) {
|
|
37
|
+
this.#drawArrowForCard(card, cardMap, bbox, svg)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#clearSvg(svg) {
|
|
42
|
+
svg.replaceChildren()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#buildCardMap() {
|
|
46
|
+
const cardMap = new Map()
|
|
47
|
+
for (const c of this.cardsTargets) {
|
|
48
|
+
cardMap.set(c.dataset.uuid, c)
|
|
49
|
+
}
|
|
50
|
+
return cardMap
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#setupSvgDimensions(svg) {
|
|
54
|
+
const bbox = this.element.getBoundingClientRect()
|
|
55
|
+
svg.setAttribute("width", bbox.width)
|
|
56
|
+
svg.setAttribute("height", bbox.height)
|
|
57
|
+
svg.setAttribute("viewBox", `0 0 ${bbox.width} ${bbox.height}`)
|
|
58
|
+
return bbox
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#ensureArrowMarker(svg) {
|
|
62
|
+
if (svg.querySelector(`#${this.#markerId}`)) return
|
|
63
|
+
|
|
64
|
+
const marker = document.createElementNS(
|
|
65
|
+
"http://www.w3.org/2000/svg",
|
|
66
|
+
"marker"
|
|
67
|
+
)
|
|
68
|
+
marker.setAttribute("id", this.#markerId)
|
|
69
|
+
marker.setAttribute("markerWidth", "6")
|
|
70
|
+
marker.setAttribute("markerHeight", "6")
|
|
71
|
+
marker.setAttribute("refX", "5")
|
|
72
|
+
marker.setAttribute("refY", "3")
|
|
73
|
+
marker.setAttribute("orient", "auto")
|
|
74
|
+
|
|
75
|
+
const arrowPath = document.createElementNS(
|
|
76
|
+
"http://www.w3.org/2000/svg",
|
|
77
|
+
"path"
|
|
78
|
+
)
|
|
79
|
+
arrowPath.setAttribute("d", "M0,0 L6,3 L0,6 Z")
|
|
80
|
+
arrowPath.setAttribute("fill", "#555")
|
|
81
|
+
marker.appendChild(arrowPath)
|
|
82
|
+
|
|
83
|
+
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs")
|
|
84
|
+
defs.appendChild(marker)
|
|
85
|
+
svg.appendChild(defs)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#drawArrowForCard(card, cardMap, bbox, svg) {
|
|
89
|
+
const parentUuid = card.dataset.parentUuid
|
|
90
|
+
if (!parentUuid) return
|
|
91
|
+
|
|
92
|
+
const parentCard = cardMap.get(parentUuid)
|
|
93
|
+
if (!parentCard) return
|
|
94
|
+
|
|
95
|
+
const childRect = card.getBoundingClientRect()
|
|
96
|
+
const parentRect = parentCard.getBoundingClientRect()
|
|
97
|
+
|
|
98
|
+
const startY = parentRect.top + parentRect.height / 2 - bbox.top
|
|
99
|
+
const endY = childRect.top + childRect.height / 2 - bbox.top
|
|
100
|
+
const verticalGap = Math.abs(endY - startY)
|
|
101
|
+
|
|
102
|
+
// Skip if cards are adjacent - straight arrow is rendered by helper
|
|
103
|
+
if (verticalGap < 80) return
|
|
104
|
+
|
|
105
|
+
const path = this.#createCurvedArrowPath(startY, endY)
|
|
106
|
+
svg.appendChild(path)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#createCurvedArrowPath(startY, endY) {
|
|
110
|
+
const startX = this.#startX // left edge margin
|
|
111
|
+
const curveX = startX - this.#curveOffset // curve outward to the left
|
|
112
|
+
const pathData = `M ${startX} ${startY} C ${curveX} ${startY}, ${curveX} ${endY}, ${startX} ${endY}`
|
|
113
|
+
|
|
114
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
|
|
115
|
+
path.setAttribute("d", pathData)
|
|
116
|
+
path.setAttribute("fill", "none")
|
|
117
|
+
path.setAttribute("stroke", "#555")
|
|
118
|
+
path.setAttribute("stroke-width", "1.2")
|
|
119
|
+
path.setAttribute("marker-end", `url(#${this.#markerId})`)
|
|
120
|
+
|
|
121
|
+
return path
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module PromptNavigator
|
|
2
|
+
class PromptExecution < ApplicationRecord
|
|
3
|
+
belongs_to :previous, class_name: "PromptNavigator::PromptExecution", optional: true
|
|
4
|
+
|
|
5
|
+
before_create :set_execution_id
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def set_execution_id
|
|
10
|
+
self.execution_id ||= SecureRandom.uuid
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Prompt navigator</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "prompt_navigator/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<%
|
|
2
|
+
active_uuid = locals[:active_uuid]
|
|
3
|
+
card_path = locals[:card_path]
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<h2>History</h2>
|
|
7
|
+
<% if @history.present? %>
|
|
8
|
+
<div class="history-stack" data-controller="history">
|
|
9
|
+
<% @history.each_with_index do |ann, idx| %>
|
|
10
|
+
<%= render 'prompt_navigator/history_card', locals: { ann: ann, next_ann: @history[idx + 1], is_active: ann.execution_id == active_uuid, card_path: card_path } %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<svg class="history-arrows" data-history-target="svg"></svg>
|
|
13
|
+
</div>
|
|
14
|
+
<% else %>
|
|
15
|
+
<p class="history-empty">No history yet</p>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%
|
|
2
|
+
ann = locals[:ann]
|
|
3
|
+
next_ann = locals[:next_ann]
|
|
4
|
+
is_active = locals[:is_active]
|
|
5
|
+
parent_uuid = ann.previous&.execution_id
|
|
6
|
+
card_path = locals[:card_path]
|
|
7
|
+
%>
|
|
8
|
+
|
|
9
|
+
<div class="history-card<%= ' is-active' if is_active %>" data-history-target="cards" data-uuid="<%= ann.execution_id %>" data-parent-uuid="<%= parent_uuid %>">
|
|
10
|
+
<%= link_to card_path.call(ann.execution_id), class: "history-card-link" do %>
|
|
11
|
+
<div class="history-card-prompt"><%= truncate(ann.prompt.to_s, length: 30) %></div>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<%# Check !next_ann.nil? to prevent the "↑" arrow from appearing below the bottom history entry, as parent_uuid would be nil in that case. %>
|
|
16
|
+
<% if !next_ann.nil? && parent_uuid == next_ann.execution_id %>
|
|
17
|
+
<div class="history-straight-arrow">↑</div>
|
|
18
|
+
<% end %>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
module PromptNavigator
|
|
3
|
+
module Generators
|
|
4
|
+
class ModelingGenerator < Rails::Generators::Base
|
|
5
|
+
include Rails::Generators::Migration
|
|
6
|
+
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def self.next_migration_number(dirname)
|
|
10
|
+
next_migration_number = current_migration_number(dirname) + 1
|
|
11
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add_migrations
|
|
15
|
+
migration_template "db/migrate/20260129073026_create_prompt_navigator_prompt_executions.rb", "db/migrate/create_prompt_navigator_prompt_executions.rb"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreatePromptNavigatorPromptExecutions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :prompt_navigator_prompt_executions do |t|
|
|
4
|
+
t.references :previous, foreign_key: { to_table: :prompt_navigator_prompt_executions }, index: true, null: true
|
|
5
|
+
# Unique identifier for prompt execution
|
|
6
|
+
# Used to highlight active entries in History list and display details on click
|
|
7
|
+
t.string :execution_id
|
|
8
|
+
t.text :prompt
|
|
9
|
+
t.string :llm_platform
|
|
10
|
+
t.string :model
|
|
11
|
+
t.string :configuration
|
|
12
|
+
t.text :response
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptNavigator
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace PromptNavigator
|
|
6
|
+
|
|
7
|
+
# Initialize helper methods to be available in ActionView
|
|
8
|
+
# This makes PromptNavigator::Helpers methods accessible in Rails views
|
|
9
|
+
initializer "prompt_navigator.helpers" do
|
|
10
|
+
ActiveSupport.on_load(:action_view) do
|
|
11
|
+
include PromptNavigator::Helpers
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "prompt_navigator.controllers" do
|
|
16
|
+
ActiveSupport.on_load(:action_controller) do
|
|
17
|
+
include PromptNavigator::HistoryManageable
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Configure asset paths for the engine
|
|
22
|
+
# This ensures that JavaScript and CSS files are properly loaded
|
|
23
|
+
initializer "prompt_navigator.assets", before: "sprockets.environment" do |app|
|
|
24
|
+
# Add asset paths
|
|
25
|
+
app.config.assets.paths << root.join("app/assets/stylesheets").to_s
|
|
26
|
+
app.config.assets.paths << root.join("app/javascript").to_s
|
|
27
|
+
|
|
28
|
+
# Precompile assets
|
|
29
|
+
if app.config.respond_to?(:assets)
|
|
30
|
+
app.config.assets.precompile += %w[
|
|
31
|
+
prompt_navigator/history.css
|
|
32
|
+
prompt_navigator/application.css
|
|
33
|
+
controllers/history_controller.js
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prompt_navigator/version"
|
|
4
|
+
require "prompt_navigator/engine"
|
|
5
|
+
require "prompt_navigator/helpers"
|
|
6
|
+
require_relative "../app/controllers/concerns/prompt_navigator/history_manageable"
|
|
7
|
+
|
|
8
|
+
module PromptNavigator
|
|
9
|
+
# Your code goes here...
|
|
10
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: prompt_navigator
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- dhq_boiler
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 8.1.2
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 8.1.2
|
|
32
|
+
description: PromptNavigator is a Rails engine that provides a visual history stack
|
|
33
|
+
UI for tracking LLM prompt executions. It includes a self-referencing PromptExecution
|
|
34
|
+
model for building conversation trees, Stimulus-powered SVG arrow visualization
|
|
35
|
+
between parent-child history cards, and automatic asset pipeline integration.
|
|
36
|
+
email:
|
|
37
|
+
- dhq_boiler@live.jp
|
|
38
|
+
executables: []
|
|
39
|
+
extensions: []
|
|
40
|
+
extra_rdoc_files: []
|
|
41
|
+
files:
|
|
42
|
+
- MIT-LICENSE
|
|
43
|
+
- README.md
|
|
44
|
+
- Rakefile
|
|
45
|
+
- app/assets/config/prompt_navigator_manifest.js
|
|
46
|
+
- app/assets/stylesheets/prompt_navigator/application.css
|
|
47
|
+
- app/assets/stylesheets/prompt_navigator/history.css
|
|
48
|
+
- app/controllers/concerns/prompt_navigator/history_manageable.rb
|
|
49
|
+
- app/controllers/prompt_navigator/application_controller.rb
|
|
50
|
+
- app/helpers/prompt_navigator/application_helper.rb
|
|
51
|
+
- app/javascript/controllers/history_controller.js
|
|
52
|
+
- app/jobs/prompt_navigator/application_job.rb
|
|
53
|
+
- app/mailers/prompt_navigator/application_mailer.rb
|
|
54
|
+
- app/models/prompt_navigator/application_record.rb
|
|
55
|
+
- app/models/prompt_navigator/prompt_execution.rb
|
|
56
|
+
- app/views/layouts/prompt_navigator/application.html.erb
|
|
57
|
+
- app/views/prompt_navigator/_history.html.erb
|
|
58
|
+
- app/views/prompt_navigator/_history_card.html.erb
|
|
59
|
+
- config/routes.rb
|
|
60
|
+
- lib/generators/prompt_navigator/modeling/modeling_generator.rb
|
|
61
|
+
- lib/generators/prompt_navigator/modeling/templates/db/migrate/20260129073026_create_prompt_navigator_prompt_executions.rb
|
|
62
|
+
- lib/prompt_navigator.rb
|
|
63
|
+
- lib/prompt_navigator/engine.rb
|
|
64
|
+
- lib/prompt_navigator/helpers.rb
|
|
65
|
+
- lib/prompt_navigator/version.rb
|
|
66
|
+
- lib/tasks/prompt_navigator_tasks.rake
|
|
67
|
+
homepage: https://github.com/jdkim/prompt_manager
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
allowed_push_host: https://rubygems.org
|
|
72
|
+
homepage_uri: https://github.com/jdkim/prompt_manager
|
|
73
|
+
source_code_uri: https://github.com/jdkim/prompt_manager
|
|
74
|
+
changelog_uri: https://github.com/jdkim/prompt_manager/blob/main/CHANGELOG.md
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.2.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.6.9
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: A Rails engine for managing and visualizing LLM prompt execution history.
|
|
92
|
+
test_files: []
|