context_drop 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 +21 -0
- data/README.md +337 -0
- data/Rakefile +20 -0
- data/SPEC.md +532 -0
- data/app/assets/javascripts/context_drop/context_drop.js +3508 -0
- data/app/assets/stylesheets/context_drop/application.css +2 -0
- data/app/controllers/context_drop/application_controller.rb +114 -0
- data/app/controllers/context_drop/attachments_controller.rb +31 -0
- data/app/controllers/context_drop/configs_controller.rb +20 -0
- data/app/controllers/context_drop/dashboard/feedbacks_controller.rb +107 -0
- data/app/controllers/context_drop/feedbacks_controller.rb +208 -0
- data/app/controllers/context_drop/health_controller.rb +9 -0
- data/app/controllers/context_drop/mcp_controller.rb +67 -0
- data/app/helpers/context_drop/widget_helper.rb +62 -0
- data/app/javascript/context_drop/__tests__/widget.test.ts +319 -0
- data/app/javascript/context_drop/api-client.ts +87 -0
- data/app/javascript/context_drop/element-picker.ts +1004 -0
- data/app/javascript/context_drop/icons.ts +35 -0
- data/app/javascript/context_drop/index.ts +11 -0
- data/app/javascript/context_drop/sanitizer.ts +454 -0
- data/app/javascript/context_drop/screenshot.ts +214 -0
- data/app/javascript/context_drop/styles.ts +690 -0
- data/app/javascript/context_drop/types.ts +110 -0
- data/app/javascript/context_drop/widget.ts +1120 -0
- data/app/models/context_drop/application_record.rb +7 -0
- data/app/models/context_drop/attachment.rb +30 -0
- data/app/models/context_drop/element_reference.rb +47 -0
- data/app/models/context_drop/feedback.rb +94 -0
- data/app/services/context_drop/metadata_sanitizer.rb +215 -0
- data/app/services/context_drop/upload_validator.rb +217 -0
- data/app/views/context_drop/dashboard/feedbacks/index.html.erb +86 -0
- data/app/views/context_drop/dashboard/feedbacks/show.html.erb +121 -0
- data/app/views/layouts/context_drop/application.html.erb +215 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20261109000001_create_context_drop_feedbacks.rb +23 -0
- data/db/migrate/20261109000002_create_context_drop_element_references.rb +29 -0
- data/db/migrate/20261109000003_create_context_drop_attachments.rb +24 -0
- data/lib/context_drop/base64_util.rb +15 -0
- data/lib/context_drop/configuration.rb +155 -0
- data/lib/context_drop/engine.rb +32 -0
- data/lib/context_drop/mcp/dispatcher.rb +197 -0
- data/lib/context_drop/mcp/prompts/investigate_feedback.rb +83 -0
- data/lib/context_drop/mcp/server.rb +25 -0
- data/lib/context_drop/mcp/stdio_runner.rb +62 -0
- data/lib/context_drop/mcp/tools/get_feedback.rb +133 -0
- data/lib/context_drop/mcp/tools/get_feedback_attachment.rb +71 -0
- data/lib/context_drop/mcp/tools/list_feedbacks.rb +127 -0
- data/lib/context_drop/mcp/tools/update_feedback_status.rb +72 -0
- data/lib/context_drop/version.rb +5 -0
- data/lib/context_drop.rb +32 -0
- data/lib/generators/context_drop/install/install_generator.rb +52 -0
- data/lib/generators/context_drop/install/templates/initializer.rb.tt +55 -0
- data/lib/tasks/context_drop_tasks.rake +14 -0
- metadata +246 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.25rem;">
|
|
2
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
3
|
+
<a href="<%= dashboard_feedbacks_path %>" class="cd-btn">← Back to List</a>
|
|
4
|
+
<h1 style="margin: 0; font-size: 1.25rem;">Feedback #<%= @feedback.id %></h1>
|
|
5
|
+
<span class="cd-badge cd-badge-<%= @feedback.status %>"><%= @feedback.status %></span>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
|
9
|
+
<% [
|
|
10
|
+
["reviewed", "Mark Reviewed"],
|
|
11
|
+
["resolved", "Mark Resolved"],
|
|
12
|
+
["dismissed", "Dismiss"]
|
|
13
|
+
].each do |target_status, label| %>
|
|
14
|
+
<%= form_with url: dashboard_feedback_path(@feedback), method: :patch, local: true, style: "display: inline;" do %>
|
|
15
|
+
<input type="hidden" name="status" value="<%= target_status %>">
|
|
16
|
+
<button type="submit"
|
|
17
|
+
class="cd-btn <%= 'cd-btn-primary' if target_status == 'resolved' %>"
|
|
18
|
+
<%= "disabled" if @feedback.status == target_status %>>
|
|
19
|
+
<%= label %>
|
|
20
|
+
</button>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="cd-card">
|
|
27
|
+
<h2 style="margin-top: 0; font-size: 1rem; color: var(--cd-muted);">Feedback Message</h2>
|
|
28
|
+
<p style="font-size: 1rem; white-space: pre-wrap; margin: 0 0 1.25rem 0;"><%= @feedback.message.presence || "(No message text provided)" %></p>
|
|
29
|
+
|
|
30
|
+
<dl class="cd-meta-list">
|
|
31
|
+
<dt>Page URL</dt>
|
|
32
|
+
<dd><code class="cd-mono"><%= @feedback.page_url %></code></dd>
|
|
33
|
+
<dt>Submitter</dt>
|
|
34
|
+
<dd><%= @feedback.submitter_label %></dd>
|
|
35
|
+
<dt>Submitted At</dt>
|
|
36
|
+
<dd><%= @feedback.created_at&.iso8601 %></dd>
|
|
37
|
+
<dt>User Agent</dt>
|
|
38
|
+
<dd><%= @feedback.user_agent.presence || "N/A" %></dd>
|
|
39
|
+
<dt>Metadata</dt>
|
|
40
|
+
<dd><code class="cd-mono"><%= JSON.generate(@feedback.metadata || {}) %></code></dd>
|
|
41
|
+
</dl>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="cd-card">
|
|
45
|
+
<h2 style="margin-top: 0; font-size: 1rem;">
|
|
46
|
+
DOM Element References (<%= @feedback.element_references.size %>)
|
|
47
|
+
</h2>
|
|
48
|
+
|
|
49
|
+
<% if @feedback.element_references.empty? %>
|
|
50
|
+
<p style="color: var(--cd-muted); margin: 0;">No DOM elements were selected for this submission.</p>
|
|
51
|
+
<% else %>
|
|
52
|
+
<div class="cd-grid">
|
|
53
|
+
<% @feedback.element_references.each do |el| %>
|
|
54
|
+
<div style="border: 1px solid var(--cd-border); border-radius: 0.5rem; padding: 1rem;">
|
|
55
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
|
56
|
+
<code class="cd-mono"><<%= el.tag_name %>></code>
|
|
57
|
+
<% if el.captured_at %>
|
|
58
|
+
<span style="font-size: 0.75rem; color: var(--cd-muted);"><%= el.captured_at.iso8601 %></span>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
<dl class="cd-meta-list">
|
|
62
|
+
<dt>Selector</dt>
|
|
63
|
+
<dd><code class="cd-mono"><%= el.selector %></code></dd>
|
|
64
|
+
<dt>DOM Path</dt>
|
|
65
|
+
<dd><code class="cd-mono"><%= el.dom_path.presence || "N/A" %></code></dd>
|
|
66
|
+
<dt>Text Content</dt>
|
|
67
|
+
<dd><%= el.text_content.presence || "(empty)" %></dd>
|
|
68
|
+
<dt>Bounding Rect</dt>
|
|
69
|
+
<dd><code class="cd-mono"><%= JSON.generate(el.bounding_rect || {}) %></code></dd>
|
|
70
|
+
<dt>Viewport</dt>
|
|
71
|
+
<dd><code class="cd-mono"><%= JSON.generate(el.viewport || {}) %></code></dd>
|
|
72
|
+
<dt>Attributes</dt>
|
|
73
|
+
<dd><code class="cd-mono"><%= JSON.generate(el.attributes_snapshot || {}) %></code></dd>
|
|
74
|
+
</dl>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
</div>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="cd-card">
|
|
82
|
+
<h2 style="margin-top: 0; font-size: 1rem;">
|
|
83
|
+
Screenshots & Attachments (<%= @feedback.attachments.size %>)
|
|
84
|
+
</h2>
|
|
85
|
+
|
|
86
|
+
<% if @feedback.attachments.empty? %>
|
|
87
|
+
<p style="color: var(--cd-muted); margin: 0;">No binary attachments or screenshots were included.</p>
|
|
88
|
+
<% else %>
|
|
89
|
+
<div class="cd-grid">
|
|
90
|
+
<% @feedback.attachments.each do |att| %>
|
|
91
|
+
<div style="border: 1px solid var(--cd-border); border-radius: 0.5rem; padding: 1rem;">
|
|
92
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
|
93
|
+
<span class="cd-badge cd-badge-new"><%= att.attachment_type %></span>
|
|
94
|
+
<% if att.element_reference %>
|
|
95
|
+
<span style="font-size: 0.75rem; color: var(--cd-muted);">
|
|
96
|
+
Element: <code class="cd-mono"><%= att.element_reference.selector %></code>
|
|
97
|
+
</span>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<% if att.respond_to?(:file) && att.file.attached? %>
|
|
102
|
+
<div style="margin: 0.5rem 0;">
|
|
103
|
+
<% if main_app.respond_to?(:url_for) %>
|
|
104
|
+
<img src="<%= main_app.url_for(att.file) %>"
|
|
105
|
+
alt="Attachment #<%= att.id %>"
|
|
106
|
+
style="max-width: 100%; border-radius: 0.375rem; border: 1px solid var(--cd-border);" />
|
|
107
|
+
<% end %>
|
|
108
|
+
</div>
|
|
109
|
+
<div style="font-size: 0.75rem; color: var(--cd-muted);">
|
|
110
|
+
<%= att.file.filename %> (<%= att.file.content_type %>, <%= att.file.byte_size %> bytes)
|
|
111
|
+
</div>
|
|
112
|
+
<% end %>
|
|
113
|
+
|
|
114
|
+
<div style="margin-top: 0.5rem;">
|
|
115
|
+
<code class="cd-mono"><%= JSON.generate(att.metadata || {}) %></code>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>ContextDrop — Feedback Triage Dashboard</title>
|
|
7
|
+
<%= csrf_meta_tags if respond_to?(:csrf_meta_tags) %>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--cd-bg: #f8fafc;
|
|
11
|
+
--cd-surface: #ffffff;
|
|
12
|
+
--cd-border: #e2e8f0;
|
|
13
|
+
--cd-text: #0f172a;
|
|
14
|
+
--cd-muted: #64748b;
|
|
15
|
+
--cd-primary: #2563eb;
|
|
16
|
+
--cd-primary-hover: #1d4ed8;
|
|
17
|
+
--cd-new-bg: #dbeafe;
|
|
18
|
+
--cd-new-text: #1e40af;
|
|
19
|
+
--cd-reviewed-bg: #fef3c7;
|
|
20
|
+
--cd-reviewed-text: #92400e;
|
|
21
|
+
--cd-resolved-bg: #dcfce7;
|
|
22
|
+
--cd-resolved-text: #166534;
|
|
23
|
+
--cd-dismissed-bg: #f1f5f9;
|
|
24
|
+
--cd-dismissed-text: #475569;
|
|
25
|
+
}
|
|
26
|
+
* { box-sizing: border-box; }
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
30
|
+
background: var(--cd-bg);
|
|
31
|
+
color: var(--cd-text);
|
|
32
|
+
line-height: 1.5;
|
|
33
|
+
}
|
|
34
|
+
.cd-header {
|
|
35
|
+
background: var(--cd-surface);
|
|
36
|
+
border-bottom: 1px solid var(--cd-border);
|
|
37
|
+
padding: 0.875rem 1.5rem;
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: space-between;
|
|
41
|
+
}
|
|
42
|
+
.cd-brand {
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
font-size: 1.125rem;
|
|
45
|
+
color: var(--cd-text);
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 0.5rem;
|
|
50
|
+
}
|
|
51
|
+
.cd-brand-badge {
|
|
52
|
+
font-size: 0.75rem;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
padding: 0.125rem 0.5rem;
|
|
55
|
+
border-radius: 9999px;
|
|
56
|
+
background: var(--cd-new-bg);
|
|
57
|
+
color: var(--cd-new-text);
|
|
58
|
+
}
|
|
59
|
+
.cd-container {
|
|
60
|
+
max-width: 1200px;
|
|
61
|
+
margin: 1.5rem auto;
|
|
62
|
+
padding: 0 1.5rem;
|
|
63
|
+
}
|
|
64
|
+
.cd-flash {
|
|
65
|
+
padding: 0.75rem 1rem;
|
|
66
|
+
border-radius: 0.5rem;
|
|
67
|
+
margin-bottom: 1rem;
|
|
68
|
+
font-size: 0.875rem;
|
|
69
|
+
border: 1px solid var(--cd-border);
|
|
70
|
+
background: var(--cd-surface);
|
|
71
|
+
}
|
|
72
|
+
.cd-flash-notice { border-color: #86efac; background: #f0fdf4; color: #166534; }
|
|
73
|
+
.cd-flash-alert { border-color: #fca5a5; background: #fef2f2; color: #991b1b; }
|
|
74
|
+
.cd-card {
|
|
75
|
+
background: var(--cd-surface);
|
|
76
|
+
border: 1px solid var(--cd-border);
|
|
77
|
+
border-radius: 0.75rem;
|
|
78
|
+
padding: 1.25rem;
|
|
79
|
+
margin-bottom: 1.25rem;
|
|
80
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
81
|
+
}
|
|
82
|
+
.cd-tabs {
|
|
83
|
+
display: flex;
|
|
84
|
+
gap: 0.5rem;
|
|
85
|
+
flex-wrap: wrap;
|
|
86
|
+
margin-bottom: 1rem;
|
|
87
|
+
}
|
|
88
|
+
.cd-tab {
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: 0.375rem;
|
|
92
|
+
padding: 0.4rem 0.85rem;
|
|
93
|
+
border-radius: 0.5rem;
|
|
94
|
+
font-size: 0.875rem;
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
color: var(--cd-muted);
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
border: 1px solid transparent;
|
|
99
|
+
}
|
|
100
|
+
.cd-tab:hover { background: #f1f5f9; color: var(--cd-text); }
|
|
101
|
+
.cd-tab.active {
|
|
102
|
+
background: var(--cd-surface);
|
|
103
|
+
color: var(--cd-primary);
|
|
104
|
+
border-color: var(--cd-border);
|
|
105
|
+
font-weight: 600;
|
|
106
|
+
}
|
|
107
|
+
.cd-filter-bar {
|
|
108
|
+
display: flex;
|
|
109
|
+
gap: 0.75rem;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
margin-bottom: 1rem;
|
|
112
|
+
}
|
|
113
|
+
.cd-input {
|
|
114
|
+
padding: 0.5rem 0.75rem;
|
|
115
|
+
border: 1px solid var(--cd-border);
|
|
116
|
+
border-radius: 0.5rem;
|
|
117
|
+
font-size: 0.875rem;
|
|
118
|
+
min-width: 220px;
|
|
119
|
+
}
|
|
120
|
+
.cd-btn {
|
|
121
|
+
display: inline-flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 0.375rem;
|
|
124
|
+
padding: 0.5rem 0.9rem;
|
|
125
|
+
border-radius: 0.5rem;
|
|
126
|
+
font-size: 0.875rem;
|
|
127
|
+
font-weight: 500;
|
|
128
|
+
border: 1px solid var(--cd-border);
|
|
129
|
+
background: var(--cd-surface);
|
|
130
|
+
color: var(--cd-text);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
text-decoration: none;
|
|
133
|
+
}
|
|
134
|
+
.cd-btn:hover { background: #f8fafc; }
|
|
135
|
+
.cd-btn-primary {
|
|
136
|
+
background: var(--cd-primary);
|
|
137
|
+
border-color: var(--cd-primary);
|
|
138
|
+
color: #ffffff;
|
|
139
|
+
}
|
|
140
|
+
.cd-btn-primary:hover { background: var(--cd-primary-hover); }
|
|
141
|
+
.cd-table {
|
|
142
|
+
width: 100%;
|
|
143
|
+
border-collapse: collapse;
|
|
144
|
+
font-size: 0.875rem;
|
|
145
|
+
}
|
|
146
|
+
.cd-table th, .cd-table td {
|
|
147
|
+
text-align: left;
|
|
148
|
+
padding: 0.75rem 0.875rem;
|
|
149
|
+
border-bottom: 1px solid var(--cd-border);
|
|
150
|
+
vertical-align: top;
|
|
151
|
+
}
|
|
152
|
+
.cd-table th {
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
color: var(--cd-muted);
|
|
155
|
+
font-size: 0.75rem;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
letter-spacing: 0.04em;
|
|
158
|
+
}
|
|
159
|
+
.cd-badge {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
padding: 0.15rem 0.55rem;
|
|
163
|
+
border-radius: 9999px;
|
|
164
|
+
font-size: 0.75rem;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
text-transform: capitalize;
|
|
167
|
+
}
|
|
168
|
+
.cd-badge-new { background: var(--cd-new-bg); color: var(--cd-new-text); }
|
|
169
|
+
.cd-badge-reviewed { background: var(--cd-reviewed-bg); color: var(--cd-reviewed-text); }
|
|
170
|
+
.cd-badge-resolved { background: var(--cd-resolved-bg); color: var(--cd-resolved-text); }
|
|
171
|
+
.cd-badge-dismissed { background: var(--cd-dismissed-bg); color: var(--cd-dismissed-text); }
|
|
172
|
+
.cd-mono {
|
|
173
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
174
|
+
font-size: 0.8125rem;
|
|
175
|
+
background: #f1f5f9;
|
|
176
|
+
padding: 0.15rem 0.4rem;
|
|
177
|
+
border-radius: 0.25rem;
|
|
178
|
+
}
|
|
179
|
+
.cd-grid {
|
|
180
|
+
display: grid;
|
|
181
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
182
|
+
gap: 1rem;
|
|
183
|
+
}
|
|
184
|
+
.cd-meta-list {
|
|
185
|
+
margin: 0;
|
|
186
|
+
display: grid;
|
|
187
|
+
grid-template-columns: 140px 1fr;
|
|
188
|
+
row-gap: 0.5rem;
|
|
189
|
+
font-size: 0.875rem;
|
|
190
|
+
}
|
|
191
|
+
.cd-meta-list dt { color: var(--cd-muted); font-weight: 500; }
|
|
192
|
+
.cd-meta-list dd { margin: 0; word-break: break-all; }
|
|
193
|
+
</style>
|
|
194
|
+
</head>
|
|
195
|
+
<body>
|
|
196
|
+
<header class="cd-header">
|
|
197
|
+
<a class="cd-brand" href="<%= dashboard_feedbacks_path %>">
|
|
198
|
+
<span>ContextDrop</span>
|
|
199
|
+
<span class="cd-brand-badge">Feedback Triage</span>
|
|
200
|
+
</a>
|
|
201
|
+
<span style="font-size: 0.8125rem; color: var(--cd-muted);">v<%= ContextDrop::VERSION %></span>
|
|
202
|
+
</header>
|
|
203
|
+
|
|
204
|
+
<main class="cd-container">
|
|
205
|
+
<% if flash[:notice].present? %>
|
|
206
|
+
<div class="cd-flash cd-flash-notice"><%= flash[:notice] %></div>
|
|
207
|
+
<% end %>
|
|
208
|
+
<% if flash[:alert].present? %>
|
|
209
|
+
<div class="cd-flash cd-flash-alert"><%= flash[:alert] %></div>
|
|
210
|
+
<% end %>
|
|
211
|
+
|
|
212
|
+
<%= yield %>
|
|
213
|
+
</main>
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
ContextDrop::Engine.routes.draw do
|
|
4
|
+
get "config", to: "configs#show"
|
|
5
|
+
get "assets/context_drop.js", to: "configs#widget_bundle"
|
|
6
|
+
post "attachments", to: "attachments#create"
|
|
7
|
+
post "feedback", to: "feedbacks#create"
|
|
8
|
+
get "health", to: "health#show"
|
|
9
|
+
post "mcp", to: "mcp#create"
|
|
10
|
+
|
|
11
|
+
namespace :dashboard do
|
|
12
|
+
root to: "feedbacks#index"
|
|
13
|
+
resources :feedbacks, only: %i[index show update]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
root to: "dashboard/feedbacks#index"
|
|
17
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateContextDropFeedbacks < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
json_type = connection.adapter_name.downcase.include?("postgres") ? :jsonb : :json
|
|
6
|
+
|
|
7
|
+
create_table :context_drop_feedbacks do |t|
|
|
8
|
+
t.references :submitter, polymorphic: true, null: true, index: { name: "idx_context_drop_feedbacks_on_submitter" }
|
|
9
|
+
t.string :session_id, null: true
|
|
10
|
+
t.text :message, null: true
|
|
11
|
+
t.string :page_url, null: false
|
|
12
|
+
t.string :user_agent, null: true
|
|
13
|
+
t.public_send(json_type, :metadata, null: false, default: {})
|
|
14
|
+
t.string :status, null: false, default: "new"
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :context_drop_feedbacks, :status
|
|
20
|
+
add_index :context_drop_feedbacks, :created_at
|
|
21
|
+
add_index :context_drop_feedbacks, :page_url
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateContextDropElementReferences < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
json_type = connection.adapter_name.downcase.include?("postgres") ? :jsonb : :json
|
|
6
|
+
|
|
7
|
+
create_table :context_drop_element_references do |t|
|
|
8
|
+
t.references :feedback,
|
|
9
|
+
null: false,
|
|
10
|
+
foreign_key: { to_table: :context_drop_feedbacks, on_delete: :cascade },
|
|
11
|
+
index: { name: "idx_context_drop_element_refs_on_feedback_id" }
|
|
12
|
+
t.string :client_ref, null: true
|
|
13
|
+
t.string :tag_name, null: false
|
|
14
|
+
t.string :selector, null: false
|
|
15
|
+
t.string :dom_path, null: true
|
|
16
|
+
t.text :text_content, null: true
|
|
17
|
+
t.public_send(json_type, :attributes_snapshot, null: false, default: {})
|
|
18
|
+
t.public_send(json_type, :bounding_rect, null: false, default: {})
|
|
19
|
+
t.public_send(json_type, :viewport, null: false, default: {})
|
|
20
|
+
t.public_send(json_type, :metadata, null: false, default: {})
|
|
21
|
+
t.datetime :captured_at, null: true
|
|
22
|
+
|
|
23
|
+
t.timestamps
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
add_index :context_drop_element_references, :selector
|
|
27
|
+
add_index :context_drop_element_references, :tag_name
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateContextDropAttachments < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
json_type = connection.adapter_name.downcase.include?("postgres") ? :jsonb : :json
|
|
6
|
+
|
|
7
|
+
create_table :context_drop_attachments do |t|
|
|
8
|
+
t.references :feedback,
|
|
9
|
+
null: false,
|
|
10
|
+
foreign_key: { to_table: :context_drop_feedbacks, on_delete: :cascade },
|
|
11
|
+
index: { name: "idx_context_drop_attachments_on_feedback_id" }
|
|
12
|
+
t.references :element_reference,
|
|
13
|
+
null: true,
|
|
14
|
+
foreign_key: { to_table: :context_drop_element_references, on_delete: :nullify },
|
|
15
|
+
index: { name: "idx_context_drop_attachments_on_element_ref_id" }
|
|
16
|
+
t.string :attachment_type, null: false
|
|
17
|
+
t.public_send(json_type, :metadata, null: false, default: {})
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
add_index :context_drop_attachments, :attachment_type
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContextDrop
|
|
4
|
+
class Configuration
|
|
5
|
+
VALID_POSITIONS = %i[bottom_right bottom_left top_right top_left].freeze
|
|
6
|
+
|
|
7
|
+
MANDATORY_EXCLUDED_SELECTORS = [
|
|
8
|
+
"[data-feedback-ignore]",
|
|
9
|
+
"[data-feedback-sensitive]",
|
|
10
|
+
"input[type='password']",
|
|
11
|
+
"input[type='hidden']",
|
|
12
|
+
"[autocomplete^='cc-']"
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
DEFAULT_ALLOWED_CONTENT_TYPES = %w[
|
|
16
|
+
image/png
|
|
17
|
+
image/jpeg
|
|
18
|
+
image/webp
|
|
19
|
+
image/gif
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
attr_accessor :enabled,
|
|
23
|
+
:storage,
|
|
24
|
+
:active_storage_service,
|
|
25
|
+
:allow_anonymous,
|
|
26
|
+
:max_attachments,
|
|
27
|
+
:max_element_references,
|
|
28
|
+
:max_attachment_size,
|
|
29
|
+
:allowed_content_types,
|
|
30
|
+
:max_image_dimensions,
|
|
31
|
+
:element_selection,
|
|
32
|
+
:screenshot_capture,
|
|
33
|
+
:element_capture_padding,
|
|
34
|
+
:excluded_selectors,
|
|
35
|
+
:current_submitter,
|
|
36
|
+
:authorize_submission,
|
|
37
|
+
:authorize_dashboard,
|
|
38
|
+
:rate_limit,
|
|
39
|
+
:on_feedback_created,
|
|
40
|
+
:mcp_enabled,
|
|
41
|
+
:mcp_api_key,
|
|
42
|
+
:authorize_mcp
|
|
43
|
+
|
|
44
|
+
attr_reader :position
|
|
45
|
+
|
|
46
|
+
def initialize
|
|
47
|
+
@enabled = false
|
|
48
|
+
@position = :bottom_right
|
|
49
|
+
@storage = :active_storage
|
|
50
|
+
@active_storage_service = nil
|
|
51
|
+
@allow_anonymous = true
|
|
52
|
+
|
|
53
|
+
@max_attachments = 10
|
|
54
|
+
@max_element_references = 10
|
|
55
|
+
@max_attachment_size = 10 * 1024 * 1024 # 10 MB
|
|
56
|
+
@allowed_content_types = DEFAULT_ALLOWED_CONTENT_TYPES.dup
|
|
57
|
+
@max_image_dimensions = [4096, 4096]
|
|
58
|
+
|
|
59
|
+
@element_selection = true
|
|
60
|
+
@screenshot_capture = true
|
|
61
|
+
@element_capture_padding = 16
|
|
62
|
+
|
|
63
|
+
@excluded_selectors = MANDATORY_EXCLUDED_SELECTORS.dup
|
|
64
|
+
|
|
65
|
+
@current_submitter = lambda do |controller|
|
|
66
|
+
controller.respond_to?(:current_user, true) ? controller.send(:current_user) : nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@authorize_submission = ->(_controller) { true }
|
|
70
|
+
|
|
71
|
+
@authorize_dashboard = lambda do |controller|
|
|
72
|
+
user = controller.respond_to?(:current_user, true) ? controller.send(:current_user) : nil
|
|
73
|
+
is_admin = user.respond_to?(:admin?) && user.admin?
|
|
74
|
+
is_dev = defined?(Rails) && Rails.respond_to?(:env) && Rails.env.development?
|
|
75
|
+
Boolean(is_admin || is_dev)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@rate_limit = { max_requests: 20, period: 3600 }
|
|
79
|
+
@on_feedback_created = ->(_feedback) {}
|
|
80
|
+
|
|
81
|
+
@mcp_enabled = false
|
|
82
|
+
@mcp_api_key = nil
|
|
83
|
+
@authorize_mcp = nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def position=(value)
|
|
87
|
+
sym = value.to_s.strip.to_sym
|
|
88
|
+
unless VALID_POSITIONS.include?(sym)
|
|
89
|
+
raise ArgumentError, "Invalid ContextDrop position #{value.inspect}. Must be one of: #{VALID_POSITIONS.join(', ')}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@position = sym
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def effective_excluded_selectors
|
|
96
|
+
(MANDATORY_EXCLUDED_SELECTORS + Array(@excluded_selectors).map(&:to_s)).uniq
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_submitter(controller)
|
|
100
|
+
return nil unless @current_submitter.respond_to?(:call)
|
|
101
|
+
|
|
102
|
+
@current_submitter.call(controller)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def submission_authorized?(controller)
|
|
106
|
+
submitter = resolve_submitter(controller)
|
|
107
|
+
return false if !@allow_anonymous && submitter.nil?
|
|
108
|
+
return true unless @authorize_submission.respond_to?(:call)
|
|
109
|
+
|
|
110
|
+
!!@authorize_submission.call(controller)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def dashboard_authorized?(controller)
|
|
114
|
+
return false unless @authorize_dashboard.respond_to?(:call)
|
|
115
|
+
|
|
116
|
+
!!@authorize_dashboard.call(controller)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def public_widget_config(overrides = {})
|
|
120
|
+
resolved_position = overrides[:position] ? normalize_position(overrides[:position]) : @position
|
|
121
|
+
|
|
122
|
+
{
|
|
123
|
+
enabled: !!@enabled,
|
|
124
|
+
position: resolved_position.to_s,
|
|
125
|
+
placeholder: (overrides[:placeholder] || "What would you like to tell us?").to_s,
|
|
126
|
+
features: {
|
|
127
|
+
element_selection: !!@element_selection,
|
|
128
|
+
screenshot_capture: !!@screenshot_capture,
|
|
129
|
+
element_capture_padding: @element_capture_padding.to_i
|
|
130
|
+
},
|
|
131
|
+
limits: {
|
|
132
|
+
max_attachments: @max_attachments.to_i,
|
|
133
|
+
max_element_references: @max_element_references.to_i,
|
|
134
|
+
max_attachment_size: @max_attachment_size.to_i,
|
|
135
|
+
allowed_content_types: Array(@allowed_content_types),
|
|
136
|
+
max_image_dimensions: Array(@max_image_dimensions)
|
|
137
|
+
},
|
|
138
|
+
privacy: {
|
|
139
|
+
excluded_selectors: effective_excluded_selectors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def normalize_position(pos)
|
|
147
|
+
sym = pos.to_s.strip.to_sym
|
|
148
|
+
VALID_POSITIONS.include?(sym) ? sym : @position
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def Boolean(val) # rubocop:disable Naming/MethodName
|
|
152
|
+
!!val
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ContextDrop
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace ContextDrop
|
|
6
|
+
|
|
7
|
+
initializer "context_drop.assets" do |app|
|
|
8
|
+
if app.config.respond_to?(:assets) && app.config.assets.respond_to?(:paths)
|
|
9
|
+
app.config.assets.paths << root.join("app/assets/javascripts").to_s
|
|
10
|
+
app.config.assets.paths << root.join("app/assets/stylesheets").to_s
|
|
11
|
+
app.config.assets.paths << root.join("app/javascript").to_s
|
|
12
|
+
|
|
13
|
+
if app.config.assets.respond_to?(:precompile)
|
|
14
|
+
app.config.assets.precompile += %w[
|
|
15
|
+
context_drop/context_drop.js
|
|
16
|
+
context_drop/application.css
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
initializer "context_drop.helpers" do
|
|
23
|
+
ActiveSupport.on_load(:action_view) do
|
|
24
|
+
include ContextDrop::WidgetHelper
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rake_tasks do
|
|
29
|
+
load File.expand_path("../tasks/context_drop_tasks.rake", __dir__)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|