@demon-utils/playwright 0.1.5 → 0.1.6
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/bin/demon-demo-review.js +549 -67
- package/dist/bin/demon-demo-review.js.map +7 -6
- package/dist/index.js +911 -63
- package/dist/index.js.map +8 -5
- package/package.json +1 -1
- package/src/bin/demon-demo-review.ts +74 -9
- package/src/git-context.test.ts +90 -0
- package/src/git-context.ts +62 -0
- package/src/html-generator.e2e.test.ts +349 -0
- package/src/html-generator.test.ts +380 -14
- package/src/html-generator.ts +342 -33
- package/src/index.ts +9 -3
- package/src/recorder.test.ts +161 -0
- package/src/recorder.ts +74 -0
- package/src/review-types.ts +19 -1
- package/src/review.test.ts +257 -59
- package/src/review.ts +147 -31
package/dist/index.js
CHANGED
|
@@ -1,3 +1,377 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
7
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
8
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
9
|
+
for (let key of __getOwnPropNames(mod))
|
|
10
|
+
if (!__hasOwnProp.call(to, key))
|
|
11
|
+
__defProp(to, key, {
|
|
12
|
+
get: () => mod[key],
|
|
13
|
+
enumerable: true
|
|
14
|
+
});
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, {
|
|
20
|
+
get: all[name],
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
set: (newValue) => all[name] = () => newValue
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
27
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
28
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
29
|
+
}) : x)(function(x) {
|
|
30
|
+
if (typeof require !== "undefined")
|
|
31
|
+
return require.apply(this, arguments);
|
|
32
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// node:path
|
|
36
|
+
var exports_path = {};
|
|
37
|
+
__export(exports_path, {
|
|
38
|
+
sep: () => sep,
|
|
39
|
+
resolve: () => resolve,
|
|
40
|
+
relative: () => relative,
|
|
41
|
+
posix: () => posix,
|
|
42
|
+
parse: () => parse,
|
|
43
|
+
normalize: () => normalize,
|
|
44
|
+
join: () => join,
|
|
45
|
+
isAbsolute: () => isAbsolute,
|
|
46
|
+
format: () => format,
|
|
47
|
+
extname: () => extname,
|
|
48
|
+
dirname: () => dirname,
|
|
49
|
+
delimiter: () => delimiter,
|
|
50
|
+
default: () => path_default,
|
|
51
|
+
basename: () => basename,
|
|
52
|
+
_makeLong: () => _makeLong
|
|
53
|
+
});
|
|
54
|
+
function assertPath(path) {
|
|
55
|
+
if (typeof path !== "string")
|
|
56
|
+
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
|
57
|
+
}
|
|
58
|
+
function normalizeStringPosix(path, allowAboveRoot) {
|
|
59
|
+
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
|
60
|
+
for (var i = 0;i <= path.length; ++i) {
|
|
61
|
+
if (i < path.length)
|
|
62
|
+
code = path.charCodeAt(i);
|
|
63
|
+
else if (code === 47)
|
|
64
|
+
break;
|
|
65
|
+
else
|
|
66
|
+
code = 47;
|
|
67
|
+
if (code === 47) {
|
|
68
|
+
if (lastSlash === i - 1 || dots === 1)
|
|
69
|
+
;
|
|
70
|
+
else if (lastSlash !== i - 1 && dots === 2) {
|
|
71
|
+
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
|
72
|
+
if (res.length > 2) {
|
|
73
|
+
var lastSlashIndex = res.lastIndexOf("/");
|
|
74
|
+
if (lastSlashIndex !== res.length - 1) {
|
|
75
|
+
if (lastSlashIndex === -1)
|
|
76
|
+
res = "", lastSegmentLength = 0;
|
|
77
|
+
else
|
|
78
|
+
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
79
|
+
lastSlash = i, dots = 0;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
} else if (res.length === 2 || res.length === 1) {
|
|
83
|
+
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (allowAboveRoot) {
|
|
88
|
+
if (res.length > 0)
|
|
89
|
+
res += "/..";
|
|
90
|
+
else
|
|
91
|
+
res = "..";
|
|
92
|
+
lastSegmentLength = 2;
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
if (res.length > 0)
|
|
96
|
+
res += "/" + path.slice(lastSlash + 1, i);
|
|
97
|
+
else
|
|
98
|
+
res = path.slice(lastSlash + 1, i);
|
|
99
|
+
lastSegmentLength = i - lastSlash - 1;
|
|
100
|
+
}
|
|
101
|
+
lastSlash = i, dots = 0;
|
|
102
|
+
} else if (code === 46 && dots !== -1)
|
|
103
|
+
++dots;
|
|
104
|
+
else
|
|
105
|
+
dots = -1;
|
|
106
|
+
}
|
|
107
|
+
return res;
|
|
108
|
+
}
|
|
109
|
+
function _format(sep, pathObject) {
|
|
110
|
+
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
|
111
|
+
if (!dir)
|
|
112
|
+
return base;
|
|
113
|
+
if (dir === pathObject.root)
|
|
114
|
+
return dir + base;
|
|
115
|
+
return dir + sep + base;
|
|
116
|
+
}
|
|
117
|
+
function resolve() {
|
|
118
|
+
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
|
119
|
+
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
|
120
|
+
var path;
|
|
121
|
+
if (i >= 0)
|
|
122
|
+
path = arguments[i];
|
|
123
|
+
else {
|
|
124
|
+
if (cwd === undefined)
|
|
125
|
+
cwd = process.cwd();
|
|
126
|
+
path = cwd;
|
|
127
|
+
}
|
|
128
|
+
if (assertPath(path), path.length === 0)
|
|
129
|
+
continue;
|
|
130
|
+
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
|
131
|
+
}
|
|
132
|
+
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
|
133
|
+
if (resolvedPath.length > 0)
|
|
134
|
+
return "/" + resolvedPath;
|
|
135
|
+
else
|
|
136
|
+
return "/";
|
|
137
|
+
else if (resolvedPath.length > 0)
|
|
138
|
+
return resolvedPath;
|
|
139
|
+
else
|
|
140
|
+
return ".";
|
|
141
|
+
}
|
|
142
|
+
function normalize(path) {
|
|
143
|
+
if (assertPath(path), path.length === 0)
|
|
144
|
+
return ".";
|
|
145
|
+
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
|
146
|
+
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
|
147
|
+
path = ".";
|
|
148
|
+
if (path.length > 0 && trailingSeparator)
|
|
149
|
+
path += "/";
|
|
150
|
+
if (isAbsolute)
|
|
151
|
+
return "/" + path;
|
|
152
|
+
return path;
|
|
153
|
+
}
|
|
154
|
+
function isAbsolute(path) {
|
|
155
|
+
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
|
156
|
+
}
|
|
157
|
+
function join() {
|
|
158
|
+
if (arguments.length === 0)
|
|
159
|
+
return ".";
|
|
160
|
+
var joined;
|
|
161
|
+
for (var i = 0;i < arguments.length; ++i) {
|
|
162
|
+
var arg = arguments[i];
|
|
163
|
+
if (assertPath(arg), arg.length > 0)
|
|
164
|
+
if (joined === undefined)
|
|
165
|
+
joined = arg;
|
|
166
|
+
else
|
|
167
|
+
joined += "/" + arg;
|
|
168
|
+
}
|
|
169
|
+
if (joined === undefined)
|
|
170
|
+
return ".";
|
|
171
|
+
return normalize(joined);
|
|
172
|
+
}
|
|
173
|
+
function relative(from, to) {
|
|
174
|
+
if (assertPath(from), assertPath(to), from === to)
|
|
175
|
+
return "";
|
|
176
|
+
if (from = resolve(from), to = resolve(to), from === to)
|
|
177
|
+
return "";
|
|
178
|
+
var fromStart = 1;
|
|
179
|
+
for (;fromStart < from.length; ++fromStart)
|
|
180
|
+
if (from.charCodeAt(fromStart) !== 47)
|
|
181
|
+
break;
|
|
182
|
+
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
|
183
|
+
for (;toStart < to.length; ++toStart)
|
|
184
|
+
if (to.charCodeAt(toStart) !== 47)
|
|
185
|
+
break;
|
|
186
|
+
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
|
187
|
+
for (;i <= length; ++i) {
|
|
188
|
+
if (i === length) {
|
|
189
|
+
if (toLen > length) {
|
|
190
|
+
if (to.charCodeAt(toStart + i) === 47)
|
|
191
|
+
return to.slice(toStart + i + 1);
|
|
192
|
+
else if (i === 0)
|
|
193
|
+
return to.slice(toStart + i);
|
|
194
|
+
} else if (fromLen > length) {
|
|
195
|
+
if (from.charCodeAt(fromStart + i) === 47)
|
|
196
|
+
lastCommonSep = i;
|
|
197
|
+
else if (i === 0)
|
|
198
|
+
lastCommonSep = 0;
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
|
203
|
+
if (fromCode !== toCode)
|
|
204
|
+
break;
|
|
205
|
+
else if (fromCode === 47)
|
|
206
|
+
lastCommonSep = i;
|
|
207
|
+
}
|
|
208
|
+
var out = "";
|
|
209
|
+
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
|
210
|
+
if (i === fromEnd || from.charCodeAt(i) === 47)
|
|
211
|
+
if (out.length === 0)
|
|
212
|
+
out += "..";
|
|
213
|
+
else
|
|
214
|
+
out += "/..";
|
|
215
|
+
if (out.length > 0)
|
|
216
|
+
return out + to.slice(toStart + lastCommonSep);
|
|
217
|
+
else {
|
|
218
|
+
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
|
219
|
+
++toStart;
|
|
220
|
+
return to.slice(toStart);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function _makeLong(path) {
|
|
224
|
+
return path;
|
|
225
|
+
}
|
|
226
|
+
function dirname(path) {
|
|
227
|
+
if (assertPath(path), path.length === 0)
|
|
228
|
+
return ".";
|
|
229
|
+
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
|
230
|
+
for (var i = path.length - 1;i >= 1; --i)
|
|
231
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
232
|
+
if (!matchedSlash) {
|
|
233
|
+
end = i;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
} else
|
|
237
|
+
matchedSlash = false;
|
|
238
|
+
if (end === -1)
|
|
239
|
+
return hasRoot ? "/" : ".";
|
|
240
|
+
if (hasRoot && end === 1)
|
|
241
|
+
return "//";
|
|
242
|
+
return path.slice(0, end);
|
|
243
|
+
}
|
|
244
|
+
function basename(path, ext) {
|
|
245
|
+
if (ext !== undefined && typeof ext !== "string")
|
|
246
|
+
throw TypeError('"ext" argument must be a string');
|
|
247
|
+
assertPath(path);
|
|
248
|
+
var start = 0, end = -1, matchedSlash = true, i;
|
|
249
|
+
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
|
250
|
+
if (ext.length === path.length && ext === path)
|
|
251
|
+
return "";
|
|
252
|
+
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
|
253
|
+
for (i = path.length - 1;i >= 0; --i) {
|
|
254
|
+
var code = path.charCodeAt(i);
|
|
255
|
+
if (code === 47) {
|
|
256
|
+
if (!matchedSlash) {
|
|
257
|
+
start = i + 1;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
if (firstNonSlashEnd === -1)
|
|
262
|
+
matchedSlash = false, firstNonSlashEnd = i + 1;
|
|
263
|
+
if (extIdx >= 0)
|
|
264
|
+
if (code === ext.charCodeAt(extIdx)) {
|
|
265
|
+
if (--extIdx === -1)
|
|
266
|
+
end = i;
|
|
267
|
+
} else
|
|
268
|
+
extIdx = -1, end = firstNonSlashEnd;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (start === end)
|
|
272
|
+
end = firstNonSlashEnd;
|
|
273
|
+
else if (end === -1)
|
|
274
|
+
end = path.length;
|
|
275
|
+
return path.slice(start, end);
|
|
276
|
+
} else {
|
|
277
|
+
for (i = path.length - 1;i >= 0; --i)
|
|
278
|
+
if (path.charCodeAt(i) === 47) {
|
|
279
|
+
if (!matchedSlash) {
|
|
280
|
+
start = i + 1;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
} else if (end === -1)
|
|
284
|
+
matchedSlash = false, end = i + 1;
|
|
285
|
+
if (end === -1)
|
|
286
|
+
return "";
|
|
287
|
+
return path.slice(start, end);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function extname(path) {
|
|
291
|
+
assertPath(path);
|
|
292
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
|
293
|
+
for (var i = path.length - 1;i >= 0; --i) {
|
|
294
|
+
var code = path.charCodeAt(i);
|
|
295
|
+
if (code === 47) {
|
|
296
|
+
if (!matchedSlash) {
|
|
297
|
+
startPart = i + 1;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (end === -1)
|
|
303
|
+
matchedSlash = false, end = i + 1;
|
|
304
|
+
if (code === 46) {
|
|
305
|
+
if (startDot === -1)
|
|
306
|
+
startDot = i;
|
|
307
|
+
else if (preDotState !== 1)
|
|
308
|
+
preDotState = 1;
|
|
309
|
+
} else if (startDot !== -1)
|
|
310
|
+
preDotState = -1;
|
|
311
|
+
}
|
|
312
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
|
313
|
+
return "";
|
|
314
|
+
return path.slice(startDot, end);
|
|
315
|
+
}
|
|
316
|
+
function format(pathObject) {
|
|
317
|
+
if (pathObject === null || typeof pathObject !== "object")
|
|
318
|
+
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
|
319
|
+
return _format("/", pathObject);
|
|
320
|
+
}
|
|
321
|
+
function parse(path) {
|
|
322
|
+
assertPath(path);
|
|
323
|
+
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
|
324
|
+
if (path.length === 0)
|
|
325
|
+
return ret;
|
|
326
|
+
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
|
327
|
+
if (isAbsolute2)
|
|
328
|
+
ret.root = "/", start = 1;
|
|
329
|
+
else
|
|
330
|
+
start = 0;
|
|
331
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
|
332
|
+
for (;i >= start; --i) {
|
|
333
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
334
|
+
if (!matchedSlash) {
|
|
335
|
+
startPart = i + 1;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (end === -1)
|
|
341
|
+
matchedSlash = false, end = i + 1;
|
|
342
|
+
if (code === 46) {
|
|
343
|
+
if (startDot === -1)
|
|
344
|
+
startDot = i;
|
|
345
|
+
else if (preDotState !== 1)
|
|
346
|
+
preDotState = 1;
|
|
347
|
+
} else if (startDot !== -1)
|
|
348
|
+
preDotState = -1;
|
|
349
|
+
}
|
|
350
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
|
351
|
+
if (end !== -1)
|
|
352
|
+
if (startPart === 0 && isAbsolute2)
|
|
353
|
+
ret.base = ret.name = path.slice(1, end);
|
|
354
|
+
else
|
|
355
|
+
ret.base = ret.name = path.slice(startPart, end);
|
|
356
|
+
} else {
|
|
357
|
+
if (startPart === 0 && isAbsolute2)
|
|
358
|
+
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
|
359
|
+
else
|
|
360
|
+
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
|
361
|
+
ret.ext = path.slice(startDot, end);
|
|
362
|
+
}
|
|
363
|
+
if (startPart > 0)
|
|
364
|
+
ret.dir = path.slice(0, startPart - 1);
|
|
365
|
+
else if (isAbsolute2)
|
|
366
|
+
ret.dir = "/";
|
|
367
|
+
return ret;
|
|
368
|
+
}
|
|
369
|
+
var sep = "/", delimiter = ":", posix, path_default;
|
|
370
|
+
var init_path = __esm(() => {
|
|
371
|
+
posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
|
372
|
+
path_default = posix;
|
|
373
|
+
});
|
|
374
|
+
|
|
1
375
|
// src/commentary.ts
|
|
2
376
|
var TOOLTIP_ID = "demon-commentary-tooltip";
|
|
3
377
|
async function showCommentary(page, options) {
|
|
@@ -85,33 +459,87 @@ async function hideCommentary(page) {
|
|
|
85
459
|
await page.waitForTimeout(300);
|
|
86
460
|
}
|
|
87
461
|
// src/review.ts
|
|
88
|
-
|
|
89
|
-
|
|
462
|
+
var GIT_DIFF_MAX_CHARS = 50000;
|
|
463
|
+
function buildReviewPrompt(options) {
|
|
464
|
+
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
465
|
+
const demoEntries = filenames.map((f) => {
|
|
466
|
+
const steps = stepsMap[f] ?? [];
|
|
467
|
+
const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
|
|
90
468
|
`);
|
|
91
|
-
|
|
469
|
+
return `Video: ${f}
|
|
470
|
+
Recorded steps:
|
|
471
|
+
${stepLines || "(no steps recorded)"}`;
|
|
472
|
+
});
|
|
473
|
+
const sections = [];
|
|
474
|
+
if (guidelines && guidelines.length > 0) {
|
|
475
|
+
sections.push(`## Coding Guidelines
|
|
476
|
+
|
|
477
|
+
${guidelines.join(`
|
|
478
|
+
|
|
479
|
+
`)}`);
|
|
480
|
+
}
|
|
481
|
+
if (gitDiff) {
|
|
482
|
+
let diff = gitDiff;
|
|
483
|
+
if (diff.length > GIT_DIFF_MAX_CHARS) {
|
|
484
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
|
|
485
|
+
|
|
486
|
+
... (diff truncated at 50k characters)`;
|
|
487
|
+
}
|
|
488
|
+
sections.push(`## Git Diff
|
|
489
|
+
|
|
490
|
+
\`\`\`diff
|
|
491
|
+
${diff}
|
|
492
|
+
\`\`\``);
|
|
493
|
+
}
|
|
494
|
+
sections.push(`## Demo Recordings
|
|
495
|
+
|
|
496
|
+
${demoEntries.join(`
|
|
92
497
|
|
|
93
|
-
|
|
498
|
+
`)}`);
|
|
499
|
+
return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.
|
|
94
500
|
|
|
95
|
-
|
|
501
|
+
${sections.join(`
|
|
502
|
+
|
|
503
|
+
`)}
|
|
504
|
+
|
|
505
|
+
## Task
|
|
506
|
+
|
|
507
|
+
Review the code changes and demo recordings. Generate a JSON object matching this exact schema:
|
|
96
508
|
|
|
97
509
|
{
|
|
98
510
|
"demos": [
|
|
99
511
|
{
|
|
100
512
|
"file": "<filename>",
|
|
101
|
-
"summary": "<a
|
|
102
|
-
"annotations": [
|
|
103
|
-
{ "timestampSeconds": <number>, "text": "<annotation text>" }
|
|
104
|
-
]
|
|
513
|
+
"summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
|
|
105
514
|
}
|
|
106
|
-
]
|
|
515
|
+
],
|
|
516
|
+
"review": {
|
|
517
|
+
"summary": "<2-3 sentence overview of the changes>",
|
|
518
|
+
"highlights": ["<positive aspect 1>", "<positive aspect 2>"],
|
|
519
|
+
"verdict": "approve" | "request_changes",
|
|
520
|
+
"verdictReason": "<one sentence justifying the verdict>",
|
|
521
|
+
"issues": [
|
|
522
|
+
{
|
|
523
|
+
"severity": "major" | "minor" | "nit",
|
|
524
|
+
"description": "<what the issue is and how to fix it>"
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
}
|
|
107
528
|
}
|
|
108
529
|
|
|
109
530
|
Rules:
|
|
110
531
|
- Return ONLY the JSON object, no markdown fences or extra text.
|
|
111
532
|
- Include one entry in "demos" for each filename, in the same order.
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
- "
|
|
533
|
+
- "file" must exactly match the provided filename.
|
|
534
|
+
- "verdict" must be exactly "approve" or "request_changes".
|
|
535
|
+
- Use "request_changes" if there are any "major" issues.
|
|
536
|
+
- "severity" must be exactly "major", "minor", or "nit".
|
|
537
|
+
- "major": bugs, security issues, broken functionality, guideline violations.
|
|
538
|
+
- "minor": code quality, readability, missing edge cases.
|
|
539
|
+
- "nit": style, naming, trivial improvements.
|
|
540
|
+
- "highlights" must have at least one entry.
|
|
541
|
+
- "issues" can be an empty array if there are no issues.
|
|
542
|
+
- Verify that demo steps demonstrate the acceptance criteria being met.`;
|
|
115
543
|
}
|
|
116
544
|
async function invokeClaude(prompt, options) {
|
|
117
545
|
const spawnFn = options?.spawn ?? defaultSpawn;
|
|
@@ -132,10 +560,25 @@ async function invokeClaude(prompt, options) {
|
|
|
132
560
|
}
|
|
133
561
|
return output.trim();
|
|
134
562
|
}
|
|
135
|
-
|
|
563
|
+
var VALID_VERDICTS = new Set(["approve", "request_changes"]);
|
|
564
|
+
var VALID_SEVERITIES = new Set(["major", "minor", "nit"]);
|
|
565
|
+
function extractJson(raw) {
|
|
566
|
+
try {
|
|
567
|
+
JSON.parse(raw);
|
|
568
|
+
return raw;
|
|
569
|
+
} catch {}
|
|
570
|
+
const start = raw.indexOf("{");
|
|
571
|
+
const end = raw.lastIndexOf("}");
|
|
572
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
573
|
+
throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
|
|
574
|
+
}
|
|
575
|
+
return raw.slice(start, end + 1);
|
|
576
|
+
}
|
|
577
|
+
function parseLlmResponse(raw) {
|
|
578
|
+
const jsonStr = extractJson(raw);
|
|
136
579
|
let parsed;
|
|
137
580
|
try {
|
|
138
|
-
parsed = JSON.parse(
|
|
581
|
+
parsed = JSON.parse(jsonStr);
|
|
139
582
|
} catch {
|
|
140
583
|
throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
|
|
141
584
|
}
|
|
@@ -157,20 +600,44 @@ function parseReviewMetadata(raw) {
|
|
|
157
600
|
if (typeof d["summary"] !== "string") {
|
|
158
601
|
throw new Error("Each demo must have a 'summary' string");
|
|
159
602
|
}
|
|
160
|
-
|
|
161
|
-
|
|
603
|
+
}
|
|
604
|
+
if (typeof obj["review"] !== "object" || obj["review"] === null) {
|
|
605
|
+
throw new Error("Missing 'review' object in response");
|
|
606
|
+
}
|
|
607
|
+
const review = obj["review"];
|
|
608
|
+
if (typeof review["summary"] !== "string") {
|
|
609
|
+
throw new Error("review.summary must be a string");
|
|
610
|
+
}
|
|
611
|
+
if (!Array.isArray(review["highlights"])) {
|
|
612
|
+
throw new Error("review.highlights must be an array");
|
|
613
|
+
}
|
|
614
|
+
if (review["highlights"].length === 0) {
|
|
615
|
+
throw new Error("review.highlights must not be empty");
|
|
616
|
+
}
|
|
617
|
+
for (const h of review["highlights"]) {
|
|
618
|
+
if (typeof h !== "string") {
|
|
619
|
+
throw new Error("Each highlight must be a string");
|
|
162
620
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
621
|
+
}
|
|
622
|
+
if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
|
|
623
|
+
throw new Error("review.verdict must be 'approve' or 'request_changes'");
|
|
624
|
+
}
|
|
625
|
+
if (typeof review["verdictReason"] !== "string") {
|
|
626
|
+
throw new Error("review.verdictReason must be a string");
|
|
627
|
+
}
|
|
628
|
+
if (!Array.isArray(review["issues"])) {
|
|
629
|
+
throw new Error("review.issues must be an array");
|
|
630
|
+
}
|
|
631
|
+
for (const issue of review["issues"]) {
|
|
632
|
+
if (typeof issue !== "object" || issue === null) {
|
|
633
|
+
throw new Error("Each issue must be an object");
|
|
634
|
+
}
|
|
635
|
+
const i = issue;
|
|
636
|
+
if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
|
|
637
|
+
throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
|
|
638
|
+
}
|
|
639
|
+
if (typeof i["description"] !== "string") {
|
|
640
|
+
throw new Error("Each issue must have a 'description' string");
|
|
174
641
|
}
|
|
175
642
|
}
|
|
176
643
|
return parsed;
|
|
@@ -196,6 +663,46 @@ function concatUint8Arrays(arrays) {
|
|
|
196
663
|
}
|
|
197
664
|
return result;
|
|
198
665
|
}
|
|
666
|
+
// src/git-context.ts
|
|
667
|
+
var {readFileSync} = (() => ({}));
|
|
668
|
+
var defaultExec = async (cmd, cwd) => {
|
|
669
|
+
const proc = Bun.spawnSync(cmd, { cwd });
|
|
670
|
+
if (proc.exitCode !== 0) {
|
|
671
|
+
const stderr = proc.stderr.toString().trim();
|
|
672
|
+
throw new Error(`Command failed (exit ${proc.exitCode}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
|
|
673
|
+
}
|
|
674
|
+
return proc.stdout.toString();
|
|
675
|
+
};
|
|
676
|
+
var defaultReadFile = (path) => {
|
|
677
|
+
return readFileSync(path, "utf-8");
|
|
678
|
+
};
|
|
679
|
+
async function getRepoContext(demosDir, options) {
|
|
680
|
+
const exec = options?.exec ?? defaultExec;
|
|
681
|
+
const readFile = options?.readFile ?? defaultReadFile;
|
|
682
|
+
const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
|
|
683
|
+
let gitDiff;
|
|
684
|
+
const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
|
|
685
|
+
if (workingDiff.length > 0) {
|
|
686
|
+
gitDiff = workingDiff;
|
|
687
|
+
} else {
|
|
688
|
+
gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
|
|
689
|
+
}
|
|
690
|
+
const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
|
|
691
|
+
const files = lsOutput.split(`
|
|
692
|
+
`).filter((f) => f.length > 0);
|
|
693
|
+
const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
|
|
694
|
+
const guidelines = [];
|
|
695
|
+
for (const file of files) {
|
|
696
|
+
const basename = file.split("/").pop() ?? "";
|
|
697
|
+
if (guidelinePatterns.includes(basename)) {
|
|
698
|
+
const fullPath = `${gitRoot}/${file}`;
|
|
699
|
+
const content = readFile(fullPath);
|
|
700
|
+
guidelines.push(`# ${file}
|
|
701
|
+
${content}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return { gitDiff, guidelines };
|
|
705
|
+
}
|
|
199
706
|
// src/html-generator.ts
|
|
200
707
|
function escapeHtml(s) {
|
|
201
708
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -203,6 +710,32 @@ function escapeHtml(s) {
|
|
|
203
710
|
function escapeAttr(s) {
|
|
204
711
|
return escapeHtml(s);
|
|
205
712
|
}
|
|
713
|
+
function renderReviewSection(review) {
|
|
714
|
+
const bannerClass = review.verdict === "approve" ? "approve" : "request-changes";
|
|
715
|
+
const verdictLabel = review.verdict === "approve" ? "Approved" : "Changes Requested";
|
|
716
|
+
const highlightsHtml = review.highlights.map((h) => `<li>${escapeHtml(h)}</li>`).join(`
|
|
717
|
+
`);
|
|
718
|
+
const issuesHtml = review.issues.length > 0 ? review.issues.map((issue) => {
|
|
719
|
+
const badgeLabel = issue.severity.toUpperCase();
|
|
720
|
+
return `<div class="issue ${issue.severity}"><span class="severity-badge">${badgeLabel}</span> <span class="issue-text">${escapeHtml(issue.description)}</span><button class="feedback-add-issue" data-issue="${escapeAttr(issue.description)}">+</button></div>`;
|
|
721
|
+
}).join(`
|
|
722
|
+
`) : '<p class="no-issues">No issues found.</p>';
|
|
723
|
+
return `<section class="review-section">
|
|
724
|
+
<div class="verdict-banner ${bannerClass}">
|
|
725
|
+
<strong>${verdictLabel}</strong>: ${escapeHtml(review.verdictReason)}
|
|
726
|
+
</div>
|
|
727
|
+
<div class="review-body">
|
|
728
|
+
<h2>Summary</h2>
|
|
729
|
+
<p>${escapeHtml(review.summary)}</p>
|
|
730
|
+
<h2>Highlights</h2>
|
|
731
|
+
<ul class="highlights-list">
|
|
732
|
+
${highlightsHtml}
|
|
733
|
+
</ul>
|
|
734
|
+
<h2>Issues</h2>
|
|
735
|
+
${issuesHtml}
|
|
736
|
+
</div>
|
|
737
|
+
</section>`;
|
|
738
|
+
}
|
|
206
739
|
function generateReviewHtml(options) {
|
|
207
740
|
const { metadata, title = "Demo Review" } = options;
|
|
208
741
|
if (metadata.demos.length === 0) {
|
|
@@ -215,6 +748,9 @@ function generateReviewHtml(options) {
|
|
|
215
748
|
}).join(`
|
|
216
749
|
`);
|
|
217
750
|
const metadataJson = JSON.stringify(metadata).replace(/<\//g, "<\\/");
|
|
751
|
+
const reviewHtml = metadata.review ? renderReviewSection(metadata.review) : "";
|
|
752
|
+
const hasReview = !!metadata.review;
|
|
753
|
+
const defaultTab = hasReview ? "summary" : "demos";
|
|
218
754
|
return `<!DOCTYPE html>
|
|
219
755
|
<html lang="en">
|
|
220
756
|
<head>
|
|
@@ -226,9 +762,40 @@ function generateReviewHtml(options) {
|
|
|
226
762
|
body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
|
|
227
763
|
header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
|
|
228
764
|
header h1 { font-size: 1.4rem; color: #e94560; }
|
|
229
|
-
.
|
|
765
|
+
.tab-bar { display: flex; gap: 0; background: #16213e; border-bottom: 2px solid #0f3460; padding: 0 2rem; }
|
|
766
|
+
.tab-btn { padding: 0.7rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; color: #999; font-size: 0.95rem; cursor: pointer; font-family: inherit; transition: all 0.15s; margin-bottom: -2px; }
|
|
767
|
+
.tab-btn:hover { color: #e0e0e0; }
|
|
768
|
+
.tab-btn.active { color: #e94560; border-bottom-color: #e94560; }
|
|
769
|
+
.tab-panel { display: none; }
|
|
770
|
+
.tab-panel.active { display: block; }
|
|
771
|
+
.review-section { padding: 1.5rem 2rem; }
|
|
772
|
+
.verdict-banner { padding: 1rem 1.5rem; border-radius: 6px; font-size: 1rem; margin-bottom: 1.5rem; }
|
|
773
|
+
.verdict-banner.approve { background: #1b4332; border: 1px solid #2d6a4f; color: #95d5b2; }
|
|
774
|
+
.verdict-banner.request-changes { background: #4a1520; border: 1px solid #842029; color: #f5c6cb; }
|
|
775
|
+
.review-body { max-width: 900px; }
|
|
776
|
+
.review-body h2 { font-size: 1.1rem; color: #e94560; margin: 1.2rem 0 0.5rem; }
|
|
777
|
+
.review-body p { font-size: 0.95rem; line-height: 1.6; color: #ccc; }
|
|
778
|
+
.highlights-list { list-style: disc; padding-left: 1.5rem; margin-bottom: 0.5rem; }
|
|
779
|
+
.highlights-list li { font-size: 0.95rem; line-height: 1.5; color: #95d5b2; margin-bottom: 0.3rem; }
|
|
780
|
+
.issue { padding: 0.6rem 0.8rem; margin-bottom: 0.5rem; border-radius: 4px; font-size: 0.9rem; line-height: 1.4; }
|
|
781
|
+
.issue.major { background: rgba(132, 32, 41, 0.3); border-left: 4px solid #dc3545; }
|
|
782
|
+
.issue.minor { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; }
|
|
783
|
+
.issue.nit { background: rgba(108, 117, 125, 0.2); border-left: 4px solid #6c757d; }
|
|
784
|
+
.severity-badge { display: inline-block; font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: 3px; margin-right: 0.5rem; vertical-align: middle; }
|
|
785
|
+
.issue.major .severity-badge { background: #dc3545; color: #fff; }
|
|
786
|
+
.issue.minor .severity-badge { background: #ffc107; color: #000; }
|
|
787
|
+
.issue.nit .severity-badge { background: #6c757d; color: #fff; }
|
|
788
|
+
.no-issues { color: #95d5b2; font-style: italic; }
|
|
789
|
+
.demos-section { padding: 1rem 0; }
|
|
790
|
+
.review-layout { display: flex; height: 600px; }
|
|
230
791
|
.video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
|
|
231
|
-
.video-
|
|
792
|
+
.video-wrapper { position: relative; width: 100%; max-height: 100%; display: flex; flex-direction: column; }
|
|
793
|
+
.video-wrapper video { width: 100%; max-height: calc(100% - 36px); border-radius: 4px 4px 0 0; display: block; cursor: pointer; }
|
|
794
|
+
.video-controls { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #16213e; border-radius: 0 0 4px 4px; }
|
|
795
|
+
.video-controls button { background: none; border: none; color: #e0e0e0; cursor: pointer; font-size: 1rem; padding: 0; width: 20px; display: flex; align-items: center; justify-content: center; }
|
|
796
|
+
.video-controls button:hover { color: #e94560; }
|
|
797
|
+
.video-controls input[type="range"] { flex: 1; height: 4px; accent-color: #e94560; cursor: pointer; }
|
|
798
|
+
.video-controls .vc-time { font-size: 0.75rem; color: #999; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
232
799
|
.side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
|
|
233
800
|
.side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
|
|
234
801
|
.side-panel section { margin-bottom: 1.5rem; }
|
|
@@ -238,44 +805,109 @@ function generateReviewHtml(options) {
|
|
|
238
805
|
#demo-list button:hover { background: #0f3460; }
|
|
239
806
|
#demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
|
|
240
807
|
#summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
|
|
241
|
-
#
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
808
|
+
#steps-list { list-style: none; }
|
|
809
|
+
#steps-list li { margin-bottom: 0.3rem; }
|
|
810
|
+
#steps-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: transparent; color: #53a8b6; border: none; border-left: 3px solid transparent; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
|
|
811
|
+
#steps-list button:hover { color: #e94560; }
|
|
812
|
+
#steps-list button.step-active { background: rgba(233, 69, 96, 0.15); color: #e94560; border-left-color: #e94560; }
|
|
245
813
|
.timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
|
|
814
|
+
.issue { position: relative; }
|
|
815
|
+
.feedback-add-issue { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.45rem; line-height: 1; }
|
|
816
|
+
.feedback-add-issue:hover { background: #53a8b6; color: #1a1a2e; }
|
|
817
|
+
#feedback-selection-btn { display: none; position: absolute; z-index: 1000; padding: 0.35rem 0.7rem; background: #e94560; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; white-space: nowrap; }
|
|
818
|
+
.feedback-layout { display: flex; gap: 1.5rem; padding: 1.5rem 2rem; }
|
|
819
|
+
.feedback-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
820
|
+
.feedback-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
821
|
+
#feedback-list { list-style: none; padding: 0; }
|
|
822
|
+
#feedback-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; background: #16213e; border: 1px solid #0f3460; border-radius: 4px; margin-bottom: 0.4rem; font-size: 0.9rem; }
|
|
823
|
+
#feedback-list li span { flex: 1; }
|
|
824
|
+
.feedback-remove { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; }
|
|
825
|
+
.feedback-remove:hover { color: #ff6b7a; }
|
|
826
|
+
#feedback-general { width: 100%; min-height: 100px; background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; padding: 0.6rem; font-family: inherit; font-size: 0.9rem; resize: vertical; }
|
|
827
|
+
#feedback-preview { background: #0f0f23; color: #ccc; border: 1px solid #0f3460; border-radius: 4px; padding: 1rem; white-space: pre-wrap; font-size: 0.85rem; line-height: 1.5; flex: 1; min-height: 200px; overflow-y: auto; }
|
|
828
|
+
#feedback-copy { align-self: flex-end; padding: 0.5rem 1rem; background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
|
|
829
|
+
#feedback-copy:hover { background: #53a8b6; color: #1a1a2e; }
|
|
246
830
|
</style>
|
|
247
831
|
</head>
|
|
248
832
|
<body>
|
|
249
833
|
<header>
|
|
250
834
|
<h1>${escapeHtml(title)}</h1>
|
|
251
835
|
</header>
|
|
252
|
-
<
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
</
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
836
|
+
<nav class="tab-bar">
|
|
837
|
+
${hasReview ? `<button class="tab-btn${defaultTab === "summary" ? " active" : ""}" data-tab="summary">Summary</button>` : ""}
|
|
838
|
+
<button class="tab-btn${defaultTab === "demos" ? " active" : ""}" data-tab="demos">Demos</button>
|
|
839
|
+
${hasReview ? `<button class="tab-btn" data-tab="feedback">Feedback</button>` : ""}
|
|
840
|
+
</nav>
|
|
841
|
+
<main>
|
|
842
|
+
${hasReview ? `<div id="tab-summary" class="tab-panel${defaultTab === "summary" ? " active" : ""}">
|
|
843
|
+
${reviewHtml}
|
|
844
|
+
</div>` : ""}
|
|
845
|
+
<div id="tab-demos" class="tab-panel${defaultTab === "demos" ? " active" : ""}">
|
|
846
|
+
<section class="demos-section">
|
|
847
|
+
<div class="review-layout">
|
|
848
|
+
<div class="video-panel">
|
|
849
|
+
<div class="video-wrapper">
|
|
850
|
+
<video id="review-video" src="${escapeAttr(firstDemo.file)}"></video>
|
|
851
|
+
<div class="video-controls">
|
|
852
|
+
<button id="vc-play" aria-label="Play">▶</button>
|
|
853
|
+
<input id="vc-seek" type="range" min="0" max="100" value="0" step="0.1">
|
|
854
|
+
<span class="vc-time" id="vc-time">0:00 / 0:00</span>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
</div>
|
|
858
|
+
<div class="side-panel">
|
|
859
|
+
<section>
|
|
860
|
+
<h2>Demos</h2>
|
|
861
|
+
<ul id="demo-list">
|
|
260
862
|
${demoButtons}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
863
|
+
</ul>
|
|
864
|
+
</section>
|
|
865
|
+
<section>
|
|
866
|
+
<h2>Summary</h2>
|
|
867
|
+
<p id="summary-text"></p>
|
|
868
|
+
</section>
|
|
869
|
+
<section id="steps-section">
|
|
870
|
+
<h2>Steps</h2>
|
|
871
|
+
<ul id="steps-list"></ul>
|
|
872
|
+
</section>
|
|
873
|
+
</div>
|
|
874
|
+
</div>
|
|
875
|
+
</section>
|
|
271
876
|
</div>
|
|
877
|
+
${hasReview ? `<div id="tab-feedback" class="tab-panel">
|
|
878
|
+
<div class="feedback-layout">
|
|
879
|
+
<div class="feedback-left">
|
|
880
|
+
<h2>Feedback Items</h2>
|
|
881
|
+
<ul id="feedback-list"></ul>
|
|
882
|
+
<h2>General Feedback</h2>
|
|
883
|
+
<textarea id="feedback-general" placeholder="Add general feedback here..."></textarea>
|
|
884
|
+
</div>
|
|
885
|
+
<div class="feedback-right">
|
|
886
|
+
<h2>Preview</h2>
|
|
887
|
+
<pre id="feedback-preview"></pre>
|
|
888
|
+
<button id="feedback-copy">Copy to clipboard</button>
|
|
889
|
+
</div>
|
|
890
|
+
</div>
|
|
891
|
+
</div>` : ""}
|
|
272
892
|
</main>
|
|
893
|
+
${hasReview ? `<button id="feedback-selection-btn">Add to feedback</button>` : ""}
|
|
273
894
|
<script>
|
|
274
895
|
(function() {
|
|
896
|
+
// Tab switching
|
|
897
|
+
var tabBtns = document.querySelectorAll(".tab-btn");
|
|
898
|
+
var tabPanels = document.querySelectorAll(".tab-panel");
|
|
899
|
+
tabBtns.forEach(function(btn) {
|
|
900
|
+
btn.addEventListener("click", function() {
|
|
901
|
+
var target = btn.getAttribute("data-tab");
|
|
902
|
+
tabBtns.forEach(function(b) { b.classList.toggle("active", b === btn); });
|
|
903
|
+
tabPanels.forEach(function(p) { p.classList.toggle("active", p.id === "tab-" + target); });
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
|
|
275
907
|
var metadata = ${metadataJson};
|
|
276
908
|
var video = document.getElementById("review-video");
|
|
277
909
|
var summaryText = document.getElementById("summary-text");
|
|
278
|
-
var
|
|
910
|
+
var stepsList = document.getElementById("steps-list");
|
|
279
911
|
var demoButtons = document.querySelectorAll("#demo-list button");
|
|
280
912
|
|
|
281
913
|
function esc(s) {
|
|
@@ -300,13 +932,13 @@ function generateReviewHtml(options) {
|
|
|
300
932
|
btn.classList.toggle("active", i === index);
|
|
301
933
|
});
|
|
302
934
|
|
|
303
|
-
var
|
|
304
|
-
demo.
|
|
305
|
-
|
|
306
|
-
'<span class="timestamp">' + esc(formatTime(
|
|
307
|
-
esc(
|
|
935
|
+
var stepsHtml = "";
|
|
936
|
+
demo.steps.forEach(function(step) {
|
|
937
|
+
stepsHtml += '<li><button data-time="' + step.timestampSeconds + '">' +
|
|
938
|
+
'<span class="timestamp">' + esc(formatTime(step.timestampSeconds)) + '</span>' +
|
|
939
|
+
esc(step.text) + '</button></li>';
|
|
308
940
|
});
|
|
309
|
-
|
|
941
|
+
stepsList.innerHTML = stepsHtml;
|
|
310
942
|
}
|
|
311
943
|
|
|
312
944
|
demoButtons.forEach(function(btn) {
|
|
@@ -315,7 +947,7 @@ function generateReviewHtml(options) {
|
|
|
315
947
|
});
|
|
316
948
|
});
|
|
317
949
|
|
|
318
|
-
|
|
950
|
+
stepsList.addEventListener("click", function(e) {
|
|
319
951
|
var btn = e.target.closest("button[data-time]");
|
|
320
952
|
if (btn) {
|
|
321
953
|
video.currentTime = parseFloat(btn.getAttribute("data-time"));
|
|
@@ -323,19 +955,235 @@ function generateReviewHtml(options) {
|
|
|
323
955
|
}
|
|
324
956
|
});
|
|
325
957
|
|
|
958
|
+
video.addEventListener("timeupdate", function() {
|
|
959
|
+
var buttons = document.querySelectorAll("#steps-list button[data-time]");
|
|
960
|
+
var ct = video.currentTime;
|
|
961
|
+
var activeIdx = -1;
|
|
962
|
+
buttons.forEach(function(btn, i) {
|
|
963
|
+
if (parseFloat(btn.getAttribute("data-time")) <= ct) activeIdx = i;
|
|
964
|
+
btn.classList.remove("step-active");
|
|
965
|
+
});
|
|
966
|
+
if (activeIdx >= 0) buttons[activeIdx].classList.add("step-active");
|
|
967
|
+
});
|
|
968
|
+
|
|
326
969
|
selectDemo(0);
|
|
970
|
+
|
|
971
|
+
// Custom video controls
|
|
972
|
+
var playBtn = document.getElementById("vc-play");
|
|
973
|
+
var seekBar = document.getElementById("vc-seek");
|
|
974
|
+
var timeDisplay = document.getElementById("vc-time");
|
|
975
|
+
var seeking = false;
|
|
976
|
+
|
|
977
|
+
function fmtTime(sec) {
|
|
978
|
+
var m = Math.floor(sec / 60);
|
|
979
|
+
var s = Math.floor(sec % 60);
|
|
980
|
+
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function updateTime() {
|
|
984
|
+
var cur = video.currentTime || 0;
|
|
985
|
+
var dur = video.duration || 0;
|
|
986
|
+
timeDisplay.textContent = fmtTime(cur) + " / " + fmtTime(dur);
|
|
987
|
+
if (!seeking && dur) seekBar.value = (cur / dur) * 100;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function updatePlayBtn() {
|
|
991
|
+
playBtn.innerHTML = video.paused ? "▶" : "▮▮";
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
playBtn.addEventListener("click", function() {
|
|
995
|
+
video.paused ? video.play() : video.pause();
|
|
996
|
+
});
|
|
997
|
+
video.addEventListener("click", function() {
|
|
998
|
+
video.paused ? video.play() : video.pause();
|
|
999
|
+
});
|
|
1000
|
+
video.addEventListener("play", updatePlayBtn);
|
|
1001
|
+
video.addEventListener("pause", updatePlayBtn);
|
|
1002
|
+
video.addEventListener("ended", updatePlayBtn);
|
|
1003
|
+
video.addEventListener("timeupdate", updateTime);
|
|
1004
|
+
video.addEventListener("loadedmetadata", updateTime);
|
|
1005
|
+
|
|
1006
|
+
seekBar.addEventListener("input", function() {
|
|
1007
|
+
seeking = true;
|
|
1008
|
+
if (video.duration) {
|
|
1009
|
+
video.currentTime = (seekBar.value / 100) * video.duration;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
seekBar.addEventListener("change", function() { seeking = false; });
|
|
1013
|
+
|
|
1014
|
+
// Feedback tab logic
|
|
1015
|
+
if (document.getElementById("tab-feedback")) {
|
|
1016
|
+
var feedbackItems = [];
|
|
1017
|
+
var feedbackList = document.getElementById("feedback-list");
|
|
1018
|
+
var feedbackGeneral = document.getElementById("feedback-general");
|
|
1019
|
+
var feedbackPreview = document.getElementById("feedback-preview");
|
|
1020
|
+
var feedbackCopy = document.getElementById("feedback-copy");
|
|
1021
|
+
var selectionBtn = document.getElementById("feedback-selection-btn");
|
|
1022
|
+
|
|
1023
|
+
function addFeedbackItem(text) {
|
|
1024
|
+
var trimmed = text.trim();
|
|
1025
|
+
if (!trimmed) return;
|
|
1026
|
+
for (var i = 0; i < feedbackItems.length; i++) {
|
|
1027
|
+
if (feedbackItems[i] === trimmed) return;
|
|
1028
|
+
}
|
|
1029
|
+
feedbackItems.push(trimmed);
|
|
1030
|
+
renderFeedback();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function removeFeedbackItem(index) {
|
|
1034
|
+
feedbackItems.splice(index, 1);
|
|
1035
|
+
renderFeedback();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function renderFeedback() {
|
|
1039
|
+
var html = "";
|
|
1040
|
+
feedbackItems.forEach(function(item, i) {
|
|
1041
|
+
html += '<li><span>' + esc(item) + '</span><button class="feedback-remove" data-index="' + i + '">X</button></li>';
|
|
1042
|
+
});
|
|
1043
|
+
feedbackList.innerHTML = html;
|
|
1044
|
+
updatePreview();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function updatePreview() {
|
|
1048
|
+
var lines = "";
|
|
1049
|
+
feedbackItems.forEach(function(item, i) {
|
|
1050
|
+
lines += (i + 1) + ". Address: " + item + "\\n";
|
|
1051
|
+
});
|
|
1052
|
+
var general = feedbackGeneral.value.trim();
|
|
1053
|
+
if (general) {
|
|
1054
|
+
lines += "\\nGeneral feedback:\\n" + general;
|
|
1055
|
+
}
|
|
1056
|
+
feedbackPreview.textContent = lines;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Issue "+" buttons
|
|
1060
|
+
var summaryTab = document.getElementById("tab-summary");
|
|
1061
|
+
if (summaryTab) {
|
|
1062
|
+
summaryTab.addEventListener("click", function(e) {
|
|
1063
|
+
var btn = e.target.closest(".feedback-add-issue");
|
|
1064
|
+
if (btn) {
|
|
1065
|
+
addFeedbackItem(btn.getAttribute("data-issue"));
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Text selection floating button
|
|
1071
|
+
var selectionTimeout;
|
|
1072
|
+
document.addEventListener("mouseup", function(e) {
|
|
1073
|
+
clearTimeout(selectionTimeout);
|
|
1074
|
+
selectionTimeout = setTimeout(function() {
|
|
1075
|
+
var sel = window.getSelection();
|
|
1076
|
+
var text = sel ? sel.toString().trim() : "";
|
|
1077
|
+
if (!text) return;
|
|
1078
|
+
var anchor = sel.anchorNode;
|
|
1079
|
+
var inSummary = false;
|
|
1080
|
+
var node = anchor;
|
|
1081
|
+
while (node) {
|
|
1082
|
+
if (node.id === "tab-summary") { inSummary = true; break; }
|
|
1083
|
+
node = node.parentNode;
|
|
1084
|
+
}
|
|
1085
|
+
if (!inSummary) return;
|
|
1086
|
+
selectionBtn.style.display = "block";
|
|
1087
|
+
selectionBtn.style.left = e.pageX + "px";
|
|
1088
|
+
selectionBtn.style.top = (e.pageY - 35) + "px";
|
|
1089
|
+
selectionBtn._selectedText = text;
|
|
1090
|
+
}, 100);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
selectionBtn.addEventListener("click", function() {
|
|
1094
|
+
if (selectionBtn._selectedText) {
|
|
1095
|
+
addFeedbackItem(selectionBtn._selectedText);
|
|
1096
|
+
}
|
|
1097
|
+
selectionBtn.style.display = "none";
|
|
1098
|
+
window.getSelection().removeAllRanges();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
document.addEventListener("mousedown", function(e) {
|
|
1102
|
+
if (e.target !== selectionBtn) {
|
|
1103
|
+
selectionBtn.style.display = "none";
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// Remove buttons
|
|
1108
|
+
feedbackList.addEventListener("click", function(e) {
|
|
1109
|
+
var btn = e.target.closest(".feedback-remove");
|
|
1110
|
+
if (btn) {
|
|
1111
|
+
removeFeedbackItem(parseInt(btn.getAttribute("data-index"), 10));
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// Textarea input
|
|
1116
|
+
feedbackGeneral.addEventListener("input", updatePreview);
|
|
1117
|
+
|
|
1118
|
+
// Copy button
|
|
1119
|
+
feedbackCopy.addEventListener("click", function() {
|
|
1120
|
+
var text = feedbackPreview.textContent;
|
|
1121
|
+
function onCopied() {
|
|
1122
|
+
feedbackCopy.textContent = "Copied!";
|
|
1123
|
+
setTimeout(function() { feedbackCopy.textContent = "Copy to clipboard"; }, 1500);
|
|
1124
|
+
}
|
|
1125
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1126
|
+
navigator.clipboard.writeText(text).then(onCopied, onCopied);
|
|
1127
|
+
} else {
|
|
1128
|
+
onCopied();
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
renderFeedback();
|
|
1133
|
+
}
|
|
327
1134
|
})();
|
|
328
1135
|
</script>
|
|
329
1136
|
</body>
|
|
330
1137
|
</html>`;
|
|
331
1138
|
}
|
|
1139
|
+
// src/recorder.ts
|
|
1140
|
+
class DemoRecorder {
|
|
1141
|
+
steps = [];
|
|
1142
|
+
startTime;
|
|
1143
|
+
showCommentaryFn;
|
|
1144
|
+
testStepFn;
|
|
1145
|
+
constructor(options) {
|
|
1146
|
+
this.startTime = Date.now();
|
|
1147
|
+
this.showCommentaryFn = options?.showCommentary ?? showCommentary;
|
|
1148
|
+
this.testStepFn = options?.testStep;
|
|
1149
|
+
}
|
|
1150
|
+
async step(page, text, options) {
|
|
1151
|
+
const body = async () => {
|
|
1152
|
+
await this.showCommentaryFn(page, {
|
|
1153
|
+
selector: options.selector,
|
|
1154
|
+
text
|
|
1155
|
+
});
|
|
1156
|
+
const timestampSeconds = Math.round((Date.now() - this.startTime) / 100) / 10;
|
|
1157
|
+
this.steps.push({ text, timestampSeconds });
|
|
1158
|
+
};
|
|
1159
|
+
if (this.testStepFn) {
|
|
1160
|
+
await this.testStepFn(text, body);
|
|
1161
|
+
} else {
|
|
1162
|
+
await body();
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
getSteps() {
|
|
1166
|
+
return [...this.steps];
|
|
1167
|
+
}
|
|
1168
|
+
async save(outputDir) {
|
|
1169
|
+
const { join: join2 } = await Promise.resolve().then(() => (init_path(), exports_path));
|
|
1170
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
1171
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1172
|
+
const filePath = join2(outputDir, "demo-steps.json");
|
|
1173
|
+
writeFileSync(filePath, JSON.stringify(this.steps, null, 2) + `
|
|
1174
|
+
`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
332
1177
|
export {
|
|
333
1178
|
showCommentary,
|
|
334
|
-
|
|
1179
|
+
parseLlmResponse,
|
|
335
1180
|
invokeClaude,
|
|
336
1181
|
hideCommentary,
|
|
1182
|
+
getRepoContext,
|
|
337
1183
|
generateReviewHtml,
|
|
338
|
-
|
|
1184
|
+
extractJson,
|
|
1185
|
+
buildReviewPrompt,
|
|
1186
|
+
DemoRecorder
|
|
339
1187
|
};
|
|
340
1188
|
|
|
341
|
-
//# debugId=
|
|
1189
|
+
//# debugId=160BDBF1FB742F2264756E2164756E21
|