ruby_llm-agents 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/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- metadata +164 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
# ActionCable channel for real-time execution updates
|
|
6
|
+
#
|
|
7
|
+
# Broadcasts execution create/update events to subscribed clients.
|
|
8
|
+
# Used by the dashboard to show live execution status changes.
|
|
9
|
+
#
|
|
10
|
+
# Inherits from the host app's ApplicationCable::Channel (note the :: prefix)
|
|
11
|
+
#
|
|
12
|
+
class ExecutionsChannel < ::ApplicationCable::Channel
|
|
13
|
+
def subscribed
|
|
14
|
+
stream_from "ruby_llm_agents:executions"
|
|
15
|
+
logger.info "[RubyLLM::Agents] Client subscribed to executions channel"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unsubscribed
|
|
19
|
+
logger.info "[RubyLLM::Agents] Client unsubscribed from executions channel"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class AgentsController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@agents = AgentRegistry.all_with_details
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def show
|
|
11
|
+
@agent_type = params[:id]
|
|
12
|
+
@agent_class = AgentRegistry.find(@agent_type)
|
|
13
|
+
@agent_active = @agent_class.present?
|
|
14
|
+
|
|
15
|
+
# Get stats for different time periods
|
|
16
|
+
@stats = Execution.stats_for(@agent_type, period: :all_time)
|
|
17
|
+
@stats_today = Execution.stats_for(@agent_type, period: :today)
|
|
18
|
+
|
|
19
|
+
# Get available filter options for this agent
|
|
20
|
+
@versions = Execution.by_agent(@agent_type).distinct.pluck(:agent_version).compact.sort.reverse
|
|
21
|
+
@models = Execution.by_agent(@agent_type).distinct.pluck(:model_id).compact.sort
|
|
22
|
+
@temperatures = Execution.by_agent(@agent_type).distinct.pluck(:temperature).compact.sort
|
|
23
|
+
|
|
24
|
+
# Build filtered scope
|
|
25
|
+
base_scope = Execution.by_agent(@agent_type)
|
|
26
|
+
|
|
27
|
+
# Apply status filter
|
|
28
|
+
if params[:statuses].present?
|
|
29
|
+
statuses = params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")
|
|
30
|
+
base_scope = base_scope.where(status: statuses) if statuses.any?(&:present?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Apply version filter
|
|
34
|
+
if params[:versions].present?
|
|
35
|
+
versions = params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")
|
|
36
|
+
base_scope = base_scope.where(agent_version: versions) if versions.any?(&:present?)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Apply model filter
|
|
40
|
+
if params[:models].present?
|
|
41
|
+
models = params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")
|
|
42
|
+
base_scope = base_scope.where(model_id: models) if models.any?(&:present?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Apply temperature filter
|
|
46
|
+
if params[:temperatures].present?
|
|
47
|
+
temps = params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")
|
|
48
|
+
base_scope = base_scope.where(temperature: temps) if temps.any?(&:present?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Apply time range filter
|
|
52
|
+
base_scope = base_scope.where("created_at >= ?", params[:days].to_i.days.ago) if params[:days].present?
|
|
53
|
+
|
|
54
|
+
# Paginate
|
|
55
|
+
page = (params[:page] || 1).to_i
|
|
56
|
+
per_page = 25
|
|
57
|
+
offset = (page - 1) * per_page
|
|
58
|
+
|
|
59
|
+
filtered_scope = base_scope.order(created_at: :desc)
|
|
60
|
+
total_count = filtered_scope.count
|
|
61
|
+
@executions = filtered_scope.limit(per_page).offset(offset)
|
|
62
|
+
|
|
63
|
+
@pagination = {
|
|
64
|
+
current_page: page,
|
|
65
|
+
per_page: per_page,
|
|
66
|
+
total_count: total_count,
|
|
67
|
+
total_pages: (total_count.to_f / per_page).ceil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Filter stats for summary display
|
|
71
|
+
@filter_stats = {
|
|
72
|
+
total_count: total_count,
|
|
73
|
+
total_cost: base_scope.sum(:total_cost),
|
|
74
|
+
total_tokens: base_scope.sum(:total_tokens)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Get trend data for charts (30 days)
|
|
78
|
+
@trend_data = Execution.trend_analysis(agent_type: @agent_type, days: 30)
|
|
79
|
+
|
|
80
|
+
# Get status distribution for pie chart
|
|
81
|
+
@status_distribution = Execution.by_agent(@agent_type)
|
|
82
|
+
.group(:status)
|
|
83
|
+
.count
|
|
84
|
+
|
|
85
|
+
# Agent configuration (if class exists)
|
|
86
|
+
if @agent_class
|
|
87
|
+
@config = {
|
|
88
|
+
model: @agent_class.model,
|
|
89
|
+
temperature: @agent_class.temperature,
|
|
90
|
+
version: @agent_class.version,
|
|
91
|
+
timeout: @agent_class.timeout,
|
|
92
|
+
cache_enabled: @agent_class.cache_enabled?,
|
|
93
|
+
cache_ttl: @agent_class.cache_ttl,
|
|
94
|
+
params: @agent_class.params
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class ApplicationController < ActionController::Base
|
|
6
|
+
layout "rubyllm/agents/application"
|
|
7
|
+
|
|
8
|
+
before_action :authenticate_dashboard!
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def authenticate_dashboard!
|
|
13
|
+
auth_proc = RubyLLM::Agents.configuration.dashboard_auth
|
|
14
|
+
return if auth_proc.call(self)
|
|
15
|
+
|
|
16
|
+
render plain: "Unauthorized", status: :unauthorized
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class DashboardController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@stats = daily_stats
|
|
8
|
+
@recent_executions = Execution.recent(10)
|
|
9
|
+
@hourly_activity = Execution.hourly_activity_chart
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def daily_stats
|
|
15
|
+
scope = Execution.today
|
|
16
|
+
{
|
|
17
|
+
total_executions: scope.count,
|
|
18
|
+
successful: scope.successful.count,
|
|
19
|
+
failed: scope.failed.count,
|
|
20
|
+
total_cost: scope.total_cost_sum || 0,
|
|
21
|
+
total_tokens: scope.total_tokens_sum || 0,
|
|
22
|
+
avg_duration_ms: scope.avg_duration&.round || 0,
|
|
23
|
+
success_rate: calculate_success_rate(scope)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def calculate_success_rate(scope)
|
|
28
|
+
total = scope.count
|
|
29
|
+
return 0.0 if total.zero?
|
|
30
|
+
(scope.successful.count.to_f / total * 100).round(1)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
class ExecutionsController < ApplicationController
|
|
6
|
+
def index
|
|
7
|
+
@agent_types = Execution.distinct.pluck(:agent_type)
|
|
8
|
+
@statuses = Execution.statuses.keys
|
|
9
|
+
load_paginated_executions
|
|
10
|
+
load_filter_stats
|
|
11
|
+
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.html
|
|
14
|
+
format.turbo_stream
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
@execution = Execution.find(params[:id])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def search
|
|
23
|
+
@agent_types = Execution.distinct.pluck(:agent_type)
|
|
24
|
+
@statuses = Execution.statuses.keys
|
|
25
|
+
load_paginated_executions
|
|
26
|
+
load_filter_stats
|
|
27
|
+
|
|
28
|
+
respond_to do |format|
|
|
29
|
+
format.html { render :index }
|
|
30
|
+
format.turbo_stream do
|
|
31
|
+
render turbo_stream: turbo_stream.replace(
|
|
32
|
+
"executions_list",
|
|
33
|
+
partial: "ruby_llm/agents/executions/list",
|
|
34
|
+
locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def load_paginated_executions
|
|
43
|
+
page = (params[:page] || 1).to_i
|
|
44
|
+
per_page = 25
|
|
45
|
+
offset = (page - 1) * per_page
|
|
46
|
+
|
|
47
|
+
base_scope = filtered_executions.order(created_at: :desc)
|
|
48
|
+
total_count = base_scope.count
|
|
49
|
+
@executions = base_scope.limit(per_page).offset(offset)
|
|
50
|
+
|
|
51
|
+
@pagination = {
|
|
52
|
+
current_page: page,
|
|
53
|
+
per_page: per_page,
|
|
54
|
+
total_count: total_count,
|
|
55
|
+
total_pages: (total_count.to_f / per_page).ceil
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def load_filter_stats
|
|
60
|
+
scope = filtered_executions
|
|
61
|
+
@filter_stats = {
|
|
62
|
+
total_count: scope.count,
|
|
63
|
+
total_cost: scope.sum(:total_cost) || 0,
|
|
64
|
+
total_tokens: scope.sum(:total_tokens) || 0
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filtered_executions
|
|
69
|
+
scope = Execution.all
|
|
70
|
+
|
|
71
|
+
# Support multiple agent types (comma-separated or array)
|
|
72
|
+
if params[:agent_types].present?
|
|
73
|
+
agent_types = params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")
|
|
74
|
+
scope = scope.where(agent_type: agent_types) if agent_types.any?(&:present?)
|
|
75
|
+
elsif params[:agent_type].present?
|
|
76
|
+
scope = scope.by_agent(params[:agent_type])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Support multiple statuses (comma-separated or array)
|
|
80
|
+
if params[:statuses].present?
|
|
81
|
+
statuses = params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")
|
|
82
|
+
scope = scope.where(status: statuses) if statuses.any?(&:present?)
|
|
83
|
+
elsif params[:status].present?
|
|
84
|
+
scope = scope.where(status: params[:status])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
scope = scope.where("created_at >= ?", params[:days].to_i.days.ago) if params[:days].present?
|
|
88
|
+
|
|
89
|
+
scope
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Agents
|
|
5
|
+
module ApplicationHelper
|
|
6
|
+
include Chartkick::Helper if defined?(Chartkick)
|
|
7
|
+
|
|
8
|
+
def ruby_llm_agents
|
|
9
|
+
RubyLLM::Agents::Engine.routes.url_helpers
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def number_to_human_short(number, prefix: nil, precision: 1)
|
|
13
|
+
return "#{prefix}0" if number.nil? || number.zero?
|
|
14
|
+
|
|
15
|
+
abs_number = number.to_f.abs
|
|
16
|
+
formatted = if abs_number >= 1_000_000_000
|
|
17
|
+
"#{(number / 1_000_000_000.0).round(precision)}B"
|
|
18
|
+
elsif abs_number >= 1_000_000
|
|
19
|
+
"#{(number / 1_000_000.0).round(precision)}M"
|
|
20
|
+
elsif abs_number >= 1_000
|
|
21
|
+
"#{(number / 1_000.0).round(precision)}K"
|
|
22
|
+
elsif abs_number < 1 && abs_number > 0
|
|
23
|
+
number.round(precision + 3).to_s
|
|
24
|
+
else
|
|
25
|
+
number.round(precision).to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
"#{prefix}#{formatted}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def highlight_json(obj)
|
|
32
|
+
return "" if obj.nil?
|
|
33
|
+
|
|
34
|
+
json_string = JSON.pretty_generate(obj)
|
|
35
|
+
highlight_json_string(json_string)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def highlight_json_string(json_string)
|
|
39
|
+
return "" if json_string.blank?
|
|
40
|
+
|
|
41
|
+
tokens = []
|
|
42
|
+
i = 0
|
|
43
|
+
chars = json_string.chars
|
|
44
|
+
|
|
45
|
+
while i < chars.length
|
|
46
|
+
char = chars[i]
|
|
47
|
+
|
|
48
|
+
case char
|
|
49
|
+
when '"'
|
|
50
|
+
# Parse string
|
|
51
|
+
str_start = i
|
|
52
|
+
i += 1
|
|
53
|
+
while i < chars.length
|
|
54
|
+
if chars[i] == '\\'
|
|
55
|
+
i += 2
|
|
56
|
+
elsif chars[i] == '"'
|
|
57
|
+
i += 1
|
|
58
|
+
break
|
|
59
|
+
else
|
|
60
|
+
i += 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
tokens << { type: :string, value: chars[str_start...i].join }
|
|
64
|
+
when /[0-9\-]/
|
|
65
|
+
# Parse number
|
|
66
|
+
num_start = i
|
|
67
|
+
i += 1
|
|
68
|
+
while i < chars.length && chars[i] =~ /[0-9.eE+\-]/
|
|
69
|
+
i += 1
|
|
70
|
+
end
|
|
71
|
+
tokens << { type: :number, value: chars[num_start...i].join }
|
|
72
|
+
when 't'
|
|
73
|
+
# true
|
|
74
|
+
if chars[i, 4].join == 'true'
|
|
75
|
+
tokens << { type: :boolean, value: 'true' }
|
|
76
|
+
i += 4
|
|
77
|
+
else
|
|
78
|
+
tokens << { type: :text, value: char }
|
|
79
|
+
i += 1
|
|
80
|
+
end
|
|
81
|
+
when 'f'
|
|
82
|
+
# false
|
|
83
|
+
if chars[i, 5].join == 'false'
|
|
84
|
+
tokens << { type: :boolean, value: 'false' }
|
|
85
|
+
i += 5
|
|
86
|
+
else
|
|
87
|
+
tokens << { type: :text, value: char }
|
|
88
|
+
i += 1
|
|
89
|
+
end
|
|
90
|
+
when 'n'
|
|
91
|
+
# null
|
|
92
|
+
if chars[i, 4].join == 'null'
|
|
93
|
+
tokens << { type: :null, value: 'null' }
|
|
94
|
+
i += 4
|
|
95
|
+
else
|
|
96
|
+
tokens << { type: :text, value: char }
|
|
97
|
+
i += 1
|
|
98
|
+
end
|
|
99
|
+
when ':', ',', '{', '}', '[', ']', ' ', "\n", "\t"
|
|
100
|
+
tokens << { type: :punct, value: char }
|
|
101
|
+
i += 1
|
|
102
|
+
else
|
|
103
|
+
tokens << { type: :text, value: char }
|
|
104
|
+
i += 1
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Build highlighted HTML - detect if string is a key (followed by :)
|
|
109
|
+
result = []
|
|
110
|
+
tokens.each_with_index do |token, idx|
|
|
111
|
+
case token[:type]
|
|
112
|
+
when :string
|
|
113
|
+
# Check if this is a key (next non-whitespace token is :)
|
|
114
|
+
is_key = false
|
|
115
|
+
(idx + 1...tokens.length).each do |j|
|
|
116
|
+
if tokens[j][:type] == :punct
|
|
117
|
+
if tokens[j][:value] == ':'
|
|
118
|
+
is_key = true
|
|
119
|
+
break
|
|
120
|
+
elsif tokens[j][:value] !~ /\s/
|
|
121
|
+
break
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
break
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
escaped_value = ERB::Util.html_escape(token[:value])
|
|
129
|
+
if is_key
|
|
130
|
+
result << %(<span class="text-purple-600">#{escaped_value}</span>)
|
|
131
|
+
else
|
|
132
|
+
result << %(<span class="text-green-600">#{escaped_value}</span>)
|
|
133
|
+
end
|
|
134
|
+
when :number
|
|
135
|
+
result << %(<span class="text-blue-600">#{token[:value]}</span>)
|
|
136
|
+
when :boolean
|
|
137
|
+
result << %(<span class="text-amber-600">#{token[:value]}</span>)
|
|
138
|
+
when :null
|
|
139
|
+
result << %(<span class="text-gray-400">#{token[:value]}</span>)
|
|
140
|
+
else
|
|
141
|
+
result << ERB::Util.html_escape(token[:value])
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
result.join.html_safe
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Filter controller for live filtering of executions
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// <form data-controller="filter">
|
|
7
|
+
// <select data-action="change->filter#submit">...</select>
|
|
8
|
+
// </form>
|
|
9
|
+
//
|
|
10
|
+
export default class extends Controller {
|
|
11
|
+
static targets = ["form"]
|
|
12
|
+
static values = {
|
|
13
|
+
debounce: { type: Number, default: 300 }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
connect() {
|
|
17
|
+
this.timeout = null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
disconnect() {
|
|
21
|
+
if (this.timeout) {
|
|
22
|
+
clearTimeout(this.timeout)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Submit the form with debouncing
|
|
27
|
+
submit(event) {
|
|
28
|
+
if (this.timeout) {
|
|
29
|
+
clearTimeout(this.timeout)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.timeout = setTimeout(() => {
|
|
33
|
+
this.element.requestSubmit()
|
|
34
|
+
}, this.debounceValue)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Submit immediately without debounce
|
|
38
|
+
submitNow(event) {
|
|
39
|
+
this.element.requestSubmit()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update URL with current filter state
|
|
43
|
+
updateUrl() {
|
|
44
|
+
const formData = new FormData(this.element)
|
|
45
|
+
const params = new URLSearchParams()
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of formData) {
|
|
48
|
+
if (value) {
|
|
49
|
+
params.set(key, value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const newUrl = `${window.location.pathname}?${params.toString()}`
|
|
54
|
+
window.history.pushState({}, "", newUrl)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Entry point for RubyLLM::Agents Stimulus controllers
|
|
2
|
+
// Import and register controllers with your Stimulus application
|
|
3
|
+
|
|
4
|
+
import FilterController from "./filter_controller"
|
|
5
|
+
import RefreshController from "./refresh_controller"
|
|
6
|
+
|
|
7
|
+
export { FilterController, RefreshController }
|
|
8
|
+
|
|
9
|
+
export function registerControllers(application) {
|
|
10
|
+
application.register("filter", FilterController)
|
|
11
|
+
application.register("refresh", RefreshController)
|
|
12
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Auto-refresh controller for dashboard with toggle support
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// <div data-controller="refresh"
|
|
7
|
+
// data-refresh-interval-value="30000"
|
|
8
|
+
// data-refresh-enabled-value="false">
|
|
9
|
+
// <button data-action="refresh#toggle" data-refresh-target="button">
|
|
10
|
+
// Live Poll: Off
|
|
11
|
+
// </button>
|
|
12
|
+
// </div>
|
|
13
|
+
//
|
|
14
|
+
export default class extends Controller {
|
|
15
|
+
static values = {
|
|
16
|
+
interval: { type: Number, default: 30000 },
|
|
17
|
+
enabled: { type: Boolean, default: false }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
static targets = ["button", "indicator"]
|
|
21
|
+
|
|
22
|
+
connect() {
|
|
23
|
+
this.updateUI()
|
|
24
|
+
if (this.enabledValue) {
|
|
25
|
+
this.startRefresh()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
disconnect() {
|
|
30
|
+
this.stopRefresh()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toggle() {
|
|
34
|
+
this.enabledValue = !this.enabledValue
|
|
35
|
+
|
|
36
|
+
if (this.enabledValue) {
|
|
37
|
+
this.startRefresh()
|
|
38
|
+
this.refresh() // Immediate refresh when enabled
|
|
39
|
+
} else {
|
|
40
|
+
this.stopRefresh()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.updateUI()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
startRefresh() {
|
|
47
|
+
if (this.intervalValue > 0 && !this.timer) {
|
|
48
|
+
this.timer = setInterval(() => this.refresh(), this.intervalValue)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
stopRefresh() {
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearInterval(this.timer)
|
|
55
|
+
this.timer = null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
refresh() {
|
|
60
|
+
const frame = this.element.closest("turbo-frame") ||
|
|
61
|
+
this.element.querySelector("turbo-frame")
|
|
62
|
+
|
|
63
|
+
if (frame && typeof frame.reload === "function") {
|
|
64
|
+
frame.reload()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updateUI() {
|
|
69
|
+
if (this.hasButtonTarget) {
|
|
70
|
+
if (this.enabledValue) {
|
|
71
|
+
this.buttonTarget.classList.remove("bg-gray-100", "text-gray-600")
|
|
72
|
+
this.buttonTarget.classList.add("bg-green-100", "text-green-700")
|
|
73
|
+
} else {
|
|
74
|
+
this.buttonTarget.classList.remove("bg-green-100", "text-green-700")
|
|
75
|
+
this.buttonTarget.classList.add("bg-gray-100", "text-gray-600")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.hasIndicatorTarget) {
|
|
80
|
+
this.indicatorTarget.textContent = this.enabledValue ? "On" : "Off"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|