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 +263 -0
- package/dist/jat-feedback.js +527 -19
- package/dist/jat-feedback.mjs +10587 -3500
- package/package.json +4 -2
- package/supabase/migrations/1.8.0_add_agent_notes.sql +66 -0
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
|
|