@mindbowser_inc/ui-feedback-plugin 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -8,9 +8,7 @@ A drop-in floating widget for collecting annotated UI feedback on web prototypes
8
8
  - Full-page screenshots with annotations
9
9
  - Comment threads with replies
10
10
  - Open / Resolved filter
11
- - Real-time sync across tabs
12
- - Firebase **or** Supabase backend
13
- - S3 screenshot storage via a backend proxy
11
+ - Three fully independent backend options
14
12
 
15
13
  ---
16
14
 
@@ -22,22 +20,34 @@ npm install @mindbowser_inc/ui-feedback-plugin
22
20
 
23
21
  ---
24
22
 
25
- ## Quick start
23
+ ## Backends
26
24
 
27
- ### With Firebase
25
+ Choose **one** backend. They are fully independent — no mixing required.
26
+
27
+ | Backend | Database | Screenshot storage | Real-time |
28
+ |---|---|---|---|
29
+ | **Firebase** | Firestore | Firebase Storage | Yes (onSnapshot) |
30
+ | **Supabase** | Postgres | Supabase Storage | Yes (Realtime) |
31
+ | **AWS S3** | JSON files in S3 | S3 | Polling (5s default) |
32
+
33
+ ---
34
+
35
+ ## Firebase
36
+
37
+ Firestore stores comments and replies. Firebase Storage holds screenshots.
28
38
 
29
39
  ```js
30
40
  import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
31
41
 
32
42
  initFeedbackPlugin({
33
- database: {
43
+ backend: {
34
44
  provider: 'firebase',
35
- apiKey: process.env.FIREBASE_API_KEY,
36
- authDomain: process.env.FIREBASE_AUTH_DOMAIN,
37
- projectId: process.env.FIREBASE_PROJECT_ID,
38
- storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
45
+ apiKey: process.env.FIREBASE_API_KEY,
46
+ authDomain: process.env.FIREBASE_AUTH_DOMAIN,
47
+ projectId: process.env.FIREBASE_PROJECT_ID,
48
+ storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
39
49
  messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
40
- appId: process.env.FIREBASE_APP_ID,
50
+ appId: process.env.FIREBASE_APP_ID,
41
51
  },
42
52
  projectKey: 'my-project',
43
53
  })
@@ -45,24 +55,28 @@ initFeedbackPlugin({
45
55
 
46
56
  > The Firebase API key is a **public identifier**, not a secret. Secure your data with [Firebase Security Rules](https://firebase.google.com/docs/rules).
47
57
 
48
- ### With Supabase
58
+ ---
59
+
60
+ ## Supabase
61
+
62
+ Postgres stores comments and replies. Supabase Storage holds screenshots.
49
63
 
50
64
  ```js
51
65
  import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
52
66
 
