@yagejs-tools/feedback 0.0.0 → 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.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +890 -0
- package/dist/gallery.d.ts +2 -0
- package/dist/gallery.js +288 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +1057 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +735 -0
- package/dist/vite.d.ts +12 -0
- package/dist/vite.js +755 -0
- package/package.json +10 -10
package/dist/gallery.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/client/FeedbackClient.ts
|
|
2
|
+
var FeedbackClient = class {
|
|
3
|
+
constructor(server = "http://127.0.0.1:5212", timeoutMs = 3e4) {
|
|
4
|
+
this.timeoutMs = timeoutMs;
|
|
5
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 3e5)
|
|
6
|
+
throw new Error(
|
|
7
|
+
"Feedback request timeout must be an integer from 1 to 300000 ms."
|
|
8
|
+
);
|
|
9
|
+
this.base = new URL(server);
|
|
10
|
+
this.base.pathname = this.base.pathname.replace(/\/?$/, "/");
|
|
11
|
+
if (this.base.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(this.base.hostname) || this.base.username || this.base.password || this.base.search || this.base.hash)
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Feedback server must be a loopback HTTP URL without credentials, query, or fragment."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
timeoutMs;
|
|
17
|
+
base;
|
|
18
|
+
async request(route, body, signal) {
|
|
19
|
+
const timeout = AbortSignal.timeout(this.timeoutMs);
|
|
20
|
+
const response = await fetch(new URL(route.replace(/^\//, ""), this.base), {
|
|
21
|
+
...body !== void 0 ? {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
body: JSON.stringify(body)
|
|
25
|
+
} : {},
|
|
26
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok)
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Feedback server returned ${response.status}: ${await response.text()}`
|
|
31
|
+
);
|
|
32
|
+
return await response.json();
|
|
33
|
+
}
|
|
34
|
+
get galleryUrl() {
|
|
35
|
+
return new URL("gallery/", this.base).href;
|
|
36
|
+
}
|
|
37
|
+
session() {
|
|
38
|
+
return this.request("session");
|
|
39
|
+
}
|
|
40
|
+
list(status) {
|
|
41
|
+
return this.request(`/comments${status ? `?status=${status}` : ""}`);
|
|
42
|
+
}
|
|
43
|
+
show(id) {
|
|
44
|
+
return this.request(`/comments/${encodeURIComponent(id)}`);
|
|
45
|
+
}
|
|
46
|
+
save(upload, signal) {
|
|
47
|
+
return this.request("/comments", upload, signal);
|
|
48
|
+
}
|
|
49
|
+
transition(id, request) {
|
|
50
|
+
return this.request(
|
|
51
|
+
`/comments/${encodeURIComponent(id)}/transitions`,
|
|
52
|
+
request
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/gallery/main.ts
|
|
58
|
+
var get = (id) => {
|
|
59
|
+
const element = document.getElementById(id);
|
|
60
|
+
if (!element) throw new Error(`Missing gallery element: ${id}`);
|
|
61
|
+
return element;
|
|
62
|
+
};
|
|
63
|
+
var api = new URL(
|
|
64
|
+
document.querySelector('meta[name="feedback-api"]').content,
|
|
65
|
+
location.href
|
|
66
|
+
);
|
|
67
|
+
var client = new FeedbackClient(api.href);
|
|
68
|
+
var selected = /* @__PURE__ */ new Set();
|
|
69
|
+
var comments = [];
|
|
70
|
+
var project = "";
|
|
71
|
+
var page = 0;
|
|
72
|
+
var generation = 0;
|
|
73
|
+
var detailGeneration = 0;
|
|
74
|
+
var pageSize = 24;
|
|
75
|
+
var node = (tag, text, className) => {
|
|
76
|
+
const element = document.createElement(tag);
|
|
77
|
+
if (text !== void 0) element.textContent = text;
|
|
78
|
+
if (className) element.className = className;
|
|
79
|
+
return element;
|
|
80
|
+
};
|
|
81
|
+
var message = (text, error = false) => {
|
|
82
|
+
get("message").textContent = text;
|
|
83
|
+
get("message").className = error ? "error" : "";
|
|
84
|
+
};
|
|
85
|
+
function filtered() {
|
|
86
|
+
const status = get("status").value, search = get("search").value.toLowerCase().trim();
|
|
87
|
+
return comments.filter(
|
|
88
|
+
(c) => (status === "all" || (status === "pending" ? ["open", "ingested"].includes(c.status) : c.status === status)) && (!search || `${c.text} ${c.id} ${c.target.kind === "entities" ? c.target.entities.map((e) => e.name).join(" ") : c.target.kind}`.toLowerCase().includes(search))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function selectionChanged() {
|
|
92
|
+
get("codex").disabled = get(
|
|
93
|
+
"claude"
|
|
94
|
+
).disabled = selected.size === 0 || !project;
|
|
95
|
+
get("summary").textContent = `${filtered().length} comments \xB7 ${selected.size} selected. Reading and copying leave status unchanged.`;
|
|
96
|
+
}
|
|
97
|
+
function render() {
|
|
98
|
+
const items = filtered();
|
|
99
|
+
page = Math.min(page, Math.max(0, Math.ceil(items.length / pageSize) - 1));
|
|
100
|
+
get("cards").replaceChildren();
|
|
101
|
+
for (const comment of items.slice(page * pageSize, (page + 1) * pageSize)) {
|
|
102
|
+
const card = node("article", void 0, "card"), preview = node("button", void 0, "preview"), img = node("img");
|
|
103
|
+
preview.setAttribute(
|
|
104
|
+
"aria-label",
|
|
105
|
+
`View evidence: ${comment.text.slice(0, 100)}`
|
|
106
|
+
);
|
|
107
|
+
img.src = new URL(`captures/${comment.captureId}/screenshot.png`, api).href;
|
|
108
|
+
img.alt = "Captured game view";
|
|
109
|
+
img.loading = "lazy";
|
|
110
|
+
preview.append(img);
|
|
111
|
+
preview.onclick = () => {
|
|
112
|
+
void showDetail(comment.id);
|
|
113
|
+
};
|
|
114
|
+
const content = node("div", void 0, "content"), meta = node("div", void 0, "meta"), label = node("label"), checkbox = node("input");
|
|
115
|
+
checkbox.type = "checkbox";
|
|
116
|
+
checkbox.checked = selected.has(comment.id);
|
|
117
|
+
checkbox.setAttribute("aria-label", `Select comment ${comment.id}`);
|
|
118
|
+
checkbox.onchange = () => {
|
|
119
|
+
if (checkbox.checked) selected.add(comment.id);
|
|
120
|
+
else selected.delete(comment.id);
|
|
121
|
+
selectionChanged();
|
|
122
|
+
};
|
|
123
|
+
label.append(
|
|
124
|
+
checkbox,
|
|
125
|
+
node(
|
|
126
|
+
"span",
|
|
127
|
+
comment.target.kind === "global" ? "Whole view" : comment.target.kind === "area" ? "Area" : `${comment.target.entities.length} entities`
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
meta.append(label, node("span", comment.status, "badge"));
|
|
131
|
+
const open = node("button", "View evidence");
|
|
132
|
+
open.onclick = preview.onclick;
|
|
133
|
+
content.append(
|
|
134
|
+
meta,
|
|
135
|
+
node("p", comment.text, "comment"),
|
|
136
|
+
node("p", new Date(comment.created).toLocaleString(), "muted"),
|
|
137
|
+
open
|
|
138
|
+
);
|
|
139
|
+
card.append(preview, content);
|
|
140
|
+
get("cards").append(card);
|
|
141
|
+
}
|
|
142
|
+
if (!items.length)
|
|
143
|
+
get("cards").append(node("p", "No comments match this view.", "muted"));
|
|
144
|
+
get("previous").disabled = page === 0;
|
|
145
|
+
get("next").disabled = (page + 1) * pageSize >= items.length;
|
|
146
|
+
get("page").textContent = `Page ${page + 1} of ${Math.max(1, Math.ceil(items.length / pageSize))}`;
|
|
147
|
+
selectionChanged();
|
|
148
|
+
}
|
|
149
|
+
async function refresh() {
|
|
150
|
+
const current = ++generation;
|
|
151
|
+
get("refresh").disabled = true;
|
|
152
|
+
try {
|
|
153
|
+
const [session, records] = await Promise.all([
|
|
154
|
+
client.session(),
|
|
155
|
+
client.list()
|
|
156
|
+
]);
|
|
157
|
+
if (current !== generation) return;
|
|
158
|
+
project = session.project;
|
|
159
|
+
comments = records.reverse();
|
|
160
|
+
get("project").textContent = project;
|
|
161
|
+
for (const id of selected)
|
|
162
|
+
if (!comments.some((c) => c.id === id)) selected.delete(id);
|
|
163
|
+
render();
|
|
164
|
+
message("");
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (current === generation)
|
|
167
|
+
message(error instanceof Error ? error.message : String(error), true);
|
|
168
|
+
} finally {
|
|
169
|
+
if (current === generation)
|
|
170
|
+
get("refresh").disabled = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function showDetail(id) {
|
|
174
|
+
const current = ++detailGeneration;
|
|
175
|
+
const dialog = get("detail");
|
|
176
|
+
get("evidence").replaceChildren(node("p", "Loading evidence\u2026"));
|
|
177
|
+
if (!dialog.open) dialog.showModal();
|
|
178
|
+
try {
|
|
179
|
+
const data = await client.show(id);
|
|
180
|
+
if (current !== detailGeneration || !dialog.open) return;
|
|
181
|
+
const { comment, capture } = data, layout = node("div", void 0, "detail-grid"), figure = node("div", void 0, "capture"), img = node("img");
|
|
182
|
+
img.src = new URL(data.screenshotUrl, api).href;
|
|
183
|
+
img.alt = `Original capture at frame ${capture.frame}`;
|
|
184
|
+
figure.append(img);
|
|
185
|
+
const boxes = comment.target.kind === "area" ? [comment.target.rect] : comment.target.kind === "entities" ? comment.target.entities.map((e) => e.bounds) : [];
|
|
186
|
+
for (const box of boxes) {
|
|
187
|
+
const outline = node("div", void 0, "outline");
|
|
188
|
+
Object.assign(outline.style, {
|
|
189
|
+
left: `${box.x / capture.width * 100}%`,
|
|
190
|
+
top: `${box.y / capture.height * 100}%`,
|
|
191
|
+
width: `${box.width / capture.width * 100}%`,
|
|
192
|
+
height: `${box.height / capture.height * 100}%`
|
|
193
|
+
});
|
|
194
|
+
figure.append(outline);
|
|
195
|
+
}
|
|
196
|
+
const info = node("section");
|
|
197
|
+
info.append(
|
|
198
|
+
node("span", comment.status, "badge"),
|
|
199
|
+
node("p", comment.text, "comment"),
|
|
200
|
+
node(
|
|
201
|
+
"p",
|
|
202
|
+
`Frame ${capture.frame} \xB7 ${capture.width} \xD7 ${capture.height}`
|
|
203
|
+
),
|
|
204
|
+
node("p", new Date(capture.created).toLocaleString(), "muted")
|
|
205
|
+
);
|
|
206
|
+
for (const [title, value] of [
|
|
207
|
+
["Target", comment.target],
|
|
208
|
+
["Host context", capture.context],
|
|
209
|
+
["Inspector snapshot", capture.snapshot],
|
|
210
|
+
["Status history", comment.history]
|
|
211
|
+
]) {
|
|
212
|
+
const block = node("details");
|
|
213
|
+
block.append(
|
|
214
|
+
node("summary", title),
|
|
215
|
+
node("pre", JSON.stringify(value, null, 2))
|
|
216
|
+
);
|
|
217
|
+
info.append(block);
|
|
218
|
+
}
|
|
219
|
+
info.append(node("p", `Comment ${comment.id}`, "muted"));
|
|
220
|
+
layout.append(figure, info);
|
|
221
|
+
get("evidence").replaceChildren(layout);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (current === detailGeneration)
|
|
224
|
+
get("evidence").replaceChildren(
|
|
225
|
+
node(
|
|
226
|
+
"p",
|
|
227
|
+
error instanceof Error ? error.message : String(error),
|
|
228
|
+
"error"
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function copy(agent) {
|
|
234
|
+
const ids = [...selected];
|
|
235
|
+
if (!ids.length || !project) return;
|
|
236
|
+
const instruction = `${agent === "claude" ? "/yage-feedback" : "$yage-feedback"} Address these feedback comments: ${ids.join(", ")}.
|
|
237
|
+
Project: ${JSON.stringify(project)}
|
|
238
|
+
Feedback server: ${api.href}
|
|
239
|
+
Inspect each comment's screenshot and inspector snapshot, then follow the skill's acknowledgement and verification workflow. Limit this batch to the listed IDs.`;
|
|
240
|
+
try {
|
|
241
|
+
await navigator.clipboard.writeText(instruction);
|
|
242
|
+
message(
|
|
243
|
+
`Copied ${ids.length} comments for ${agent === "claude" ? "Claude" : "Codex"}. Paste into a session opened in this project.`
|
|
244
|
+
);
|
|
245
|
+
} catch {
|
|
246
|
+
get("instruction").value = instruction;
|
|
247
|
+
get("copy").showModal();
|
|
248
|
+
get("instruction").focus();
|
|
249
|
+
get("instruction").select();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
get("refresh").onclick = () => {
|
|
253
|
+
void refresh();
|
|
254
|
+
};
|
|
255
|
+
for (const id of ["status", "search"])
|
|
256
|
+
get(id).addEventListener(id === "search" ? "input" : "change", () => {
|
|
257
|
+
selected.clear();
|
|
258
|
+
page = 0;
|
|
259
|
+
render();
|
|
260
|
+
});
|
|
261
|
+
get("previous").onclick = () => {
|
|
262
|
+
page--;
|
|
263
|
+
render();
|
|
264
|
+
};
|
|
265
|
+
get("next").onclick = () => {
|
|
266
|
+
page++;
|
|
267
|
+
render();
|
|
268
|
+
};
|
|
269
|
+
get("select-page").onclick = () => {
|
|
270
|
+
filtered().slice(page * pageSize, (page + 1) * pageSize).forEach((c) => selected.add(c.id));
|
|
271
|
+
render();
|
|
272
|
+
};
|
|
273
|
+
get("clear").onclick = () => {
|
|
274
|
+
selected.clear();
|
|
275
|
+
render();
|
|
276
|
+
};
|
|
277
|
+
get("codex").onclick = () => {
|
|
278
|
+
void copy("codex");
|
|
279
|
+
};
|
|
280
|
+
get("claude").onclick = () => {
|
|
281
|
+
void copy("claude");
|
|
282
|
+
};
|
|
283
|
+
get("close-detail").onclick = () => get("detail").close();
|
|
284
|
+
get("close-copy").onclick = () => get("copy").close();
|
|
285
|
+
get("detail").addEventListener("close", () => {
|
|
286
|
+
detailGeneration++;
|
|
287
|
+
});
|
|
288
|
+
void refresh();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Plugin, EngineContext } from '@yagejs/core';
|
|
2
|
+
|
|
3
|
+
/** Physical KeyboardEvent.code plus exact modifiers; omitted modifiers are false. */
|
|
4
|
+
interface FeedbackShortcut {
|
|
5
|
+
code: string;
|
|
6
|
+
ctrl?: boolean;
|
|
7
|
+
alt?: boolean;
|
|
8
|
+
shift?: boolean;
|
|
9
|
+
meta?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface FeedbackShortcuts {
|
|
12
|
+
/** Omitted bindings retain their defaults; false disables this action's key. */
|
|
13
|
+
feedback?: FeedbackShortcut | false;
|
|
14
|
+
freeze?: FeedbackShortcut | false;
|
|
15
|
+
stepFrame?: FeedbackShortcut | false;
|
|
16
|
+
stepTenFrames?: FeedbackShortcut | false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type Json = null | boolean | number | string | Json[] | {
|
|
20
|
+
[key: string]: Json;
|
|
21
|
+
};
|
|
22
|
+
interface Rect {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
interface FeedbackEntity {
|
|
29
|
+
sceneId: string;
|
|
30
|
+
id: string;
|
|
31
|
+
generation: number;
|
|
32
|
+
name: string;
|
|
33
|
+
bounds: Rect;
|
|
34
|
+
}
|
|
35
|
+
type FeedbackTarget = {
|
|
36
|
+
kind: "global";
|
|
37
|
+
} | {
|
|
38
|
+
kind: "entities";
|
|
39
|
+
entities: FeedbackEntity[];
|
|
40
|
+
} | {
|
|
41
|
+
kind: "area";
|
|
42
|
+
rect: Rect;
|
|
43
|
+
};
|
|
44
|
+
interface FeedbackCapture {
|
|
45
|
+
version: 1;
|
|
46
|
+
id: string;
|
|
47
|
+
sessionId: string;
|
|
48
|
+
created: string;
|
|
49
|
+
frame: number;
|
|
50
|
+
url: string;
|
|
51
|
+
width: number;
|
|
52
|
+
height: number;
|
|
53
|
+
context: Json;
|
|
54
|
+
snapshot: Json;
|
|
55
|
+
entities: FeedbackEntity[];
|
|
56
|
+
}
|
|
57
|
+
interface AuthoredComment {
|
|
58
|
+
id: string;
|
|
59
|
+
captureId: string;
|
|
60
|
+
text: string;
|
|
61
|
+
created: string;
|
|
62
|
+
target: FeedbackTarget;
|
|
63
|
+
status: "open";
|
|
64
|
+
}
|
|
65
|
+
type FeedbackStatus = "open" | "ingested" | "addressed" | "resolved";
|
|
66
|
+
type FeedbackAction = "ingest" | "address" | "resolve" | "reopen";
|
|
67
|
+
interface FeedbackTransition {
|
|
68
|
+
requestId: string;
|
|
69
|
+
expectedRevision: number;
|
|
70
|
+
action: FeedbackAction;
|
|
71
|
+
actor: string;
|
|
72
|
+
note?: string;
|
|
73
|
+
}
|
|
74
|
+
interface FeedbackHistoryEntry extends FeedbackTransition {
|
|
75
|
+
revision: number;
|
|
76
|
+
created: string;
|
|
77
|
+
from: FeedbackStatus;
|
|
78
|
+
to: FeedbackStatus;
|
|
79
|
+
}
|
|
80
|
+
interface FeedbackComment extends Omit<AuthoredComment, "status"> {
|
|
81
|
+
status: FeedbackStatus;
|
|
82
|
+
revision: number;
|
|
83
|
+
history: FeedbackHistoryEntry[];
|
|
84
|
+
}
|
|
85
|
+
interface FeedbackDetail {
|
|
86
|
+
comment: FeedbackComment;
|
|
87
|
+
capture: FeedbackCapture;
|
|
88
|
+
screenshotPath: string;
|
|
89
|
+
screenshotUrl: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface FeedbackOptions {
|
|
93
|
+
/** Use the same debug flag supplied to Engine. Disabled plugins mount no UI. */
|
|
94
|
+
enabled: boolean;
|
|
95
|
+
/** Full API base URL. Defaults to dev-server metadata, then localhost:5212. */
|
|
96
|
+
server?: string;
|
|
97
|
+
/** Defaults to F8/F9 for feedback/freeze and F10/Shift+F10 for stepping. Override individual bindings or pass false to disable all. */
|
|
98
|
+
shortcuts?: boolean | FeedbackShortcuts;
|
|
99
|
+
context?: () => Json;
|
|
100
|
+
}
|
|
101
|
+
/** Composes the runtime adapter, session, transport, and DOM interface. */
|
|
102
|
+
declare class FeedbackPlugin implements Plugin {
|
|
103
|
+
private readonly options;
|
|
104
|
+
readonly name = "feedback";
|
|
105
|
+
readonly version = "0.0.0";
|
|
106
|
+
readonly dependencies: readonly ["renderer", "debug"];
|
|
107
|
+
private context;
|
|
108
|
+
private panel;
|
|
109
|
+
private readonly client;
|
|
110
|
+
private readonly shortcuts;
|
|
111
|
+
constructor(options: FeedbackOptions);
|
|
112
|
+
install(context: EngineContext): void;
|
|
113
|
+
onStart(): void;
|
|
114
|
+
onDestroy(): void;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { type AuthoredComment, type FeedbackAction, type FeedbackCapture, type FeedbackComment, type FeedbackDetail, type FeedbackEntity, type FeedbackHistoryEntry, type FeedbackOptions, FeedbackPlugin, type FeedbackShortcut, type FeedbackShortcuts, type FeedbackStatus, type FeedbackTarget, type FeedbackTransition, type Json, type Rect };
|