jat-feedback 1.7.0 → 2.0.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
@@ -32,6 +32,269 @@ The package includes the widget bundle, Supabase migration, and edge function
32
32
  | `user-role` | No | `''` | User's role (e.g., `admin`, `user`) |
33
33
  | `org-id` | No | `''` | Organization/tenant ID |
34
34
  | `org-name` | No | `''` | Organization name |
35
+ | `agent-proxy` | No | `''` | URL path for the LLM proxy endpoint (e.g., `'/api/feedback/agent'`). Enables the Agent tab. |
36
+ | `agent-model` | No | `'claude-sonnet-4-6'` | LLM model identifier passed to the proxy endpoint |
37
+ | `agent-context` | No | `''` | Static app context injected into Agent system prompt (describe your app, key pages, nav) |
38
+
39
+ ## Agent Tab: LLM Proxy Endpoint
40
+
41
+ The Agent tab lets users control the host page with natural language commands (powered by [page-agent](https://github.com/alibaba/page-agent)). To use it, set `agent-proxy` to a URL on your server that forwards LLM API calls. **API keys stay server-side — the widget never sees them.**
42
+
43
+ ### How It Works
44
+
45
+ ```
46
+ Widget (browser) Your Server LLM Provider
47
+ ───────────────── ──────────── ────────────
48
+ User types command
49
+ → page-agent builds prompt
50
+ → POST /api/feedback/agent ────→ Receives request
51
+ Adds API key from env
52
+ POST api.anthropic.com ──────→ Processes request
53
+ ← Response ◄──────────────────── Returns completion
54
+ ← JSON response ◄──────────────
55
+ → Agent executes action on page
56
+ ```
57
+
58
+ ### Proxy Endpoint Spec
59
+
60
+ Your server implements a single endpoint:
61
+
62
+ - **Method:** `POST`
63
+ - **Path:** Whatever you set in `agent-proxy` (e.g., `/api/feedback/agent`)
64
+ - **Request body:** OpenAI-compatible chat completion request
65
+
66
+ ```json
67
+ {
68
+ "model": "claude-sonnet-4-6",
69
+ "messages": [
70
+ { "role": "system", "content": "..." },
71
+ { "role": "user", "content": "Click the login button" }
72
+ ],
73
+ "tools": [...]
74
+ }
75
+ ```
76
+
77
+ - **Response:** OpenAI-compatible chat completion response
78
+ - **Auth:** Your endpoint adds the API key server-side — the widget sends no auth headers
79
+
80
+ ### SvelteKit Example
81
+
82
+ Create `src/routes/api/feedback/agent/[...path]/+server.ts`:
83
+
84
+ ```typescript
85
+ import { json, error } from '@sveltejs/kit';
86
+ import type { RequestHandler } from './$types';
87
+ import { ANTHROPIC_API_KEY } from '$env/static/private';
88
+
89
+ export const POST: RequestHandler = async ({ request, params }) => {
90
+ const path = params.path || 'chat/completions';
91
+ const body = await request.json();
92
+
93
+ const response = await fetch(`https://api.anthropic.com/v1/${path}`, {
94
+ method: 'POST',
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ 'x-api-key': ANTHROPIC_API_KEY,
98
+ 'anthropic-version': '2023-06-01',
99
+ },
100
+ body: JSON.stringify(body),
101
+ });
102
+
103
+ if (!response.ok) {
104
+ const detail = await response.text().catch(() => '');
105
+ throw error(response.status, `LLM API error: ${detail.slice(0, 200)}`);
106
+ }
107
+
108
+ const data = await response.json();
109
+ return json(data);
110
+ };
111
+ ```
112
+
113
+ **Environment variable** (`.env`):
114
+ ```
115
+ ANTHROPIC_API_KEY=sk-ant-...
116
+ ```
117
+
118
+ ### Widget Usage
119
+
120
+ ```html
121
+ <jat-feedback
122
+ endpoint="http://localhost:5173"
123
+ project="my-app"
124
+ agent-proxy="/api/feedback/agent"
125
+ agent-model="claude-sonnet-4-6"
126
+ ></jat-feedback>
127
+ ```
128
+
129
+ The Agent tab appears automatically when `agent-proxy` is set. Without it, only the feedback form and history tabs are shown.
130
+
131
+ ### Error Handling
132
+
133
+ The widget handles proxy errors gracefully:
134
+
135
+ | HTTP Status | User Message |
136
+ |-------------|-------------|
137
+ | 401 / 403 | "Check that the server has a valid API key configured" |
138
+ | 429 | "Too many requests — wait a moment and try again" |
139
+ | 500+ | Server error with detail from response body |
140
+ | Timeout (60s) | "The server may be overloaded — try again" |
141
+ | Network error | "Check that your server is running" |
142
+
143
+ ## Agent Notes: CRUD Endpoints
144
+
145
+ Agent notes store user-written markdown context that gets injected into the page-agent's system prompt. Notes have two scopes: **site-wide** (`route` is `null`) and **per-route** (keyed by URL pathname). Requires the `1.8.0_add_agent_notes.sql` migration.
146
+
147
+ ### Endpoints
148
+
149
+ | Method | Path | Description |
150
+ |--------|------|-------------|
151
+ | `GET` | `/api/feedback/notes?project=X` | List all notes for a project |
152
+ | `GET` | `/api/feedback/notes?project=X&route=/path` | Get note for a specific route |
153
+ | `PUT` | `/api/feedback/notes` | Create or update a note (upsert by project+route) |
154
+ | `DELETE` | `/api/feedback/notes/:id` | Delete a note |
155
+
156
+ ### SvelteKit Example
157
+
158
+ Create `src/routes/api/feedback/notes/+server.ts`:
159
+
160
+ ```typescript
161
+ import { json } from '@sveltejs/kit';
162
+ import type { RequestHandler } from './$types';
163
+
164
+ const CORS_HEADERS = {
165
+ 'Access-Control-Allow-Origin': '*',
166
+ 'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
167
+ 'Access-Control-Allow-Headers': 'Content-Type',
168
+ 'Access-Control-Max-Age': '86400'
169
+ };
170
+
171
+ export const OPTIONS: RequestHandler = async () => {
172
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
173
+ };
174
+
175
+ // GET /api/feedback/notes?project=X[&route=/path]
176
+ export const GET: RequestHandler = async ({ url, locals }) => {
177
+ const project = url.searchParams.get('project');
178
+ if (!project) {
179
+ return json({ error: 'project parameter is required' }, { status: 400, headers: CORS_HEADERS });
180
+ }
181
+
182
+ const supabase = locals.supabaseServiceRole;
183
+ let query = supabase.from('agent_notes').select('*').eq('project', project);
184
+
185
+ const route = url.searchParams.get('route');
186
+ if (route !== null) {
187
+ query = query.eq('route', route);
188
+ }
189
+
190
+ const { data, error } = await query.order('updated_at', { ascending: false });
191
+
192
+ if (error) {
193
+ return json({ error: error.message }, { status: 500, headers: CORS_HEADERS });
194
+ }
195
+
196
+ return json({ notes: data }, { headers: CORS_HEADERS });
197
+ };
198
+
199
+ // PUT /api/feedback/notes — upsert by project + route
200
+ export const PUT: RequestHandler = async ({ request, locals }) => {
201
+ const body = await request.json();
202
+
203
+ if (!body.project || typeof body.project !== 'string') {
204
+ return json({ error: 'project is required' }, { status: 400, headers: CORS_HEADERS });
205
+ }
206
+
207
+ const supabase = locals.supabaseServiceRole;
208
+ const route = body.route ?? null;
209
+
210
+ // Check if note already exists for this project+route
211
+ let query = supabase.from('agent_notes').select('id').eq('project', body.project);
212
+ if (route === null) {
213
+ query = query.is('route', null);
214
+ } else {
215
+ query = query.eq('route', route);
216
+ }
217
+ const { data: existing } = await query.maybeSingle();
218
+
219
+ let data, error;
220
+ if (existing) {
221
+ // Update existing note
222
+ ({ data, error } = await supabase
223
+ .from('agent_notes')
224
+ .update({ title: body.title ?? '', content: body.content ?? '' })
225
+ .eq('id', existing.id)
226
+ .select()
227
+ .single());
228
+ } else {
229
+ // Insert new note
230
+ ({ data, error } = await supabase
231
+ .from('agent_notes')
232
+ .insert({ project: body.project, route, title: body.title ?? '', content: body.content ?? '' })
233
+ .select()
234
+ .single());
235
+ }
236
+
237
+ if (error) {
238
+ return json({ error: error.message }, { status: 500, headers: CORS_HEADERS });
239
+ }
240
+
241
+ return json({ ok: true, note: data }, { status: existing ? 200 : 201, headers: CORS_HEADERS });
242
+ };
243
+ ```
244
+
245
+ Create `src/routes/api/feedback/notes/[id]/+server.ts`:
246
+
247
+ ```typescript
248
+ import { json } from '@sveltejs/kit';
249
+ import type { RequestHandler } from './$types';
250
+
251
+ const CORS_HEADERS = {
252
+ 'Access-Control-Allow-Origin': '*',
253
+ 'Access-Control-Allow-Methods': 'DELETE, OPTIONS',
254
+ 'Access-Control-Allow-Headers': 'Content-Type',
255
+ 'Access-Control-Max-Age': '86400'
256
+ };
257
+
258
+ export const OPTIONS: RequestHandler = async () => {
259
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
260
+ };
261
+
262
+ // DELETE /api/feedback/notes/:id
263
+ export const DELETE: RequestHandler = async ({ params, locals }) => {
264
+ const supabase = locals.supabaseServiceRole;
265
+
266
+ const { error } = await supabase
267
+ .from('agent_notes')
268
+ .delete()
269
+ .eq('id', params.id);
270
+
271
+ if (error) {
272
+ return json({ error: error.message }, { status: 500, headers: CORS_HEADERS });
273
+ }
274
+
275
+ return json({ ok: true }, { headers: CORS_HEADERS });
276
+ };
277
+ ```
278
+
279
+ ### Upsert Behavior
280
+
281
+ The `PUT` endpoint checks for an existing note matching `project` + `route`, then inserts or updates accordingly. The database has a unique index on `(project, COALESCE(route, ''))` as a safety net.
282
+
283
+ - If no note exists for the given `project` + `route` → creates a new note (returns `201`)
284
+ - If a note already exists for that combination → updates `title`, `content`, and `updated_at` (returns `200`)
285
+ - Site-wide notes use `route: null` (only one per project)
286
+ - Per-route notes use the URL pathname (e.g., `route: "/invoices"`)
287
+
288
+ ### CORS
289
+
290
+ All endpoints include permissive CORS headers (`Access-Control-Allow-Origin: *`) so the widget can call them cross-origin. For production, restrict the origin to your widget's domain:
291
+
292
+ ```typescript
293
+ const CORS_HEADERS = {
294
+ 'Access-Control-Allow-Origin': 'https://your-app.com',
295
+ // ...
296
+ };
297
+ ```
35
298
 
36
299
  ## What the Widget Captures
37
300