53
67
  initFeedbackPlugin({
54
- database: {
68
+ backend: {
55
69
  provider: 'supabase',
56
- url: process.env.SUPABASE_URL,
70
+ url: process.env.SUPABASE_URL,
57
71
  anonKey: process.env.SUPABASE_ANON_KEY,
58
72
  },
59
73
  projectKey: 'my-project',
60
74
  })
61
75
  ```
62
76
 
63
- > The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security) policies.
77
+ > The Supabase `anonKey` is a **public key by design**. Secure your data with [Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security).
64
78
 
65
- **Supabase setup** — run this SQL in your Supabase SQL editor:
79
+ **Required SQL** — run this in your Supabase SQL editor once:
66
80
 
67
81
  ```sql
68
82
  create table if not exists ufp_comments (
@@ -76,7 +90,6 @@ create table if not exists ufp_comments (
76
90
  resolved boolean default false,
77
91
  created_at bigint not null
78
92
  );
79
-
80
93
  create table if not exists ufp_replies (
81
94
  id uuid primary key default gen_random_uuid(),
82
95
  comment_id uuid not null references ufp_comments(id) on delete cascade,
@@ -84,19 +97,16 @@ create table if not exists ufp_replies (
84
97
  text text not null,
85
98
  created_at bigint not null
86
99
  );
87
-
88
100
  alter table ufp_comments enable row level security;
89
101
  alter table ufp_replies enable row level security;
90
102
  create policy "ufp_comments_all" on ufp_comments for all using (true) with check (true);
91
103
  create policy "ufp_replies_all" on ufp_replies for all using (true) with check (true);
92
-
93
104
  grant select, insert, update, delete on ufp_comments to anon;
94
105
  grant select, insert, update, delete on ufp_replies to anon;
95
-
96
106
  alter publication supabase_realtime add table ufp_comments;
97
107
  ```
98
108
 
99
- Then create a **public** Storage bucket named `ufp-screenshots` and add these policies:
109
+ Then in **Storage**, create a public bucket named `ufp-screenshots` and add:
100
110
 
101
111
  ```sql
102
112
  create policy "ufp_storage_insert" on storage.objects for insert to anon with check (bucket_id = 'ufp-screenshots');
@@ -106,27 +116,39 @@ create policy "ufp_storage_update" on storage.objects for update to anon using (
106
116
 
107
117
  ---
108
118
 
109
- ## S3 screenshot storage
119
+ ## AWS S3
120
+
121
+ Everything — comments, replies, and screenshots — lives in S3 as JSON/image files. No Firebase or Supabase needed.
110
122
 
111
- Store screenshots in S3 while keeping your database on Firebase or Supabase. **AWS credentials must never appear in frontend code** — provide a backend proxy endpoint instead.
123
+ **AWS credentials must never appear in frontend code.** This adapter calls your own backend API, which holds the credentials and talks to S3.
112
124
 
113
125
  ```js
126
+ import { initFeedbackPlugin } from '@mindbowser_inc/ui-feedback-plugin'
127
+
114
128
  initFeedbackPlugin({
115
- database: {
116
- provider: 'supabase',
117
- url: process.env.SUPABASE_URL,
118
- anonKey: process.env.SUPABASE_ANON_KEY,
119
- },
120
- screenshots: {
121
- provider: 's3',
122
- uploadEndpoint: 'https://your-api.com/upload-screenshot',
123
- headers: { Authorization: 'Bearer your-internal-token' },
129
+ backend: {
130
+ provider: 's3',
131
+ apiUrl: 'https://your-api.com/ufp', // your backend base URL
132
+ headers: { Authorization: 'Bearer your-internal-token' },
133
+ pollInterval: 5000, // ms between comment refreshes (default: 5000)
124
134
  },
125
135
  projectKey: 'my-project',
126
136
  })
127
137
  ```
128
138
 
129
- Your endpoint receives `POST { commentId: string, imageData: string }` (base64) and must return `{ url: string }`. See the [minimal Node/Express example](src/adapters/s3.ts).
139
+ **Your backend must expose these endpoints:**
140
+
141
+ | Method | Path | Body | Returns |
142
+ |---|---|---|---|
143
+ | GET | `/comments?pageUrl=...&projectKey=...` | — | `FeedbackComment[]` |
144
+ | POST | `/comments` | `FeedbackComment` (no id) | `{ id: string }` |
145
+ | PUT | `/comments/:id` | `Partial<FeedbackComment>` | `200` |
146
+ | DELETE | `/comments/:id` | — | `200` |
147
+ | GET | `/replies?commentId=...` | — | `Reply[]` |
148
+ | POST | `/replies` | `Reply` (no id) | `{ id: string }` |
149
+ | POST | `/screenshots` | `{ commentId, imageData }` (base64) | `{ url: string }` |
150
+
151
+ A full Node.js/Express reference server is included as comments inside [`src/adapters/s3.ts`](src/adapters/s3.ts).
130
152
 
131
153
  ---
132
154
 
@@ -134,23 +156,22 @@ Your endpoint receives `POST { commentId: string, imageData: string }` (base64)
134
156
 
135
157
  ```ts
136
158
  initFeedbackPlugin({
137
- database: FirebaseProviderConfig | SupabaseProviderConfig // required
138
- screenshots?: 'firebase' | 'supabase' | S3ScreenshotConfig // optional, defaults to same as database
139
- projectKey?: string // namespace comments per project (default: 'default')
140
- position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' // default: 'bottom-right'
141
- theme?: { primaryColor?: string } // default: '#6366f1'
159
+ backend: FirebaseProviderConfig | SupabaseProviderConfig | S3ProviderConfig // required
160
+ projectKey?: string // namespace comments per project (default: 'default')
161
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
162
+ theme?: { primaryColor?: string } // default: '#6366f1'
142
163
  })
143
164
  ```
144
165
 
145
166
  ---
146
167
 
147
- ## Usage in plain HTML (UMD)
168
+ ## Plain HTML (UMD / CDN)
148
169
 
149
170
  ```html
150
171
  <script src="https://unpkg.com/@mindbowser_inc/ui-feedback-plugin/dist/ui-feedback-plugin.umd.js"></script>
151
172
  <script>
152
173
  UIFeedbackPlugin.initFeedbackPlugin({
153
- database: { provider: 'supabase', url: '...', anonKey: '...' },
174
+ backend: { provider: 'supabase', url: '...', anonKey: '...' },
154
175
  projectKey: 'my-project',
155
176
  })
156
177
  </script>
package/dist/index.d.ts CHANGED
@@ -1,16 +1,15 @@
1
- declare type DatabaseConfig = FirebaseProviderConfig | SupabaseProviderConfig;
1
+ declare type BackendConfig = FirebaseProviderConfig | SupabaseProviderConfig | S3ProviderConfig;
2
2
 
3
+ /**
4
+ * Firebase backend — Firestore for comments/replies, Firebase Storage for screenshots.
5
+ * The API key is a public identifier, not a secret. Secure data with Firebase Security Rules.
6
+ */
3
7
  declare interface FirebaseProviderConfig {
4
8
  provider: 'firebase';
5
- /**
6
- * Firebase API key. This is a public identifier — not a secret.
7
- * Secure your data with Firebase Security Rules.
8
- * Store in an environment variable (VITE_FIREBASE_API_KEY, REACT_APP_FIREBASE_API_KEY, etc.)
9
- */
10
9
  apiKey: string;
11
10
  authDomain: string;
12
11
  projectId: string;
13
- /** Required when using Firebase Storage for screenshots. */
12
+ /** Required for screenshot storage. */
14
13
  storageBucket?: string;
15
14
  messagingSenderId?: string;
16
15
  appId?: string;
@@ -19,15 +18,14 @@ declare interface FirebaseProviderConfig {
19
18
  export declare function initFeedbackPlugin(config: PluginConfig): () => void;
20
19
 
21
20
  export declare interface PluginConfig {
22
- /** Database provider for storing comments and replies. */
23
- database: DatabaseConfig;
24
21
  /**
25
- * Screenshot storage provider.
26
- * Defaults to the same provider as `database`.
27
- * Use `s3` to keep screenshots off Firebase/Supabase.
22
+ * Storage backend. Pick one:
23
+ * - `firebase` — Firestore + Firebase Storage
24
+ * - `supabase` — Postgres + Supabase Storage
25
+ * - `s3` — Everything in AWS S3 via your backend API (no Firebase/Supabase needed)
28
26
  */
29
- screenshots?: ScreenshotConfig;
30
- /** Namespace comments by project. Use this when multiple prototypes share a backend. */
27
+ backend: BackendConfig;
28
+ /** Namespace comments by project (default: 'default'). */
31
29
  projectKey?: string;
32
30
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
33
31
  theme?: {
@@ -36,37 +34,34 @@ export declare interface PluginConfig {
36
34
  }
37
35
 
38
36
  /**
39
- * S3 screenshot storage via a backend proxy endpoint.
40
- * NEVER pass AWS credentials to the frontend — your backend holds those.
41
- * Your endpoint receives { commentId, imageData } and returns { url }.
37
+ * AWS S3 backend — all data (comments, replies, screenshots) stored in S3.
38
+ * No Firebase or Supabase needed.
39
+ *
40
+ * AWS credentials MUST stay on your server — this adapter calls your backend API,
41
+ * which in turn reads/writes S3. See src/adapters/s3.ts for the required API contract
42
+ * and a full Node.js/Express reference implementation.
42
43
  */
43
- declare interface S3ScreenshotConfig {
44
+ declare interface S3ProviderConfig {
44
45
  provider: 's3';
45
- /** Your backend endpoint that accepts the screenshot and returns { url: string } */
46
- uploadEndpoint: string;
47
- /** Optional headers (e.g. an authorization token for your own backend) */
46
+ /** Base URL of your backend API (e.g. https://your-api.com/ufp). No trailing slash. */
47
+ apiUrl: string;
48
+ /** Optional headers sent with every request (e.g. { Authorization: 'Bearer token' }). */
48
49
  headers?: Record<string, string>;
50
+ /**
51
+ * How often (ms) to poll for new comments. S3 has no real-time push.
52
+ * Default: 5000 (5 seconds).
53
+ */
54
+ pollInterval?: number;
49
55
  }
50
56
 
51
57
  /**
52
- * Controls where screenshots are stored.
53
- * - `'firebase'` / `'supabase'`: reuse the same provider configured in `database`
54
- * - `'s3'`: forward screenshots to your own backend proxy (AWS credentials stay on your server)
58
+ * Supabase backend — Postgres for comments/replies, Supabase Storage for screenshots.
59
+ * The anonKey is a public key by design. Secure data with Row Level Security (RLS) policies.
60
+ * See src/adapters/supabase.ts for the required SQL setup.
55
61
  */
56
- declare type ScreenshotConfig = {
57
- provider: 'firebase';
58
- } | {
59
- provider: 'supabase';
60
- } | S3ScreenshotConfig;
61
-
62
62
  declare interface SupabaseProviderConfig {
63
63
  provider: 'supabase';
64
64
  url: string;
65
- /**
66
- * Supabase anon key. This is a public key — not a secret.
67
- * Secure your data with Supabase Row Level Security (RLS) policies.
68
- * Store in an environment variable.
69
- */
70
65
  anonKey: string;
71
66
  }
72
67
 
@@ -0,0 +1,66 @@
1
+ var c = Object.defineProperty;
2
+ var l = (r, e, t) => e in r ? c(r, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : r[e] = t;
3
+ var n = (r, e, t) => l(r, typeof e != "symbol" ? e + "" : e, t);
4
+ class d {
5
+ constructor(e, t = {}, s = 5e3) {
6
+ n(this, "base");
7
+ n(this, "headers");
8
+ n(this, "pollInterval");
9
+ this.base = e.replace(/\/$/, ""), this.headers = t, this.pollInterval = s;
10
+ }
11
+ async req(e, t, s) {
12
+ const a = await fetch(`${this.base}${t}`, {
13
+ method: e,
14
+ headers: { "Content-Type": "application/json", ...this.headers },
15
+ body: s !== void 0 ? JSON.stringify(s) : void 0
16
+ });
17
+ if (!a.ok)
18
+ throw new Error(`[ui-feedback-plugin] S3 API ${e} ${t} → ${a.status} ${a.statusText}`);
19
+ if (e !== "DELETE")
20
+ return a.json();
21
+ }
22
+ async addComment(e) {
23
+ return (await this.req("POST", "/comments", e)).id;
24
+ }
25
+ async getComments(e, t) {
26
+ const s = new URLSearchParams({ pageUrl: e, projectKey: t });
27
+ return this.req("GET", `/comments?${s}`);
28
+ }
29
+ async updateComment(e, t) {
30
+ await this.req("PUT", `/comments/${e}`, t);
31
+ }
32
+ async deleteComment(e) {
33
+ await this.req("DELETE", `/comments/${e}`);
34
+ }
35
+ async addReply(e) {
36
+ return (await this.req("POST", "/replies", e)).id;
37
+ }
38
+ async getReplies(e) {
39
+ return this.req("GET", `/replies?commentId=${encodeURIComponent(e)}`);
40
+ }
41
+ subscribeToComments(e, t, s) {
42
+ let a = !0;
43
+ const o = async () => {
44
+ if (a) {
45
+ try {
46
+ const i = await this.getComments(e, t);
47
+ a && s(i);
48
+ } catch (i) {
49
+ console.error("[ui-feedback-plugin] S3 poll error:", i);
50
+ }
51
+ a && setTimeout(o, this.pollInterval);
52
+ }
53
+ };
54
+ return o(), () => {
55
+ a = !1;
56
+ };
57
+ }
58
+ async uploadScreenshot(e, t) {
59
+ const s = t.includes(",") ? t.split(",")[1] : t;
60
+ return (await this.req("POST", "/screenshots", { commentId: e, imageData: s })).url;
61
+ }
62
+ }
63
+ export {
64
+ d as S3Adapter
65
+ };
66
+ //# sourceMappingURL=s3-4V57aJiZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-4V57aJiZ.mjs","sources":["../src/adapters/s3.ts"],"sourcesContent":["/**\n * AWS S3 adapter — stores everything (comments, replies, screenshots) via your backend API.\n * AWS credentials live on YOUR server; this adapter only calls HTTP endpoints.\n *\n * ── Required API contract ─────────────────────────────────────────────────────\n *\n * GET {apiUrl}/comments?pageUrl=...&projectKey=... → FeedbackComment[]\n * POST {apiUrl}/comments → { id: string }\n * PUT {apiUrl}/comments/:id → 200\n * DELETE {apiUrl}/comments/:id → 200\n *\n * GET {apiUrl}/replies?commentId=... → Reply[]\n * POST {apiUrl}/replies → { id: string }\n *\n * POST {apiUrl}/screenshots → { url: string }\n * body: { commentId: string, imageData: string (base64) }\n *\n * ── Minimal Node.js / Express reference server ────────────────────────────────\n *\n * const express = require('express')\n * const { S3Client, PutObjectCommand, GetObjectCommand,\n * DeleteObjectCommand, ListObjectsV2Command } = require('@aws-sdk/client-s3')\n * const { v4: uuidv4 } = require('uuid')\n * const crypto = require('crypto')\n *\n * const s3 = new S3Client({ region: process.env.AWS_REGION })\n * const BUCKET = process.env.S3_BUCKET\n * const app = express()\n * app.use(express.json({ limit: '10mb' }))\n *\n * const hashUrl = (url) => crypto.createHash('md5').update(url).digest('hex')\n *\n * async function s3Get(key) {\n * const r = await s3.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }))\n * return JSON.parse(await r.Body.transformToString())\n * }\n * async function s3Put(key, data, contentType = 'application/json') {\n * const body = typeof data === 'string' ? data : JSON.stringify(data)\n * await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: body, ContentType: contentType }))\n * }\n * async function s3List(prefix) {\n * const r = await s3.send(new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix }))\n * return r.Contents ?? []\n * }\n *\n * // Comments\n * app.get('/ufp/comments', async (req, res) => {\n * const { pageUrl, projectKey } = req.query\n * const prefix = `comments/${projectKey}/${hashUrl(pageUrl)}/`\n * const items = await s3List(prefix)\n * const comments = await Promise.all(items.map(i => s3Get(i.Key)))\n * res.json(comments.sort((a, b) => b.createdAt - a.createdAt))\n * })\n *\n * app.post('/ufp/comments', async (req, res) => {\n * const comment = req.body\n * const id = uuidv4()\n * const key = `comments/${comment.projectKey}/${hashUrl(comment.pageUrl)}/${id}.json`\n * await s3Put(key, { id, ...comment })\n * res.json({ id })\n * })\n *\n * app.put('/ufp/comments/:id', async (req, res) => {\n * // Requires you to store a separate index: id → s3 key\n * // Simplest: scan prefix and find matching id\n * const { id } = req.params\n * const allPrefixes = await s3List('comments/')\n * const match = allPrefixes.find(i => i.Key.endsWith(`/${id}.json`))\n * if (!match) return res.status(404).end()\n * const existing = await s3Get(match.Key)\n * await s3Put(match.Key, { ...existing, ...req.body })\n * res.status(200).end()\n * })\n *\n * app.delete('/ufp/comments/:id', async (req, res) => {\n * const { id } = req.params\n * const allItems = await s3List('comments/')\n * const match = allItems.find(i => i.Key.endsWith(`/${id}.json`))\n * if (match) await s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: match.Key }))\n * // Also delete replies\n * const replies = await s3List(`replies/${id}/`)\n * await Promise.all(replies.map(i => s3.send(new DeleteObjectCommand({ Bucket: BUCKET, Key: i.Key }))))\n * res.status(200).end()\n * })\n *\n * // Replies\n * app.get('/ufp/replies', async (req, res) => {\n * const { commentId } = req.query\n * const items = await s3List(`replies/${commentId}/`)\n * const replies = await Promise.all(items.map(i => s3Get(i.Key)))\n * res.json(replies.sort((a, b) => a.createdAt - b.createdAt))\n * })\n *\n * app.post('/ufp/replies', async (req, res) => {\n * const reply = req.body\n * const id = uuidv4()\n * await s3Put(`replies/${reply.commentId}/${id}.json`, { id, ...reply })\n * res.json({ id })\n * })\n *\n * // Screenshots\n * app.post('/ufp/screenshots', async (req, res) => {\n * const { commentId, imageData } = req.body\n * const buffer = Buffer.from(imageData, 'base64')\n * const key = `screenshots/${commentId}.png`\n * await s3.send(new PutObjectCommand({\n * Bucket: BUCKET, Key: key, Body: buffer,\n * ContentType: 'image/png',\n * }))\n * res.json({ url: `https://${BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}` })\n * })\n *\n * app.listen(3001, () => console.log('UFP S3 API running on :3001'))\n */\nimport type { FeedbackComment, Reply, DatabaseAdapter, ScreenshotAdapter } from '../core/types'\n\nexport class S3Adapter implements DatabaseAdapter, ScreenshotAdapter {\n private base: string\n private headers: Record<string, string>\n private pollInterval: number\n\n constructor(apiUrl: string, headers: Record<string, string> = {}, pollInterval = 5000) {\n this.base = apiUrl.replace(/\\/$/, '')\n this.headers = headers\n this.pollInterval = pollInterval\n }\n\n private async req<T>(method: string, path: string, body?: unknown): Promise<T> {\n const res = await fetch(`${this.base}${path}`, {\n method,\n headers: { 'Content-Type': 'application/json', ...this.headers },\n body: body !== undefined ? JSON.stringify(body) : undefined,\n })\n if (!res.ok) {\n throw new Error(`[ui-feedback-plugin] S3 API ${method} ${path} → ${res.status} ${res.statusText}`)\n }\n // DELETE returns 200 with no body\n if (method === 'DELETE') return undefined as T\n return res.json() as Promise<T>\n }\n\n async addComment(comment: Omit<FeedbackComment, 'id'>): Promise<string> {\n const data = await this.req<{ id: string }>('POST', '/comments', comment)\n return data.id\n }\n\n async getComments(pageUrl: string, projectKey: string): Promise<FeedbackComment[]> {\n const params = new URLSearchParams({ pageUrl, projectKey })\n return this.req<FeedbackComment[]>('GET', `/comments?${params}`)\n }\n\n async updateComment(id: string, data: Partial<FeedbackComment>): Promise<void> {\n await this.req('PUT', `/comments/${id}`, data)\n }\n\n async deleteComment(id: string): Promise<void> {\n await this.req('DELETE', `/comments/${id}`)\n }\n\n async addReply(reply: Omit<Reply, 'id'>): Promise<string> {\n const data = await this.req<{ id: string }>('POST', '/replies', reply)\n return data.id\n }\n\n async getReplies(commentId: string): Promise<Reply[]> {\n return this.req<Reply[]>('GET', `/replies?commentId=${encodeURIComponent(commentId)}`)\n }\n\n subscribeToComments(\n pageUrl: string,\n projectKey: string,\n callback: (comments: FeedbackComment[]) => void\n ): () => void {\n let active = true\n\n const poll = async () => {\n if (!active) return\n try {\n const comments = await this.getComments(pageUrl, projectKey)\n if (active) callback(comments)\n } catch (err) {\n console.error('[ui-feedback-plugin] S3 poll error:', err)\n }\n if (active) setTimeout(poll, this.pollInterval)\n }\n\n poll()\n return () => { active = false }\n }\n\n async uploadScreenshot(commentId: string, dataUrl: string): Promise<string> {\n const imageData = dataUrl.includes(',') ? dataUrl.split(',')[1] : dataUrl\n const data = await this.req<{ url: string }>('POST', '/screenshots', { commentId, imageData })\n return data.url\n }\n}\n"],"names":["S3Adapter","apiUrl","headers","pollInterval","__publicField","method","path","body","res","comment","pageUrl","projectKey","params","id","data","reply","commentId","callback","active","poll","comments","err","dataUrl","imageData"],"mappings":";;;AAoHO,MAAMA,EAAwD;AAAA,EAKnE,YAAYC,GAAgBC,IAAkC,CAAA,GAAIC,IAAe,KAAM;AAJ/E,IAAAC,EAAA;AACA,IAAAA,EAAA;AACA,IAAAA,EAAA;AAGN,SAAK,OAAOH,EAAO,QAAQ,OAAO,EAAE,GACpC,KAAK,UAAUC,GACf,KAAK,eAAeC;AAAA,EACtB;AAAA,EAEA,MAAc,IAAOE,GAAgBC,GAAcC,GAA4B;AAC7E,UAAMC,IAAM,MAAM,MAAM,GAAG,KAAK,IAAI,GAAGF,CAAI,IAAI;AAAA,MAC7C,QAAAD;AAAA,MACA,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,QAAA;AAAA,MACvD,MAAME,MAAS,SAAY,KAAK,UAAUA,CAAI,IAAI;AAAA,IAAA,CACnD;AACD,QAAI,CAACC,EAAI;AACP,YAAM,IAAI,MAAM,+BAA+BH,CAAM,IAAIC,CAAI,MAAME,EAAI,MAAM,IAAIA,EAAI,UAAU,EAAE;AAGnG,QAAIH,MAAW;AACf,aAAOG,EAAI,KAAA;AAAA,EACb;AAAA,EAEA,MAAM,WAAWC,GAAuD;AAEtE,YADa,MAAM,KAAK,IAAoB,QAAQ,aAAaA,CAAO,GAC5D;AAAA,EACd;AAAA,EAEA,MAAM,YAAYC,GAAiBC,GAAgD;AACjF,UAAMC,IAAS,IAAI,gBAAgB,EAAE,SAAAF,GAAS,YAAAC,GAAY;AAC1D,WAAO,KAAK,IAAuB,OAAO,aAAaC,CAAM,EAAE;AAAA,EACjE;AAAA,EAEA,MAAM,cAAcC,GAAYC,GAA+C;AAC7E,UAAM,KAAK,IAAI,OAAO,aAAaD,CAAE,IAAIC,CAAI;AAAA,EAC/C;AAAA,EAEA,MAAM,cAAcD,GAA2B;AAC7C,UAAM,KAAK,IAAI,UAAU,aAAaA,CAAE,EAAE;AAAA,EAC5C;AAAA,EAEA,MAAM,SAASE,GAA2C;AAExD,YADa,MAAM,KAAK,IAAoB,QAAQ,YAAYA,CAAK,GACzD;AAAA,EACd;AAAA,EAEA,MAAM,WAAWC,GAAqC;AACpD,WAAO,KAAK,IAAa,OAAO,sBAAsB,mBAAmBA,CAAS,CAAC,EAAE;AAAA,EACvF;AAAA,EAEA,oBACEN,GACAC,GACAM,GACY;AACZ,QAAIC,IAAS;AAEb,UAAMC,IAAO,YAAY;AACvB,UAAKD,GACL;AAAA,YAAI;AACF,gBAAME,IAAW,MAAM,KAAK,YAAYV,GAASC,CAAU;AAC3D,UAAIO,OAAiBE,CAAQ;AAAA,QAC/B,SAASC,GAAK;AACZ,kBAAQ,MAAM,uCAAuCA,CAAG;AAAA,QAC1D;AACA,QAAIH,KAAQ,WAAWC,GAAM,KAAK,YAAY;AAAA;AAAA,IAChD;AAEA,WAAAA,EAAA,GACO,MAAM;AAAE,MAAAD,IAAS;AAAA,IAAM;AAAA,EAChC;AAAA,EAEA,MAAM,iBAAiBF,GAAmBM,GAAkC;AAC1E,UAAMC,IAAYD,EAAQ,SAAS,GAAG,IAAIA,EAAQ,MAAM,GAAG,EAAE,CAAC,IAAIA;AAElE,YADa,MAAM,KAAK,IAAqB,QAAQ,gBAAgB,EAAE,WAAAN,GAAW,WAAAO,GAAW,GACjF;AAAA,EACd;AACF;"}