protege 0.1.0.alpha.1
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/README.md +24 -0
- data/Rakefile +12 -0
- data/app/assets/builds/protege.css +1128 -0
- data/app/assets/javascripts/protege/attachment_picker.js +163 -0
- data/app/assets/javascripts/protege/copy.js +80 -0
- data/app/assets/javascripts/protege/forms.js +28 -0
- data/app/assets/javascripts/protege/keys.js +212 -0
- data/app/assets/javascripts/protege/resize.js +149 -0
- data/app/assets/javascripts/protege/theme.js +25 -0
- data/app/assets/stylesheets/protege/application.css +15 -0
- data/app/assets/tailwind/application.css +137 -0
- data/app/controllers/concerns/protege/persona_scoped.rb +22 -0
- data/app/controllers/concerns/protege/thread_scoped.rb +22 -0
- data/app/controllers/protege/access_rules_controller.rb +72 -0
- data/app/controllers/protege/application_controller.rb +18 -0
- data/app/controllers/protege/archives_controller.rb +52 -0
- data/app/controllers/protege/email_domains_controller.rb +72 -0
- data/app/controllers/protege/home_controller.rb +15 -0
- data/app/controllers/protege/messages_controller.rb +46 -0
- data/app/controllers/protege/mta_domains_syncs_controller.rb +22 -0
- data/app/controllers/protege/personas_controller.rb +118 -0
- data/app/controllers/protege/replies_controller.rb +64 -0
- data/app/controllers/protege/responsibilities_controller.rb +82 -0
- data/app/controllers/protege/responsibility_runs_controller.rb +46 -0
- data/app/controllers/protege/threads_controller.rb +75 -0
- data/app/controllers/protege/traces/searches_controller.rb +19 -0
- data/app/controllers/protege/traces_controller.rb +52 -0
- data/app/helpers/protege/application_helper.rb +30 -0
- data/app/helpers/protege/components/badges_helper.rb +59 -0
- data/app/helpers/protege/components/buttons_helper.rb +141 -0
- data/app/helpers/protege/components/cards_helper.rb +48 -0
- data/app/helpers/protege/components/components_helper.rb +30 -0
- data/app/helpers/protege/components/dialogs_helper.rb +36 -0
- data/app/helpers/protege/components/forms_helper.rb +223 -0
- data/app/helpers/protege/components/hotkeys_helper.rb +60 -0
- data/app/helpers/protege/components/icons_helper.rb +41 -0
- data/app/helpers/protege/components/introspection_helper.rb +162 -0
- data/app/helpers/protege/components/layout_helper.rb +169 -0
- data/app/helpers/protege/components/menus_helper.rb +68 -0
- data/app/helpers/protege/components/pagination_helper.rb +100 -0
- data/app/helpers/protege/components/sidebar_helper.rb +211 -0
- data/app/helpers/protege/components/tables_helper.rb +59 -0
- data/app/helpers/protege/components/typography_helper.rb +64 -0
- data/app/helpers/protege/email_domains_helper.rb +22 -0
- data/app/helpers/protege/messages_helper.rb +152 -0
- data/app/helpers/protege/personas_helper.rb +90 -0
- data/app/helpers/protege/responsibilities_helper.rb +63 -0
- data/app/helpers/protege/threads_helper.rb +118 -0
- data/app/helpers/protege/trace_helper.rb +22 -0
- data/app/hooks/protege/event_logger_hook.rb +109 -0
- data/app/jobs/protege/application_job.rb +41 -0
- data/app/jobs/protege/console_inference_job.rb +57 -0
- data/app/jobs/protege/inference_job.rb +60 -0
- data/app/jobs/protege/mail_provisioning_job.rb +21 -0
- data/app/jobs/protege/responsibility_job.rb +70 -0
- data/app/jobs/protege/responsibility_tick_job.rb +19 -0
- data/app/mailboxes/protege/agent_mailbox.rb +123 -0
- data/app/mailers/protege/application_mailer.rb +65 -0
- data/app/models/concerns/protege/broadcastable_email_domain.rb +48 -0
- data/app/models/concerns/protege/broadcastable_message.rb +47 -0
- data/app/models/concerns/protege/broadcastable_persona.rb +50 -0
- data/app/models/concerns/protege/broadcastable_responsibility.rb +51 -0
- data/app/models/concerns/protege/broadcastable_thread.rb +55 -0
- data/app/models/concerns/protege/tool_scoped.rb +88 -0
- data/app/models/protege/access_rule.rb +50 -0
- data/app/models/protege/application_record.rb +18 -0
- data/app/models/protege/current.rb +23 -0
- data/app/models/protege/email_domain.rb +136 -0
- data/app/models/protege/email_thread.rb +81 -0
- data/app/models/protege/message.rb +401 -0
- data/app/models/protege/persona.rb +229 -0
- data/app/models/protege/responsibility.rb +98 -0
- data/app/models/protege/responsibility_run.rb +68 -0
- data/app/models/protege/tool_use.rb +115 -0
- data/app/models/protege/trace.rb +71 -0
- data/app/providers/protege/open_router_provider.rb +317 -0
- data/app/resolvers/README.md +255 -0
- data/app/resolvers/protege/load_file_resolver.rb +86 -0
- data/app/resolvers/protege/load_record_resolver.rb +80 -0
- data/app/resolvers/protege/load_text_resolver.rb +40 -0
- data/app/resolvers/protege/thread_history_resolver.rb +116 -0
- data/app/services/protege/introspection_tool_call_presentation.rb +134 -0
- data/app/services/protege/message_search.rb +114 -0
- data/app/services/protege/trace_search.rb +102 -0
- data/app/subscribers/protege/introspection_broadcaster.rb +324 -0
- data/app/tools/protege/create_file_tool.rb +109 -0
- data/app/tools/protege/read_attachment_tool.rb +101 -0
- data/app/tools/protege/search_emails_tool.rb +129 -0
- data/app/tools/protege/send_email_tool.rb +240 -0
- data/app/tools/protege/web_fetch_tool.rb +224 -0
- data/app/tools/protege/web_search_tool.rb +131 -0
- data/app/views/layouts/protege/application.html.slim +19 -0
- data/app/views/protege/access_rules/create.turbo_stream.slim +6 -0
- data/app/views/protege/email_domains/_email_domain_sidebar_item.html.slim +4 -0
- data/app/views/protege/email_domains/_form.html.slim +10 -0
- data/app/views/protege/email_domains/_sidebar.html.slim +11 -0
- data/app/views/protege/email_domains/index.html.slim +6 -0
- data/app/views/protege/email_domains/new.html.slim +7 -0
- data/app/views/protege/email_domains/show.html.slim +36 -0
- data/app/views/protege/home/show.html.slim +26 -0
- data/app/views/protege/messages/_message.html.slim +19 -0
- data/app/views/protege/messages/_sidebar.html.slim +13 -0
- data/app/views/protege/messages/index.html.slim +20 -0
- data/app/views/protege/personas/_access_rule.html.slim +7 -0
- data/app/views/protege/personas/_access_rule_form.html.slim +8 -0
- data/app/views/protege/personas/_access_rules.html.slim +19 -0
- data/app/views/protege/personas/_actions.html.slim +15 -0
- data/app/views/protege/personas/_form.html.slim +29 -0
- data/app/views/protege/personas/_persona_sidebar_item.html.slim +5 -0
- data/app/views/protege/personas/_sidebar.html.slim +11 -0
- data/app/views/protege/personas/edit.html.slim +7 -0
- data/app/views/protege/personas/index.html.slim +6 -0
- data/app/views/protege/personas/new.html.slim +7 -0
- data/app/views/protege/personas/show.html.slim +37 -0
- data/app/views/protege/responsibilities/_form.html.slim +21 -0
- data/app/views/protege/responsibilities/_responsibility_sidebar_item.html.slim +5 -0
- data/app/views/protege/responsibilities/_sidebar.html.slim +11 -0
- data/app/views/protege/responsibilities/edit.html.slim +7 -0
- data/app/views/protege/responsibilities/index.html.slim +6 -0
- data/app/views/protege/responsibilities/new.html.slim +7 -0
- data/app/views/protege/responsibilities/show.html.slim +22 -0
- data/app/views/protege/responsibility_runs/show.html.slim +13 -0
- data/app/views/protege/shared/_confirm_dialog.html.slim +22 -0
- data/app/views/protege/shared/_header.html.slim +14 -0
- data/app/views/protege/shared/_hotkeys.html.slim +43 -0
- data/app/views/protege/threads/_composer_footer.html.slim +15 -0
- data/app/views/protege/threads/_introspection.html.slim +7 -0
- data/app/views/protege/threads/_reasoning.html.slim +4 -0
- data/app/views/protege/threads/_reply_form.html.slim +7 -0
- data/app/views/protege/threads/_run_marker.html.slim +3 -0
- data/app/views/protege/threads/_sidebar.html.slim +15 -0
- data/app/views/protege/threads/_thread.html.slim +6 -0
- data/app/views/protege/threads/_tool_call.html.slim +8 -0
- data/app/views/protege/threads/index.html.slim +6 -0
- data/app/views/protege/threads/new.html.slim +15 -0
- data/app/views/protege/threads/show.html.slim +25 -0
- data/app/views/protege/traces/_sidebar.html.slim +15 -0
- data/app/views/protege/traces/index.html.slim +11 -0
- data/app/views/protege/traces/searches/_sidebar.html.slim +13 -0
- data/app/views/protege/traces/searches/show.html.slim +22 -0
- data/app/views/protege/traces/show.html.slim +28 -0
- data/config/routes.rb +33 -0
- data/db/migrate/20260520000001_create_protege_tables.rb +105 -0
- data/db/migrate/20260706120130_create_protege_traces.rb +30 -0
- data/db/migrate/20260707120000_replace_persona_active_with_archived_at.rb +24 -0
- data/db/migrate/20260708120000_add_disabled_tool_ids_to_personas.rb +22 -0
- data/lib/generators/protege/extension_naming.rb +38 -0
- data/lib/generators/protege/hook/hook_generator.rb +33 -0
- data/lib/generators/protege/hook/templates/hook.rb.tt +36 -0
- data/lib/generators/protege/install/install_generator.rb +170 -0
- data/lib/generators/protege/install/templates/initializer.rb.tt +84 -0
- data/lib/generators/protege/persona/persona_generator.rb +35 -0
- data/lib/generators/protege/persona/templates/persona.rb.tt +31 -0
- data/lib/generators/protege/postfix/postfix_generator.rb +74 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/Dockerfile +43 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/KeyTable +3 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/MAIL.md +135 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/README.md +56 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/SigningTable +3 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/TrustedHosts +8 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/entrypoint.sh +118 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/kamal-accessory.example.yml +32 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/main.cf +68 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendkim.conf +36 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendmarc.conf +32 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/opendmarc.ignore.hosts +9 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/policyd-spf.conf +21 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/provision.py +182 -0
- data/lib/generators/protege/postfix/templates/deploy/mail/relay-to-app.sh +28 -0
- data/lib/generators/protege/postfix/templates/github/workflows/build-mail-image.example.yml +50 -0
- data/lib/generators/protege/provider/provider_generator.rb +44 -0
- data/lib/generators/protege/provider/templates/provider.rb.tt +50 -0
- data/lib/generators/protege/resolver/resolver_generator.rb +34 -0
- data/lib/generators/protege/resolver/templates/resolver.rb.tt +31 -0
- data/lib/generators/protege/tool/templates/tool.rb.tt +50 -0
- data/lib/generators/protege/tool/tool_generator.rb +34 -0
- data/lib/protege/configuration.rb +171 -0
- data/lib/protege/engine.rb +103 -0
- data/lib/protege/errors/bad_request_error.rb +8 -0
- data/lib/protege/errors/contract_violation_error.rb +8 -0
- data/lib/protege/errors/error.rb +30 -0
- data/lib/protege/errors/generic_error.rb +7 -0
- data/lib/protege/errors/internal_error.rb +8 -0
- data/lib/protege/errors/invalid_model_error.rb +8 -0
- data/lib/protege/errors/invalid_tool_result_error.rb +9 -0
- data/lib/protege/errors/rate_limited_error.rb +8 -0
- data/lib/protege/errors/resolver_file_not_found_error.rb +9 -0
- data/lib/protege/errors/response_parse_failed_error.rb +8 -0
- data/lib/protege/errors/timeout_error.rb +8 -0
- data/lib/protege/errors/tool_not_available_error.rb +12 -0
- data/lib/protege/errors/tool_not_found_error.rb +9 -0
- data/lib/protege/errors/unauthorized_error.rb +8 -0
- data/lib/protege/errors/unavailable_error.rb +8 -0
- data/lib/protege/errors/unsupported_capability_error.rb +8 -0
- data/lib/protege/events/event.rb +98 -0
- data/lib/protege/events/inference_chunk_event.rb +17 -0
- data/lib/protege/events/inference_completed_event.rb +12 -0
- data/lib/protege/events/inference_failed_event.rb +12 -0
- data/lib/protege/events/inference_generated_event.rb +19 -0
- data/lib/protege/events/inference_max_turns_reached_event.rb +12 -0
- data/lib/protege/events/inference_started_event.rb +11 -0
- data/lib/protege/events/loop_run_completed_event.rb +11 -0
- data/lib/protege/events/loop_run_enqueued_event.rb +12 -0
- data/lib/protege/events/loop_run_failed_event.rb +12 -0
- data/lib/protege/events/loop_run_started_event.rb +11 -0
- data/lib/protege/events/tool_call_completed_event.rb +12 -0
- data/lib/protege/events/tool_call_failed_event.rb +12 -0
- data/lib/protege/events/tool_call_started_event.rb +11 -0
- data/lib/protege/events/tool_calls_received_event.rb +11 -0
- data/lib/protege/extensions/hook.rb +13 -0
- data/lib/protege/extensions/hook_mixin.rb +90 -0
- data/lib/protege/extensions/provider.rb +12 -0
- data/lib/protege/extensions/provider_mixin.rb +124 -0
- data/lib/protege/extensions/resolver.rb +12 -0
- data/lib/protege/extensions/resolver_mixin.rb +58 -0
- data/lib/protege/extensions/tool.rb +12 -0
- data/lib/protege/extensions/tool_mixin.rb +217 -0
- data/lib/protege/gateway/access_control.rb +66 -0
- data/lib/protege/gateway/access_policy.rb +94 -0
- data/lib/protege/gateway/attachment_policy.rb +115 -0
- data/lib/protege/gateway/mail/address.rb +145 -0
- data/lib/protege/gateway/mail/attachment.rb +17 -0
- data/lib/protege/gateway/mail/message_id.rb +104 -0
- data/lib/protege/gateway/mail/outbound.rb +181 -0
- data/lib/protege/gateway/mail/references.rb +107 -0
- data/lib/protege/gateway.rb +398 -0
- data/lib/protege/inference/provider/request.rb +110 -0
- data/lib/protege/inference/provider/response.rb +50 -0
- data/lib/protege/inference/provider/tool_call.rb +52 -0
- data/lib/protege/inference.rb +121 -0
- data/lib/protege/loop/schedule.rb +66 -0
- data/lib/protege/loop/scheduler.rb +42 -0
- data/lib/protege/loop.rb +37 -0
- data/lib/protege/orchestrator/context.rb +65 -0
- data/lib/protege/orchestrator/harness.rb +305 -0
- data/lib/protege/orchestrator/reply_context.rb +21 -0
- data/lib/protege/orchestrator/reply_harness.rb +68 -0
- data/lib/protege/orchestrator/resolver_chain.rb +93 -0
- data/lib/protege/orchestrator/responsibility_context.rb +22 -0
- data/lib/protege/orchestrator/responsibility_harness.rb +60 -0
- data/lib/protege/orchestrator.rb +37 -0
- data/lib/protege/subscribers/hook_dispatcher.rb +140 -0
- data/lib/protege/subscribers/tracing.rb +65 -0
- data/lib/protege/types/document_part.rb +13 -0
- data/lib/protege/types/image_part.rb +11 -0
- data/lib/protege/types/model_message.rb +40 -0
- data/lib/protege/types/result.rb +113 -0
- data/lib/protege/types/stored_file.rb +127 -0
- data/lib/protege/types/text_part.rb +8 -0
- data/lib/protege/types/tool_result.rb +16 -0
- data/lib/protege/version.rb +6 -0
- data/lib/protege.rb +47 -0
- data/lib/tasks/protege_tasks.rake +16 -0
- metadata +494 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Manages scheduled responsibilities — the Loop layer's dashboard CRUD. A top-level section (like
|
|
5
|
+
# Personas or Traces): responsibilities are listed across all personas, and each one names its owning
|
|
6
|
+
# persona in the form rather than carrying it in the URL.
|
|
7
|
+
class ResponsibilitiesController < ApplicationController
|
|
8
|
+
before_action :set_responsibility, only: %i[show edit update destroy]
|
|
9
|
+
|
|
10
|
+
# List every responsibility, newest-owner-agnostic (ordered by name via the model's default scope).
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
def index
|
|
14
|
+
@responsibilities = Responsibility.includes(:persona)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Show a single responsibility and its recent runs.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
def show; end
|
|
21
|
+
|
|
22
|
+
# Render the form for creating a responsibility.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def new
|
|
26
|
+
@responsibility = Responsibility.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Render the form for editing a responsibility.
|
|
30
|
+
#
|
|
31
|
+
# @return [void]
|
|
32
|
+
def edit; end
|
|
33
|
+
|
|
34
|
+
# Create a responsibility (its persona is chosen in the form). On failure, re-render with errors.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def create
|
|
38
|
+
@responsibility = Responsibility.new(responsibility_params)
|
|
39
|
+
|
|
40
|
+
if @responsibility.save
|
|
41
|
+
redirect_to responsibility_path(@responsibility), notice: 'Responsibility created.'
|
|
42
|
+
else
|
|
43
|
+
render :new, status: :unprocessable_entity
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Update a responsibility. On failure, re-render the edit form with validation errors.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
def update
|
|
51
|
+
if @responsibility.update(responsibility_params)
|
|
52
|
+
redirect_to responsibility_path(@responsibility), notice: 'Responsibility updated.'
|
|
53
|
+
else
|
|
54
|
+
render :edit, status: :unprocessable_entity
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Remove a responsibility.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
def destroy
|
|
62
|
+
@responsibility.destroy
|
|
63
|
+
redirect_to responsibilities_path, notice: 'Responsibility deleted.'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Load the responsibility named by the route into +@responsibility+ (its persona via the association).
|
|
69
|
+
#
|
|
70
|
+
# @return [Protege::Responsibility] the loaded responsibility
|
|
71
|
+
def set_responsibility
|
|
72
|
+
@responsibility = Responsibility.find(params[:id])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Permit the editable responsibility attributes, including the +:persona_id+ the form now supplies.
|
|
76
|
+
#
|
|
77
|
+
# @return [ActionController::Parameters] the permitted attributes
|
|
78
|
+
def responsibility_params
|
|
79
|
+
params.require(:responsibility).permit(:persona_id, :name, :instructions, :schedule, :active)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Surfaces individual runs of a responsibility — the Loop layer's run records. Creating a run is how
|
|
5
|
+
# the dashboard's "Run now" works: dispatching a responsibility immediately, modelled RESTfully as
|
|
6
|
+
# POSTing a new run rather than a custom action.
|
|
7
|
+
class ResponsibilityRunsController < ApplicationController
|
|
8
|
+
before_action :set_responsibility_run, only: [:show]
|
|
9
|
+
|
|
10
|
+
# Dispatch a responsibility now, recording a pending run and enqueuing its job. Serves
|
|
11
|
+
# +POST /responsibility_runs+.
|
|
12
|
+
#
|
|
13
|
+
# @return [void]
|
|
14
|
+
def create
|
|
15
|
+
responsibility = Responsibility.find(run_params[:responsibility_id])
|
|
16
|
+
|
|
17
|
+
@run = responsibility.dispatch!
|
|
18
|
+
|
|
19
|
+
redirect_to responsibility_run_path(@run), notice: 'Run enqueued.'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Show one run's outcome — status, timing, and any failure detail. Serves
|
|
23
|
+
# +GET /responsibility_runs/:id+.
|
|
24
|
+
#
|
|
25
|
+
# @return [void]
|
|
26
|
+
def show
|
|
27
|
+
@responsibility = @run.responsibility
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# Load the run record
|
|
33
|
+
#
|
|
34
|
+
# @return [Protege::ResponsibilityRun] the run to show
|
|
35
|
+
def set_responsibility_run
|
|
36
|
+
@run = ResponsibilityRun.find(params[:id])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Permit the responsibility a "Run now" targets.
|
|
40
|
+
#
|
|
41
|
+
# @return [ActionController::Parameters] the permitted +:responsibility_id+
|
|
42
|
+
def run_params
|
|
43
|
+
params.require(:responsibility_run).permit(:responsibility_id)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Manages email threads — the inbox list, individual thread views,
|
|
5
|
+
# and composing new conversations from the dashboard console.
|
|
6
|
+
class ThreadsController < ApplicationController
|
|
7
|
+
before_action :set_thread, only: %i[show destroy]
|
|
8
|
+
|
|
9
|
+
# List the inbox threads. Serves +GET /threads+.
|
|
10
|
+
#
|
|
11
|
+
# @return [void]
|
|
12
|
+
def index
|
|
13
|
+
@threads = EmailThread.for_inbox
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Show a thread with its messages newest-first. Serves +GET /threads/:id+.
|
|
17
|
+
#
|
|
18
|
+
# Liveness (the agent's reasoning and tool use) streams into the introspection panel, so the thread
|
|
19
|
+
# view itself is static — no per-message processing indicator to compute here.
|
|
20
|
+
#
|
|
21
|
+
# @return [void]
|
|
22
|
+
def show
|
|
23
|
+
@messages = @thread.messages.order(sent_at: :desc)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Render the compose form for a new conversation. Serves +GET /threads/new+.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
def new; end
|
|
30
|
+
|
|
31
|
+
# Compose and dispatch a new conversation through the +Gateway+. Serves +POST /threads+.
|
|
32
|
+
#
|
|
33
|
+
# Uses the same host-facing seam application code would — +Gateway.message+ — passing any uploaded
|
|
34
|
+
# files straight through as attachments. Redirects to the thread created for the resulting message.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
def create
|
|
38
|
+
persona = Persona.find(compose_params[:persona_id])
|
|
39
|
+
|
|
40
|
+
message = Gateway.message(
|
|
41
|
+
to: persona.email_address,
|
|
42
|
+
subject: compose_params[:subject],
|
|
43
|
+
body: compose_params[:body],
|
|
44
|
+
attachments: compose_params[:files] || []
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
redirect_to thread_path(message.email_thread)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Delete a thread and all its messages. Serves +DELETE /threads/:id+.
|
|
51
|
+
#
|
|
52
|
+
# @return [void]
|
|
53
|
+
def destroy
|
|
54
|
+
@thread.destroy!
|
|
55
|
+
|
|
56
|
+
redirect_to threads_path
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Load the +EmailThread+ named by +params[:id]+ into +@thread+.
|
|
62
|
+
#
|
|
63
|
+
# @return [Protege::EmailThread] the loaded thread
|
|
64
|
+
def set_thread
|
|
65
|
+
@thread = EmailThread.find(params[:id])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Permit the compose-form attributes for a new conversation.
|
|
69
|
+
#
|
|
70
|
+
# @return [ActionController::Parameters] the permitted +:persona_id+, +:subject+, +:body+, +:files+
|
|
71
|
+
def compose_params
|
|
72
|
+
params.require(:thread).permit(:persona_id, :subject, :body, files: [])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Traces
|
|
5
|
+
# Trace search — the console's cross-run finder over captured turns (see +TraceSearch+), reached from
|
|
6
|
+
# the traces sidebar's search link. Nested in the +Traces+ module because search is a view *of*
|
|
7
|
+
# traces, not a child record a trace owns (unlike a thread, which genuinely has messages).
|
|
8
|
+
class SearchesController < Protege::ApplicationController
|
|
9
|
+
# Show the search form and, in the sidebar, the traces matching the query params (see
|
|
10
|
+
# +TraceSearch+). Blank params yield no results. Serves +GET /traces/search+.
|
|
11
|
+
#
|
|
12
|
+
# @return [void]
|
|
13
|
+
def show
|
|
14
|
+
@search = TraceSearch.from_params(params)
|
|
15
|
+
@traces = @search.results
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# Traces — the review surface for captured inference turns (see +Protege::Trace+). +index+ lists the
|
|
5
|
+
# turns (newest first, optionally narrowed to the unlabeled review queue) in the sidebar; +show+
|
|
6
|
+
# renders one turn's request/response/settings snapshot; +update+ applies a human review label so the
|
|
7
|
+
# turn becomes usable training data. Read-and-label only — traces are written by the tracing
|
|
8
|
+
# subscriber, never edited here beyond the verdict.
|
|
9
|
+
class TracesController < ApplicationController
|
|
10
|
+
before_action :set_trace, only: %i[show update]
|
|
11
|
+
|
|
12
|
+
# List captured turns, newest first, for the sidebar (search lives on its own page — see
|
|
13
|
+
# +Traces::SearchesController+). Serves +GET /traces+.
|
|
14
|
+
#
|
|
15
|
+
# @return [void]
|
|
16
|
+
def index
|
|
17
|
+
@traces = Trace.recent
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Show one turn's snapshot and its review form. The sidebar sources its own list via the
|
|
21
|
+
# +recent_traces+ helper (like threads' +inbox_threads+), so nothing to load here. Serves
|
|
22
|
+
# +GET /traces/:id+.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def show; end
|
|
26
|
+
|
|
27
|
+
# Apply a review verdict (label + optional note) to a turn. Serves +PATCH /traces/:id+.
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
def update
|
|
31
|
+
@trace.apply_label(label: label_params[:label], annotation: label_params[:annotation])
|
|
32
|
+
|
|
33
|
+
redirect_to trace_path(@trace), notice: 'Trace labeled.'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Load the trace named by +params[:id]+.
|
|
39
|
+
#
|
|
40
|
+
# @return [Protege::Trace] the loaded trace
|
|
41
|
+
def set_trace
|
|
42
|
+
@trace = Trace.find(params[:id])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Permit the review verdict fields.
|
|
46
|
+
#
|
|
47
|
+
# @return [ActionController::Parameters] the permitted +:label+/+:annotation+
|
|
48
|
+
def label_params
|
|
49
|
+
params.require(:trace).permit(:label, :annotation)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
# The single aggregator of every engine helper module. The engine registers only this module on the
|
|
5
|
+
# view context (+helper Protege::ApplicationHelper+ in ApplicationController), so including every
|
|
6
|
+
# helper here is what makes their methods available in all render contexts — request views, partials,
|
|
7
|
+
# and Turbo broadcast renders alike. Two groups: the reusable, model-agnostic UI toolkit under
|
|
8
|
+
# +Protege::Components::+ — pulled in as a whole via its own aggregator, +Components::ComponentsHelper+,
|
|
9
|
+
# which includes the rest of the group (typography, tables, badges, buttons, cards, dialogs, menus,
|
|
10
|
+
# forms, layout, sidebar, pagination, the introspection panel) — and the model-named helpers at the
|
|
11
|
+
# top level (threads, personas, email
|
|
12
|
+
# domains, messages, responsibilities, traces). Any cross-helper call resolves through the view, which
|
|
13
|
+
# has them all via this module.
|
|
14
|
+
module ApplicationHelper
|
|
15
|
+
include Components::ComponentsHelper
|
|
16
|
+
include EmailDomainsHelper
|
|
17
|
+
include MessagesHelper
|
|
18
|
+
include PersonasHelper
|
|
19
|
+
include ResponsibilitiesHelper
|
|
20
|
+
include ThreadsHelper
|
|
21
|
+
include TraceHelper
|
|
22
|
+
|
|
23
|
+
# List of JS files to be added to the console layout
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<string>] javascript asset file paths.
|
|
26
|
+
def js_files
|
|
27
|
+
%w[protege/theme protege/resize protege/forms protege/copy protege/keys protege/attachment_picker]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Components
|
|
5
|
+
# Compact status indicators: the pill +badge+ and the labelled +status_dot+. Both take a semantic
|
|
6
|
+
# color and resolve it through the +badge_colors+ / +dot_colors+ maps defined alongside them, so the
|
|
7
|
+
# palette lives with the components that use it.
|
|
8
|
+
module BadgesHelper
|
|
9
|
+
# Render a small pill badge tinted by semantic color.
|
|
10
|
+
#
|
|
11
|
+
# @param text [String] the badge label
|
|
12
|
+
# @param color [Symbol] the semantic color (see +badge_colors+)
|
|
13
|
+
# @return [ActiveSupport::SafeBuffer] the badge markup
|
|
14
|
+
def badge(text, color = :gray)
|
|
15
|
+
_, fg = badge_colors(color)
|
|
16
|
+
tag.span(text, class: "inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium #{fg}",
|
|
17
|
+
style: 'background: var(--surface-subtle); border: 1px solid var(--border)')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Map a semantic badge color to its +[background, foreground]+ classes.
|
|
21
|
+
#
|
|
22
|
+
# @param color [Symbol] the semantic color
|
|
23
|
+
# @return [Array(nil, String)] the background and foreground class pair
|
|
24
|
+
def badge_colors(color)
|
|
25
|
+
case color
|
|
26
|
+
when :green then [nil, 'text-green-600']
|
|
27
|
+
when :red then [nil, 'text-red-600']
|
|
28
|
+
when :yellow then [nil, 'text-yellow-600']
|
|
29
|
+
else [nil, '']
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Render a colored status dot followed by its label.
|
|
34
|
+
#
|
|
35
|
+
# @param text [String] the status label
|
|
36
|
+
# @param color [Symbol] the semantic color (see +dot_colors+)
|
|
37
|
+
# @return [ActiveSupport::SafeBuffer] the status-dot markup
|
|
38
|
+
def status_dot(text, color = :gray)
|
|
39
|
+
dot_color = dot_colors(color)
|
|
40
|
+
tag.span(class: 'inline-flex items-center gap-1.5 text-xs', style: 'color: var(--text-muted)') do
|
|
41
|
+
tag.span('', class: "h-1.5 w-1.5 rounded-full #{dot_color}") + text
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Map a semantic status color to its dot background class.
|
|
46
|
+
#
|
|
47
|
+
# @param color [Symbol] the semantic color
|
|
48
|
+
# @return [String] the Tailwind background class for the dot
|
|
49
|
+
def dot_colors(color)
|
|
50
|
+
case color
|
|
51
|
+
when :green then 'bg-green-600'
|
|
52
|
+
when :red then 'bg-red-600'
|
|
53
|
+
when :yellow then 'bg-yellow-600'
|
|
54
|
+
else 'bg-gray-500'
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Components
|
|
5
|
+
# Clickable actions: the primary/danger +button+, the solid form-submitting +action_button+ (its
|
|
6
|
+
# +button_to+ counterpart, sharing the same look via +button_class+), the amber text +ui_link+, and
|
|
7
|
+
# the two link-styled form buttons (+inline_button+, +destructive_link+) for subtler POST/DELETE
|
|
8
|
+
# actions. Variant styling is resolved by +button_variant_style+, defined alongside them here.
|
|
9
|
+
module ButtonsHelper
|
|
10
|
+
# Render a styled button. The +:danger+ variant uses the danger hover style.
|
|
11
|
+
#
|
|
12
|
+
# @param text [String] the button label
|
|
13
|
+
# @param variant [Symbol] the visual variant (see +button_variant_style+)
|
|
14
|
+
# @param attrs [Hash] extra HTML attributes forwarded to the button
|
|
15
|
+
# @return [ActiveSupport::SafeBuffer] the button markup
|
|
16
|
+
def button(text, variant: :primary, **attrs)
|
|
17
|
+
tag.button(text,
|
|
18
|
+
class: button_class(variant, attrs.delete(:class)),
|
|
19
|
+
style: button_variant_style(variant),
|
|
20
|
+
**attrs)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Render a solid, button-styled action that submits a form — a +button_to+ (so it POSTs, unlike the
|
|
24
|
+
# formless {#button}) wearing the same styling as {#button}. Use this for actions that mutate state
|
|
25
|
+
# from a plain click (e.g. "Sync now"); the link-styled {#inline_button}/{#destructive_link} are the
|
|
26
|
+
# subtler variants.
|
|
27
|
+
#
|
|
28
|
+
# @param text [String] the button label
|
|
29
|
+
# @param href [String] the form action target
|
|
30
|
+
# @param method [Symbol] the HTTP verb the button submits (default +:post+)
|
|
31
|
+
# @param variant [Symbol] the visual variant (see +button_variant_style+)
|
|
32
|
+
# @param attrs [Hash] extra HTML attributes forwarded to the button
|
|
33
|
+
# @return [ActiveSupport::SafeBuffer] the button-to markup
|
|
34
|
+
def action_button(text, href, method: :post, variant: :primary, **attrs)
|
|
35
|
+
button_to text, href,
|
|
36
|
+
method:,
|
|
37
|
+
class: button_class(variant, attrs.delete(:class)),
|
|
38
|
+
style: button_variant_style(variant),
|
|
39
|
+
form: { class: 'inline' },
|
|
40
|
+
**attrs
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Render a form button as a link-styled element, for use in inline
|
|
44
|
+
# forms (e.g. +button_to+ with +form: { class: "inline" }).
|
|
45
|
+
#
|
|
46
|
+
# @param text [String] the button text
|
|
47
|
+
# @param href [String] the form action target
|
|
48
|
+
# @param method [Symbol] the HTTP verb the button submits
|
|
49
|
+
# @param attrs [Hash] extra HTML attributes forwarded to the button
|
|
50
|
+
# @return [ActiveSupport::SafeBuffer] the button markup
|
|
51
|
+
def inline_button(text, href, method: :post, **attrs)
|
|
52
|
+
classes = 'text-sm text-amber-600 hover:text-amber-700 cursor-pointer ' \
|
|
53
|
+
"bg-transparent border-0 p-0 #{attrs.delete(:class)}"
|
|
54
|
+
|
|
55
|
+
button_to text, href,
|
|
56
|
+
method:,
|
|
57
|
+
class: classes,
|
|
58
|
+
form: { class: 'inline' },
|
|
59
|
+
**attrs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Render an amber text link, dimmed when +muted+.
|
|
63
|
+
#
|
|
64
|
+
# @param text [String] the link text
|
|
65
|
+
# @param href [String] the link target
|
|
66
|
+
# @param muted [Boolean] whether to render the dimmed variant
|
|
67
|
+
# @param attrs [Hash] extra HTML attributes forwarded to the link
|
|
68
|
+
# @return [ActiveSupport::SafeBuffer] the link markup
|
|
69
|
+
def ui_link(text, href, muted: false, **attrs)
|
|
70
|
+
classes = if muted
|
|
71
|
+
'text-sm text-amber-400 hover:text-amber-600'
|
|
72
|
+
else
|
|
73
|
+
'text-sm text-amber-600 hover:text-amber-700'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
link_to(text, href, class: "#{classes} #{attrs.delete(:class)}", **attrs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Render a destructive action as an inline form button with optional confirm.
|
|
80
|
+
#
|
|
81
|
+
# @param text [String] the link text
|
|
82
|
+
# @param href [String] the action target
|
|
83
|
+
# @param confirm [String, nil] optional Turbo confirmation prompt
|
|
84
|
+
# @param method [Symbol] the HTTP verb the button submits
|
|
85
|
+
# @return [ActiveSupport::SafeBuffer] the button-to markup
|
|
86
|
+
# @param [Hash{Symbol => Object}] attrs
|
|
87
|
+
def destructive_link(text, href, confirm: nil, method: :delete, **attrs)
|
|
88
|
+
classes = 'text-sm text-red-500 hover:text-red-700 cursor-pointer ' \
|
|
89
|
+
"bg-transparent border-0 p-0 #{attrs.delete(:class)}"
|
|
90
|
+
|
|
91
|
+
button_to text, href,
|
|
92
|
+
method:,
|
|
93
|
+
class: classes,
|
|
94
|
+
form: { class: 'inline' },
|
|
95
|
+
data: confirm ? { turbo_confirm: confirm } : {},
|
|
96
|
+
**attrs
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Render an icon-only button — a muted glyph that brightens on hover (e.g. a composer's attach
|
|
100
|
+
# control). +label+ is both the tooltip and the accessible name, since there is no visible text.
|
|
101
|
+
#
|
|
102
|
+
# @param name [Symbol] the icon to render (see {IconsHelper#icon})
|
|
103
|
+
# @param label [String] the tooltip / accessible label
|
|
104
|
+
# @param attrs [Hash] extra HTML attributes forwarded to the button (e.g. +data:+)
|
|
105
|
+
# @return [ActiveSupport::SafeBuffer] the icon-button markup
|
|
106
|
+
def icon_button(name, label:, **attrs)
|
|
107
|
+
tag.button(icon(name),
|
|
108
|
+
type: 'button',
|
|
109
|
+
title: label,
|
|
110
|
+
'aria-label': label,
|
|
111
|
+
class: 'rounded p-1.5 opacity-70 transition-opacity hover:opacity-100 cursor-pointer',
|
|
112
|
+
style: 'color: var(--text-muted)',
|
|
113
|
+
**attrs)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The shared class list for the solid button look, worn by both {#button} and {#action_button}.
|
|
117
|
+
#
|
|
118
|
+
# @param variant [Symbol] the button variant (only +:danger+ changes the hover treatment)
|
|
119
|
+
# @param extra [String, nil] caller-supplied extra classes
|
|
120
|
+
# @return [String] the combined class string
|
|
121
|
+
def button_class(variant, extra = nil)
|
|
122
|
+
hover_class = variant == :danger ? 'btn-danger' : 'btn-outline'
|
|
123
|
+
base_class = 'inline-flex items-center justify-center rounded-sm px-2.5 py-1 text-xs ' \
|
|
124
|
+
'font-semibold uppercase tracking-wide transition-colors cursor-pointer'
|
|
125
|
+
class_names(hover_class, base_class, extra)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Map a button variant to its inline style string.
|
|
129
|
+
#
|
|
130
|
+
# @param variant [Symbol] the button variant
|
|
131
|
+
# @return [String] the variant's CSS style string
|
|
132
|
+
def button_variant_style(variant)
|
|
133
|
+
case variant
|
|
134
|
+
when :secondary then 'background: transparent; border: 1px solid var(--border); color: var(--text-muted)'
|
|
135
|
+
when :danger then 'background: transparent; border: 1px solid #ef4444; color: #ef4444'
|
|
136
|
+
else 'background: transparent; border: 1px solid var(--border); color: var(--text)'
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Components
|
|
5
|
+
# Surface-card UI elements shared across engine views. One helper among the +Components+ peers pulled
|
|
6
|
+
# together by +ComponentsHelper+; +info_card+ calls +heading_2+ (+TypographyHelper+), resolved through
|
|
7
|
+
# the view, which has every helper.
|
|
8
|
+
#
|
|
9
|
+
# Colors use CSS custom properties (--surface, --border, --text-faint) defined
|
|
10
|
+
# in tailwind/application.css so dark mode works via a single .dark class toggle.
|
|
11
|
+
module CardsHelper
|
|
12
|
+
# Render a surface card, padded unless +padding+ is false.
|
|
13
|
+
#
|
|
14
|
+
# @param padding [Boolean] whether to apply interior padding
|
|
15
|
+
# @yield the card body content
|
|
16
|
+
# @return [ActiveSupport::SafeBuffer] the card markup
|
|
17
|
+
def card(padding: true, &)
|
|
18
|
+
tag.div(class: "rounded-xl #{'p-6' if padding}",
|
|
19
|
+
style: 'background: var(--surface); border: 1px solid var(--border)', &)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Render a titled card of label/value pairs in a monospace value column.
|
|
23
|
+
#
|
|
24
|
+
# By default the label sits in a fixed-width column beside the value — right for short labels like
|
|
25
|
+
# "Address". Pass +full_width: true+ to let the label take its natural width so the value uses the
|
|
26
|
+
# full row, for longer labels (e.g. resolver class names) that would overflow the fixed column.
|
|
27
|
+
#
|
|
28
|
+
# @param title [String] the card title
|
|
29
|
+
# @param pairs [Hash{String => String}] the label => value rows
|
|
30
|
+
# @param full_width [Boolean] let the label size to its content instead of a fixed column
|
|
31
|
+
# @return [ActiveSupport::SafeBuffer] the info-card markup
|
|
32
|
+
def info_card(title, pairs = {}, full_width: false)
|
|
33
|
+
tag.div(class: 'rounded-xl p-5',
|
|
34
|
+
style: 'background: var(--surface); border: 1px solid var(--border)') do
|
|
35
|
+
tag.div(class: 'mb-3') { heading_2(title) } +
|
|
36
|
+
tag.div(class: 'space-y-1 text-sm') do
|
|
37
|
+
safe_join(pairs.map do |label, value|
|
|
38
|
+
tag.div(class: 'flex gap-8') do
|
|
39
|
+
tag.span(label, class: (full_width ? 'shrink-0' : 'w-20'), style: 'color: var(--text-faint)') +
|
|
40
|
+
tag.code(value, class: 'font-mono break-all')
|
|
41
|
+
end
|
|
42
|
+
end)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Components
|
|
5
|
+
# The aggregator for the +Protege::Components::+ UI toolkit. It holds no methods of its own — every
|
|
6
|
+
# component lives in a focused, self-describing helper (typography, tables, badges, buttons, dialogs,
|
|
7
|
+
# menus, cards, forms, layout, sidebar, pagination, introspection) — and this module includes them
|
|
8
|
+
# all, so +Protege::ApplicationHelper+ pulls the whole toolkit into the view with a single
|
|
9
|
+
# +include Components::ComponentsHelper+. Because they land on the view together, any component can
|
|
10
|
+
# freely call another (e.g. +info_card+ renders a +heading_2+) through the view.
|
|
11
|
+
#
|
|
12
|
+
# Colors throughout use CSS custom properties (--surface, --border, --text, --text-muted,
|
|
13
|
+
# --text-faint) defined in tailwind/application.css so dark mode works via a single .dark class toggle.
|
|
14
|
+
module ComponentsHelper
|
|
15
|
+
include BadgesHelper
|
|
16
|
+
include ButtonsHelper
|
|
17
|
+
include CardsHelper
|
|
18
|
+
include DialogsHelper
|
|
19
|
+
include FormsHelper
|
|
20
|
+
include HotkeysHelper
|
|
21
|
+
include IntrospectionHelper
|
|
22
|
+
include LayoutHelper
|
|
23
|
+
include MenusHelper
|
|
24
|
+
include PaginationHelper
|
|
25
|
+
include SidebarHelper
|
|
26
|
+
include TablesHelper
|
|
27
|
+
include TypographyHelper
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Protege
|
|
4
|
+
module Components
|
|
5
|
+
# Native modal +<dialog>+ elements: +dialog+ renders one (closing on backdrop click), and +show_dialog+
|
|
6
|
+
# builds the +showModal()+ snippet a trigger fires to open it by id.
|
|
7
|
+
module DialogsHelper
|
|
8
|
+
# Build the JS snippet that opens a +<dialog>+ by element id.
|
|
9
|
+
#
|
|
10
|
+
# @param element_id [String] the DOM id of the target dialog
|
|
11
|
+
# @return [String] the +showModal()+ invocation
|
|
12
|
+
def show_dialog(element_id)
|
|
13
|
+
"document.getElementById('#{element_id}').showModal()"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Render a modal +<dialog>+ that closes on backdrop click.
|
|
17
|
+
#
|
|
18
|
+
# @param element_id [String] the DOM id assigned to the dialog
|
|
19
|
+
# @yield the dialog body content
|
|
20
|
+
# @return [ActiveSupport::SafeBuffer] the dialog markup
|
|
21
|
+
# @param [Hash{Symbol => Object}] attrs
|
|
22
|
+
def dialog(element_id, **attrs, &)
|
|
23
|
+
tag.dialog(
|
|
24
|
+
id: element_id,
|
|
25
|
+
# focus:outline-none — showModal() focuses the dialog itself when it has no focusable child
|
|
26
|
+
# (e.g. the hotkey-help overlay), which would otherwise draw the UA focus ring on the whole box.
|
|
27
|
+
class: 'rounded-xl p-6 shadow-lg backdrop:bg-gray-900/50 max-w-md w-full m-auto focus:outline-none',
|
|
28
|
+
style: 'background: var(--surface); border: 1px solid var(--border); color: var(--text)',
|
|
29
|
+
onclick: 'event.target === this && this.close()',
|
|
30
|
+
**attrs,
|
|
31
|
+
&
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|