d2d-feedbackkit 0.0.0 → 0.1.1
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 +83 -0
- package/package.json +10 -1
- package/src/index.ts +1 -0
- package/src/solid.tsx +156 -420
- package/src/styles.css +21 -0
- package/src/styles.ts +35 -0
- package/src/types.ts +30 -1
- package/src/web-component.tsx +111 -3
package/README.md
CHANGED
|
@@ -11,3 +11,86 @@ import { createFeedbackLocator } from "d2d-feedbackkit";
|
|
|
11
11
|
import { mountFeedbackLocatorElement } from "d2d-feedbackkit/web-component";
|
|
12
12
|
import feedbackKitBabelPlugin from "d2d-feedbackkit/vite-plugin";
|
|
13
13
|
```
|
|
14
|
+
|
|
15
|
+
## Solid host with your own button
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { createSignal } from "solid-js";
|
|
19
|
+
import { createFeedbackLocator } from "d2d-feedbackkit";
|
|
20
|
+
import { FeedbackLocatorRoot } from "d2d-feedbackkit/widget";
|
|
21
|
+
import "d2d-feedbackkit/styles.css";
|
|
22
|
+
|
|
23
|
+
const locator = createFeedbackLocator({
|
|
24
|
+
appKey: "morgo-app",
|
|
25
|
+
appName: "Morgo App",
|
|
26
|
+
showFloatingButton: false,
|
|
27
|
+
submitFeedback: async (input) => {
|
|
28
|
+
const response = await fetch("/api/feedback", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: input.formData,
|
|
31
|
+
});
|
|
32
|
+
return response.json();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function FeedbackButton() {
|
|
37
|
+
const [open, setOpen] = createSignal(false);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<button type="button" onClick={() => setOpen(true)}>
|
|
42
|
+
Feedback
|
|
43
|
+
</button>
|
|
44
|
+
<FeedbackLocatorRoot
|
|
45
|
+
locator={locator}
|
|
46
|
+
open={open()}
|
|
47
|
+
onOpenChange={setOpen}
|
|
48
|
+
onSubmitted={() => toast.success("Feedback verstuurd")}
|
|
49
|
+
/>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Web component host
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createFeedbackLocator } from "d2d-feedbackkit";
|
|
59
|
+
import { defineFeedbackLocatorElement } from "d2d-feedbackkit/web-component";
|
|
60
|
+
|
|
61
|
+
defineFeedbackLocatorElement({ shadowRoot: true });
|
|
62
|
+
|
|
63
|
+
const locator = createFeedbackLocator({
|
|
64
|
+
appKey: "host-app",
|
|
65
|
+
appName: "Host App",
|
|
66
|
+
showFloatingButton: false,
|
|
67
|
+
enableVideoRecording: false,
|
|
68
|
+
theme: {
|
|
69
|
+
primary: "45 96% 53%",
|
|
70
|
+
primaryForeground: "30 70% 8%",
|
|
71
|
+
ring: "45 96% 53%",
|
|
72
|
+
},
|
|
73
|
+
submitFeedback: async (input) => {
|
|
74
|
+
const response = await fetch("https://feedbackkit.d2d.cloud/api/submissions", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "x-api-key": "<feedbackkit-api-key>" },
|
|
77
|
+
body: input.formData,
|
|
78
|
+
});
|
|
79
|
+
return response.json();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const widget = document.querySelector("d2d-feedbackkit");
|
|
84
|
+
widget.locator = locator;
|
|
85
|
+
widget.showFloatingButton = false;
|
|
86
|
+
|
|
87
|
+
document.querySelector("#feedback-button").addEventListener("click", () => {
|
|
88
|
+
widget.open = true;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
widget.addEventListener("feedbackkit-submitted", () => {
|
|
92
|
+
showToast("Feedback verstuurd");
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Without the `open` property the widget remains backwards compatible and shows its own floating feedback button by default.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "d2d-feedbackkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SolidJS feedback widget, source locator, and integration helpers.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
"./agent": {
|
|
38
38
|
"types": "./src/agent.ts",
|
|
39
39
|
"default": "./src/agent.ts"
|
|
40
|
+
},
|
|
41
|
+
"./styles": {
|
|
42
|
+
"types": "./src/styles.ts",
|
|
43
|
+
"default": "./src/styles.ts"
|
|
44
|
+
},
|
|
45
|
+
"./styles.css": {
|
|
46
|
+
"default": "./src/styles.css"
|
|
40
47
|
}
|
|
41
48
|
},
|
|
42
49
|
"files": [
|
|
@@ -49,6 +56,8 @@
|
|
|
49
56
|
"src/shortcuts.ts",
|
|
50
57
|
"src/solid.tsx",
|
|
51
58
|
"src/source.ts",
|
|
59
|
+
"src/styles.css",
|
|
60
|
+
"src/styles.ts",
|
|
52
61
|
"src/types.ts",
|
|
53
62
|
"src/vite-plugin.ts",
|
|
54
63
|
"src/web-component.tsx",
|
package/src/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export function createFeedbackLocator(config: FeedbackLocatorConfig): FeedbackLo
|
|
|
34
34
|
const resolvedConfig = {
|
|
35
35
|
...config,
|
|
36
36
|
hotkey: config.hotkey ?? defaultFeedbackLocatorHotkey,
|
|
37
|
+
showFloatingButton: config.showFloatingButton ?? true,
|
|
37
38
|
contextProviders: config.contextProviders ?? [],
|
|
38
39
|
sourceCollector: config.sourceCollector ?? getFeedbackLocatorSourceForElement,
|
|
39
40
|
};
|
package/src/solid.tsx
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { toBlob } from "html-to-image";
|
|
2
2
|
import {
|
|
3
3
|
ArrowUpRight,
|
|
4
|
-
Bot,
|
|
5
|
-
CheckCircle2,
|
|
6
4
|
ChevronDown,
|
|
7
5
|
FileUp,
|
|
8
6
|
Maximize2,
|
|
@@ -32,24 +30,31 @@ import {
|
|
|
32
30
|
} from "solid-js";
|
|
33
31
|
import type {
|
|
34
32
|
FeedbackLocator,
|
|
35
|
-
FeedbackLocatorAgentSessionResult,
|
|
36
33
|
FeedbackLocatorBounds,
|
|
37
|
-
|
|
34
|
+
FeedbackLocatorCloseReason,
|
|
38
35
|
FeedbackLocatorContextPreview,
|
|
36
|
+
FeedbackLocatorHostedSubmitResult,
|
|
39
37
|
FeedbackLocatorHotkey,
|
|
40
|
-
FeedbackLocatorOpenCodeThreadResult,
|
|
41
38
|
FeedbackLocatorPoint,
|
|
42
39
|
FeedbackLocatorSourceMetadata,
|
|
43
|
-
|
|
40
|
+
FeedbackLocatorTheme,
|
|
44
41
|
} from "./types";
|
|
45
42
|
import { createShortcutResolver } from "./shortcuts";
|
|
46
43
|
|
|
47
|
-
type FeedbackLocatorRootProps = {
|
|
44
|
+
export type FeedbackLocatorRootProps = {
|
|
48
45
|
locator: FeedbackLocator;
|
|
46
|
+
open?: boolean;
|
|
47
|
+
defaultOpen?: boolean;
|
|
48
|
+
onOpenChange?: (open: boolean) => void;
|
|
49
|
+
onClose?: (reason: FeedbackLocatorCloseReason) => void;
|
|
50
|
+
onSubmitted?: (result: FeedbackLocatorHostedSubmitResult) => void;
|
|
51
|
+
showFloatingButton?: boolean;
|
|
52
|
+
enableVideoRecording?: boolean;
|
|
53
|
+
theme?: FeedbackLocatorTheme;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
type FeedbackLocatorMode = "idle" | "selecting" | "annotating" | "submitting";
|
|
52
|
-
type FeedbackLocatorSubmitTarget = "
|
|
57
|
+
type FeedbackLocatorSubmitTarget = "feedback";
|
|
53
58
|
type AnnotationTool = "select" | "pen" | "rect" | "arrow" | "text";
|
|
54
59
|
|
|
55
60
|
type PenAnnotation = {
|
|
@@ -194,6 +199,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
194
199
|
let annotationDragState: AnnotationDragState | null = null;
|
|
195
200
|
|
|
196
201
|
const [mode, setMode] = createSignal<FeedbackLocatorMode>("idle");
|
|
202
|
+
const [uncontrolledOpen, setUncontrolledOpen] = createSignal(Boolean(props.defaultOpen));
|
|
197
203
|
const [hoverRect, setHoverRect] = createSignal<FeedbackLocatorBounds | null>(null);
|
|
198
204
|
const [selected, setSelected] = createSignal<ScreenshotState | null>(null);
|
|
199
205
|
const [annotations, setAnnotations] = createSignal<Annotation[]>([]);
|
|
@@ -218,14 +224,8 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
218
224
|
const [recordingUrl, setRecordingUrl] = createSignal<string | null>(null);
|
|
219
225
|
const [recordingDurationMs, setRecordingDurationMs] = createSignal<number | null>(null);
|
|
220
226
|
const [error, setError] = createSignal<string | null>(null);
|
|
227
|
+
const [successToast, setSuccessToast] = createSignal<string | null>(null);
|
|
221
228
|
const [submitTarget, setSubmitTarget] = createSignal<FeedbackLocatorSubmitTarget | null>(null);
|
|
222
|
-
const [linearResult, setLinearResult] = createSignal<FeedbackLocatorSubmitResult | null>(null);
|
|
223
|
-
const [agentSession, setAgentSession] =
|
|
224
|
-
createSignal<FeedbackLocatorAgentSessionResult | null>(null);
|
|
225
|
-
const [codexThread, setCodexThread] =
|
|
226
|
-
createSignal<FeedbackLocatorCodexThreadResult | null>(null);
|
|
227
|
-
const [openCodeThread, setOpenCodeThread] =
|
|
228
|
-
createSignal<FeedbackLocatorOpenCodeThreadResult | null>(null);
|
|
229
229
|
let mediaRecorder: MediaRecorder | undefined;
|
|
230
230
|
let recordingStream: MediaStream | undefined;
|
|
231
231
|
let recordingStartedAt = 0;
|
|
@@ -240,7 +240,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
240
240
|
|
|
241
241
|
if (matchesHotkey(event, props.locator.config.hotkey!)) {
|
|
242
242
|
event.preventDefault();
|
|
243
|
-
|
|
243
|
+
openWidget();
|
|
244
244
|
}
|
|
245
245
|
};
|
|
246
246
|
|
|
@@ -252,6 +252,18 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
252
252
|
});
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
createEffect(() => {
|
|
256
|
+
const shouldBeOpen = isWidgetOpen();
|
|
257
|
+
|
|
258
|
+
if (shouldBeOpen && mode() === "idle") {
|
|
259
|
+
beginSelection();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!shouldBeOpen && mode() !== "idle") {
|
|
263
|
+
resetState();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
255
267
|
createEffect(() => {
|
|
256
268
|
if (mode() !== "selecting") {
|
|
257
269
|
return;
|
|
@@ -280,7 +292,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
280
292
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
281
293
|
if (event.key === "Escape") {
|
|
282
294
|
event.preventDefault();
|
|
283
|
-
|
|
295
|
+
requestClose("escape");
|
|
284
296
|
}
|
|
285
297
|
};
|
|
286
298
|
|
|
@@ -387,7 +399,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
387
399
|
}
|
|
388
400
|
|
|
389
401
|
if (action === "close") {
|
|
390
|
-
|
|
402
|
+
requestClose("escape");
|
|
391
403
|
}
|
|
392
404
|
};
|
|
393
405
|
|
|
@@ -395,18 +407,38 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
395
407
|
onCleanup(() => document.removeEventListener("keydown", handleKeydown));
|
|
396
408
|
});
|
|
397
409
|
|
|
398
|
-
function
|
|
410
|
+
function isWidgetOpen() {
|
|
411
|
+
return props.open ?? uncontrolledOpen();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function setWidgetOpen(open: boolean) {
|
|
415
|
+
if (props.open === undefined) {
|
|
416
|
+
setUncontrolledOpen(open);
|
|
417
|
+
}
|
|
418
|
+
props.onOpenChange?.(open);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function openWidget() {
|
|
422
|
+
setWidgetOpen(true);
|
|
423
|
+
if (mode() === "idle") {
|
|
424
|
+
beginSelection();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function requestClose(reason: FeedbackLocatorCloseReason) {
|
|
429
|
+
resetState();
|
|
430
|
+
setWidgetOpen(false);
|
|
431
|
+
props.onClose?.(reason);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function beginSelection() {
|
|
399
435
|
setError(null);
|
|
400
436
|
setSubmitTarget(null);
|
|
401
|
-
setLinearResult(null);
|
|
402
|
-
setAgentSession(null);
|
|
403
|
-
setCodexThread(null);
|
|
404
437
|
setSelected(null);
|
|
405
438
|
setAnnotations([]);
|
|
406
439
|
setAnnotationHistory([]);
|
|
407
440
|
setRedoStack([]);
|
|
408
441
|
setDraft(null);
|
|
409
|
-
setTextDraft(null);
|
|
410
442
|
setSelectedAnnotationIndex(null);
|
|
411
443
|
setContextPreview(null);
|
|
412
444
|
annotationDragState = null;
|
|
@@ -417,7 +449,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
417
449
|
setMode("selecting");
|
|
418
450
|
}
|
|
419
451
|
|
|
420
|
-
function
|
|
452
|
+
function resetState() {
|
|
421
453
|
const current = selected();
|
|
422
454
|
if (current) {
|
|
423
455
|
URL.revokeObjectURL(current.originalUrl);
|
|
@@ -440,8 +472,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
440
472
|
clearRecording();
|
|
441
473
|
setError(null);
|
|
442
474
|
setSubmitTarget(null);
|
|
443
|
-
setLinearResult(null);
|
|
444
|
-
setAgentSession(null);
|
|
445
475
|
}
|
|
446
476
|
|
|
447
477
|
async function captureElement(element: HTMLElement) {
|
|
@@ -630,68 +660,12 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
630
660
|
}
|
|
631
661
|
|
|
632
662
|
async function submitFeedback() {
|
|
633
|
-
await runSubmission("
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
try {
|
|
640
|
-
const submitResult = await props.locator.config.submitFeedback(prepared);
|
|
641
|
-
setLinearResult(submitResult);
|
|
642
|
-
openExternalWindow(submitResult.issueUrl, pendingWindow);
|
|
643
|
-
} catch (submitError) {
|
|
644
|
-
closePendingWindow(pendingWindow);
|
|
645
|
-
throw submitError;
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
async function startAgentSession() {
|
|
651
|
-
const startAgentSessionAction = props.locator.config.startAgentSession;
|
|
652
|
-
|
|
653
|
-
if (!startAgentSessionAction) {
|
|
654
|
-
setError("Agent sessies zijn nog niet gekoppeld voor deze app.");
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
await runSubmission("agent", async (prepared) => {
|
|
659
|
-
const pendingWindow = openPendingAgentSessionWindow();
|
|
660
|
-
const session = await startAgentSessionAction(prepared);
|
|
661
|
-
setAgentSession(session);
|
|
662
|
-
openAgentSessionWindow(session, pendingWindow);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
async function startCodexThread() {
|
|
667
|
-
const startCodexThreadAction = props.locator.config.startCodexThread;
|
|
668
|
-
|
|
669
|
-
if (!startCodexThreadAction) {
|
|
670
|
-
setError("Codex threads zijn nog niet gekoppeld voor deze app.");
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
await runSubmission("codex", async (prepared) => {
|
|
675
|
-
const thread = await startCodexThreadAction(prepared);
|
|
676
|
-
setCodexThread(thread);
|
|
677
|
-
|
|
678
|
-
if (thread.linearIssue) {
|
|
679
|
-
openExternalWindow(thread.linearIssue.issueUrl);
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
async function startOpenCodeThread() {
|
|
685
|
-
const startOpenCodeThreadAction = props.locator.config.startOpenCodeThread;
|
|
686
|
-
|
|
687
|
-
if (!startOpenCodeThreadAction) {
|
|
688
|
-
setError("OpenCode sessies zijn nog niet gekoppeld voor deze app.");
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
await runSubmission("opencode", async (prepared) => {
|
|
693
|
-
const thread = await startOpenCodeThreadAction(prepared);
|
|
694
|
-
setOpenCodeThread(thread);
|
|
663
|
+
await runSubmission("feedback", async (prepared) => {
|
|
664
|
+
const submitResult = await props.locator.config.submitFeedback(prepared);
|
|
665
|
+
props.onSubmitted?.(submitResult);
|
|
666
|
+
setSuccessToast("Feedback verstuurd");
|
|
667
|
+
window.setTimeout(() => setSuccessToast(null), 4000);
|
|
668
|
+
requestClose("submitted");
|
|
695
669
|
});
|
|
696
670
|
}
|
|
697
671
|
|
|
@@ -711,8 +685,10 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
711
685
|
}
|
|
712
686
|
|
|
713
687
|
await submit(prepared);
|
|
714
|
-
|
|
715
|
-
|
|
688
|
+
if (mode() === "submitting") {
|
|
689
|
+
applySubmissionAnnotations(prepared);
|
|
690
|
+
setMode("annotating");
|
|
691
|
+
}
|
|
716
692
|
} catch (submitError) {
|
|
717
693
|
setMode("annotating");
|
|
718
694
|
setError(
|
|
@@ -1336,6 +1312,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1336
1312
|
const snapshot = () => selected();
|
|
1337
1313
|
const selectionRect = () => hoverRect();
|
|
1338
1314
|
const currentTextDraft = () => textDraft();
|
|
1315
|
+
const isVideoRecordingEnabled = () =>
|
|
1316
|
+
(props.enableVideoRecording ?? props.locator.config.enableVideoRecording) === true;
|
|
1317
|
+
const themeStyle = (): JSX.CSSProperties => {
|
|
1318
|
+
const theme = props.theme ?? props.locator.config.theme;
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
...(theme?.primary ? { "--primary": theme.primary } : {}),
|
|
1322
|
+
...(theme?.primaryForeground ? { "--primary-foreground": theme.primaryForeground } : {}),
|
|
1323
|
+
...(theme?.ring ? { "--ring": theme.ring } : {}),
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1339
1326
|
const canvasCursor = () => {
|
|
1340
1327
|
if (tool() === "select") {
|
|
1341
1328
|
return isMovingAnnotation() ? "grabbing" : "grab";
|
|
@@ -1349,12 +1336,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1349
1336
|
};
|
|
1350
1337
|
|
|
1351
1338
|
return (
|
|
1352
|
-
<div data-feedback-locator-ui="true">
|
|
1353
|
-
<Show
|
|
1339
|
+
<div data-feedback-locator-ui="true" style={themeStyle()}>
|
|
1340
|
+
<Show
|
|
1341
|
+
when={
|
|
1342
|
+
mode() === "idle" &&
|
|
1343
|
+
(props.showFloatingButton ?? props.locator.config.showFloatingButton) !== false
|
|
1344
|
+
}
|
|
1345
|
+
>
|
|
1354
1346
|
<button
|
|
1355
1347
|
type="button"
|
|
1356
1348
|
class="fixed bottom-5 right-5 z-[2147483000] inline-flex h-11 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-md transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
1357
|
-
onClick={
|
|
1349
|
+
onClick={openWidget}
|
|
1358
1350
|
title="Feedback toevoegen (Ctrl+Shift+F)"
|
|
1359
1351
|
>
|
|
1360
1352
|
<MessageSquarePlus class="h-4 w-4" />
|
|
@@ -1368,6 +1360,14 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1368
1360
|
</div>
|
|
1369
1361
|
</Show>
|
|
1370
1362
|
|
|
1363
|
+
<Show when={successToast()}>
|
|
1364
|
+
{(message) => (
|
|
1365
|
+
<div class="fixed bottom-20 right-5 z-[2147483000] max-w-sm rounded-md border border-primary/30 bg-background px-4 py-3 text-sm font-medium text-foreground shadow-md">
|
|
1366
|
+
{message()}
|
|
1367
|
+
</div>
|
|
1368
|
+
)}
|
|
1369
|
+
</Show>
|
|
1370
|
+
|
|
1371
1371
|
<Show when={mode() === "selecting"}>
|
|
1372
1372
|
<div class="fixed left-1/2 top-4 z-[2147483000] flex -translate-x-1/2 items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm text-foreground shadow-md">
|
|
1373
1373
|
<MousePointer2 class="h-4 w-4 text-primary" />
|
|
@@ -1375,7 +1375,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1375
1375
|
<button
|
|
1376
1376
|
type="button"
|
|
1377
1377
|
class="ml-2 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
1378
|
-
onClick={
|
|
1378
|
+
onClick={() => requestClose("user")}
|
|
1379
1379
|
title="Annuleren"
|
|
1380
1380
|
>
|
|
1381
1381
|
<X class="h-4 w-4" />
|
|
@@ -1410,7 +1410,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1410
1410
|
<button
|
|
1411
1411
|
type="button"
|
|
1412
1412
|
class="rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
1413
|
-
onClick={
|
|
1413
|
+
onClick={() => requestClose("user")}
|
|
1414
1414
|
title="Sluiten"
|
|
1415
1415
|
>
|
|
1416
1416
|
<X class="h-5 w-5" />
|
|
@@ -1592,109 +1592,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1592
1592
|
|
|
1593
1593
|
<aside class="flex min-h-0 flex-col bg-background">
|
|
1594
1594
|
<div class="space-y-4 overflow-auto p-4">
|
|
1595
|
-
<Show when={linearResult()}>
|
|
1596
|
-
{(issue) => (
|
|
1597
|
-
<div class="rounded-md border border-primary/40 bg-primary/10 p-3 text-sm">
|
|
1598
|
-
<div class="flex items-center gap-2 font-medium">
|
|
1599
|
-
<CheckCircle2 class="h-4 w-4 text-primary" />
|
|
1600
|
-
Linear issue aangemaakt
|
|
1601
|
-
</div>
|
|
1602
|
-
<a
|
|
1603
|
-
class="mt-2 inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline"
|
|
1604
|
-
href={issue().issueUrl}
|
|
1605
|
-
target="_blank"
|
|
1606
|
-
rel="noreferrer"
|
|
1607
|
-
>
|
|
1608
|
-
{issue().issueIdentifier}
|
|
1609
|
-
<ArrowUpRight class="h-3.5 w-3.5" />
|
|
1610
|
-
</a>
|
|
1611
|
-
</div>
|
|
1612
|
-
)}
|
|
1613
|
-
</Show>
|
|
1614
|
-
|
|
1615
|
-
<Show when={agentSession()}>
|
|
1616
|
-
{(session) => (
|
|
1617
|
-
<div class="rounded-md border border-sky-500/40 bg-sky-500/10 p-3 text-sm">
|
|
1618
|
-
<div class="flex items-center gap-2 font-medium">
|
|
1619
|
-
<CheckCircle2 class="h-4 w-4 text-sky-500" />
|
|
1620
|
-
Agent sessie gestart
|
|
1621
|
-
</div>
|
|
1622
|
-
<Show
|
|
1623
|
-
when={session().sessionUrl}
|
|
1624
|
-
fallback={
|
|
1625
|
-
<div class="mt-2 text-muted-foreground">{session().sessionId}</div>
|
|
1626
|
-
}
|
|
1627
|
-
>
|
|
1628
|
-
{(url) => (
|
|
1629
|
-
<a
|
|
1630
|
-
class="mt-2 inline-flex items-center gap-1 text-sky-600 underline-offset-4 hover:underline"
|
|
1631
|
-
href={url()}
|
|
1632
|
-
target="_blank"
|
|
1633
|
-
rel="noreferrer"
|
|
1634
|
-
>
|
|
1635
|
-
{session().sessionTitle || session().sessionId}
|
|
1636
|
-
<ArrowUpRight class="h-3.5 w-3.5" />
|
|
1637
|
-
</a>
|
|
1638
|
-
)}
|
|
1639
|
-
</Show>
|
|
1640
|
-
</div>
|
|
1641
|
-
)}
|
|
1642
|
-
</Show>
|
|
1643
|
-
|
|
1644
|
-
<Show when={codexThread()}>
|
|
1645
|
-
{(thread) => (
|
|
1646
|
-
<div class="rounded-md border border-violet-500/40 bg-violet-500/10 p-3 text-sm">
|
|
1647
|
-
<div class="flex items-center gap-2 font-medium">
|
|
1648
|
-
<CheckCircle2 class="h-4 w-4 text-violet-500" />
|
|
1649
|
-
Codex thread gestart
|
|
1650
|
-
</div>
|
|
1651
|
-
<div class="mt-2 text-muted-foreground">
|
|
1652
|
-
{thread().threadTitle || thread().threadId}
|
|
1653
|
-
</div>
|
|
1654
|
-
<Show when={thread().linearIssue}>
|
|
1655
|
-
{(issue) => (
|
|
1656
|
-
<a
|
|
1657
|
-
class="mt-2 inline-flex items-center gap-1 text-violet-600 underline-offset-4 hover:underline"
|
|
1658
|
-
href={issue().issueUrl}
|
|
1659
|
-
target="_blank"
|
|
1660
|
-
rel="noreferrer"
|
|
1661
|
-
>
|
|
1662
|
-
Linear: {issue().issueIdentifier}
|
|
1663
|
-
<ArrowUpRight class="h-3.5 w-3.5" />
|
|
1664
|
-
</a>
|
|
1665
|
-
)}
|
|
1666
|
-
</Show>
|
|
1667
|
-
</div>
|
|
1668
|
-
)}
|
|
1669
|
-
</Show>
|
|
1670
|
-
|
|
1671
|
-
<Show when={openCodeThread()}>
|
|
1672
|
-
{(thread) => (
|
|
1673
|
-
<div class="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-3 text-sm">
|
|
1674
|
-
<div class="flex items-center gap-2 font-medium">
|
|
1675
|
-
<CheckCircle2 class="h-4 w-4 text-emerald-500" />
|
|
1676
|
-
OpenCode sessie gestart
|
|
1677
|
-
</div>
|
|
1678
|
-
<div class="mt-2 text-muted-foreground">
|
|
1679
|
-
{thread().sessionTitle || thread().sessionId}
|
|
1680
|
-
</div>
|
|
1681
|
-
<Show when={thread().linearIssue}>
|
|
1682
|
-
{(issue) => (
|
|
1683
|
-
<a
|
|
1684
|
-
class="mt-2 inline-flex items-center gap-1 text-emerald-600 underline-offset-4 hover:underline"
|
|
1685
|
-
href={issue().issueUrl}
|
|
1686
|
-
target="_blank"
|
|
1687
|
-
rel="noreferrer"
|
|
1688
|
-
>
|
|
1689
|
-
Linear: {issue().issueIdentifier}
|
|
1690
|
-
<ArrowUpRight class="h-3.5 w-3.5" />
|
|
1691
|
-
</a>
|
|
1692
|
-
)}
|
|
1693
|
-
</Show>
|
|
1694
|
-
</div>
|
|
1695
|
-
)}
|
|
1696
|
-
</Show>
|
|
1697
|
-
|
|
1698
1595
|
<div>
|
|
1699
1596
|
<label class="mb-2 block text-sm font-medium">Feedback</label>
|
|
1700
1597
|
<textarea
|
|
@@ -1732,70 +1629,72 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1732
1629
|
</Show>
|
|
1733
1630
|
</div>
|
|
1734
1631
|
|
|
1735
|
-
<
|
|
1736
|
-
<
|
|
1737
|
-
|
|
1738
|
-
<div class="
|
|
1739
|
-
<
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
type="button"
|
|
1744
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
|
|
1745
|
-
disabled={mode() === "submitting" || !supportsScreenRecording()}
|
|
1746
|
-
onClick={startRecording}
|
|
1747
|
-
>
|
|
1748
|
-
<Video class="h-4 w-4" />
|
|
1749
|
-
Neem video op
|
|
1750
|
-
</button>
|
|
1751
|
-
}
|
|
1752
|
-
>
|
|
1753
|
-
<button
|
|
1754
|
-
type="button"
|
|
1755
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
|
|
1756
|
-
disabled={mode() === "submitting"}
|
|
1757
|
-
onClick={stopRecording}
|
|
1758
|
-
>
|
|
1759
|
-
<StopCircle class="h-4 w-4" />
|
|
1760
|
-
Stop opname
|
|
1761
|
-
</button>
|
|
1762
|
-
</Show>
|
|
1763
|
-
|
|
1764
|
-
<Show when={recordingBlob()}>
|
|
1765
|
-
{(blob) => (
|
|
1766
|
-
<>
|
|
1767
|
-
<a
|
|
1768
|
-
class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
|
|
1769
|
-
href={recordingUrl() ?? undefined}
|
|
1770
|
-
target="_blank"
|
|
1771
|
-
rel="noreferrer"
|
|
1772
|
-
>
|
|
1773
|
-
<Video class="h-4 w-4 shrink-0" />
|
|
1774
|
-
<span class="truncate">
|
|
1775
|
-
screen-recording.webm ({formatBytes(blob().size)},{" "}
|
|
1776
|
-
{formatDuration(recordingDurationMs())})
|
|
1777
|
-
</span>
|
|
1778
|
-
</a>
|
|
1632
|
+
<Show when={isVideoRecordingEnabled()}>
|
|
1633
|
+
<div>
|
|
1634
|
+
<label class="mb-2 block text-sm font-medium">Video-opname</label>
|
|
1635
|
+
<div class="space-y-2 rounded-md border bg-background p-3">
|
|
1636
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
1637
|
+
<Show
|
|
1638
|
+
when={recordingState() === "recording"}
|
|
1639
|
+
fallback={
|
|
1779
1640
|
<button
|
|
1780
1641
|
type="button"
|
|
1781
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
|
|
1782
|
-
disabled={mode() === "submitting"}
|
|
1783
|
-
onClick={
|
|
1642
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
|
|
1643
|
+
disabled={mode() === "submitting" || !supportsScreenRecording()}
|
|
1644
|
+
onClick={startRecording}
|
|
1784
1645
|
>
|
|
1785
|
-
<
|
|
1786
|
-
|
|
1646
|
+
<Video class="h-4 w-4" />
|
|
1647
|
+
Neem video op
|
|
1787
1648
|
</button>
|
|
1788
|
-
|
|
1789
|
-
|
|
1649
|
+
}
|
|
1650
|
+
>
|
|
1651
|
+
<button
|
|
1652
|
+
type="button"
|
|
1653
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
|
|
1654
|
+
disabled={mode() === "submitting"}
|
|
1655
|
+
onClick={stopRecording}
|
|
1656
|
+
>
|
|
1657
|
+
<StopCircle class="h-4 w-4" />
|
|
1658
|
+
Stop opname
|
|
1659
|
+
</button>
|
|
1660
|
+
</Show>
|
|
1661
|
+
|
|
1662
|
+
<Show when={recordingBlob()}>
|
|
1663
|
+
{(blob) => (
|
|
1664
|
+
<>
|
|
1665
|
+
<a
|
|
1666
|
+
class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
|
|
1667
|
+
href={recordingUrl() ?? undefined}
|
|
1668
|
+
target="_blank"
|
|
1669
|
+
rel="noreferrer"
|
|
1670
|
+
>
|
|
1671
|
+
<Video class="h-4 w-4 shrink-0" />
|
|
1672
|
+
<span class="truncate">
|
|
1673
|
+
screen-recording.webm ({formatBytes(blob().size)},{" "}
|
|
1674
|
+
{formatDuration(recordingDurationMs())})
|
|
1675
|
+
</span>
|
|
1676
|
+
</a>
|
|
1677
|
+
<button
|
|
1678
|
+
type="button"
|
|
1679
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
|
|
1680
|
+
disabled={mode() === "submitting"}
|
|
1681
|
+
onClick={clearRecording}
|
|
1682
|
+
>
|
|
1683
|
+
<Trash2 class="h-4 w-4" />
|
|
1684
|
+
Wissen
|
|
1685
|
+
</button>
|
|
1686
|
+
</>
|
|
1687
|
+
)}
|
|
1688
|
+
</Show>
|
|
1689
|
+
</div>
|
|
1690
|
+
<Show when={!supportsScreenRecording()}>
|
|
1691
|
+
<div class="text-xs text-muted-foreground">
|
|
1692
|
+
Video-opname is niet beschikbaar in deze browser.
|
|
1693
|
+
</div>
|
|
1790
1694
|
</Show>
|
|
1791
1695
|
</div>
|
|
1792
|
-
<Show when={!supportsScreenRecording()}>
|
|
1793
|
-
<div class="text-xs text-muted-foreground">
|
|
1794
|
-
Video-opname is niet beschikbaar in deze browser.
|
|
1795
|
-
</div>
|
|
1796
|
-
</Show>
|
|
1797
1696
|
</div>
|
|
1798
|
-
</
|
|
1697
|
+
</Show>
|
|
1799
1698
|
|
|
1800
1699
|
<Accordion title="Broncontext" defaultOpen>
|
|
1801
1700
|
<div class="space-y-1">
|
|
@@ -1842,11 +1741,11 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1842
1741
|
onClick={submitFeedback}
|
|
1843
1742
|
>
|
|
1844
1743
|
<Show
|
|
1845
|
-
when={submitTarget() === "
|
|
1744
|
+
when={submitTarget() === "feedback"}
|
|
1846
1745
|
fallback={
|
|
1847
1746
|
<>
|
|
1848
1747
|
<Send class="h-4 w-4" />
|
|
1849
|
-
Verstuur
|
|
1748
|
+
Verstuur feedback
|
|
1850
1749
|
</>
|
|
1851
1750
|
}
|
|
1852
1751
|
>
|
|
@@ -1854,92 +1753,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1854
1753
|
Versturen...
|
|
1855
1754
|
</Show>
|
|
1856
1755
|
</button>
|
|
1857
|
-
<button
|
|
1858
|
-
type="button"
|
|
1859
|
-
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-4 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60"
|
|
1860
|
-
disabled={
|
|
1861
|
-
mode() === "submitting" ||
|
|
1862
|
-
!prompt().trim() ||
|
|
1863
|
-
!props.locator.config.startAgentSession
|
|
1864
|
-
}
|
|
1865
|
-
onClick={startAgentSession}
|
|
1866
|
-
title={
|
|
1867
|
-
props.locator.config.startAgentSession
|
|
1868
|
-
? "Start agent met deze feedback"
|
|
1869
|
-
: "Agent sessies zijn nog niet gekoppeld"
|
|
1870
|
-
}
|
|
1871
|
-
>
|
|
1872
|
-
<Show
|
|
1873
|
-
when={submitTarget() === "agent"}
|
|
1874
|
-
fallback={
|
|
1875
|
-
<>
|
|
1876
|
-
<Bot class="h-4 w-4" />
|
|
1877
|
-
Start agent
|
|
1878
|
-
</>
|
|
1879
|
-
}
|
|
1880
|
-
>
|
|
1881
|
-
<LoaderCircle class="h-4 w-4 animate-spin" />
|
|
1882
|
-
Agent starten...
|
|
1883
|
-
</Show>
|
|
1884
|
-
</button>
|
|
1885
|
-
<div class="grid grid-cols-2 gap-2">
|
|
1886
|
-
<button
|
|
1887
|
-
type="button"
|
|
1888
|
-
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-3 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60"
|
|
1889
|
-
disabled={
|
|
1890
|
-
mode() === "submitting" ||
|
|
1891
|
-
!prompt().trim() ||
|
|
1892
|
-
!props.locator.config.startCodexThread
|
|
1893
|
-
}
|
|
1894
|
-
onClick={startCodexThread}
|
|
1895
|
-
title={
|
|
1896
|
-
props.locator.config.startCodexThread
|
|
1897
|
-
? "Start Codex thread in plan mode"
|
|
1898
|
-
: "Codex threads zijn nog niet gekoppeld"
|
|
1899
|
-
}
|
|
1900
|
-
>
|
|
1901
|
-
<Show
|
|
1902
|
-
when={submitTarget() === "codex"}
|
|
1903
|
-
fallback={
|
|
1904
|
-
<>
|
|
1905
|
-
<Bot class="h-4 w-4" />
|
|
1906
|
-
Codex
|
|
1907
|
-
</>
|
|
1908
|
-
}
|
|
1909
|
-
>
|
|
1910
|
-
<LoaderCircle class="h-4 w-4 animate-spin" />
|
|
1911
|
-
Codex...
|
|
1912
|
-
</Show>
|
|
1913
|
-
</button>
|
|
1914
|
-
<button
|
|
1915
|
-
type="button"
|
|
1916
|
-
class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-3 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-60"
|
|
1917
|
-
disabled={
|
|
1918
|
-
mode() === "submitting" ||
|
|
1919
|
-
!prompt().trim() ||
|
|
1920
|
-
!props.locator.config.startOpenCodeThread
|
|
1921
|
-
}
|
|
1922
|
-
onClick={startOpenCodeThread}
|
|
1923
|
-
title={
|
|
1924
|
-
props.locator.config.startOpenCodeThread
|
|
1925
|
-
? "Start OpenCode sessie in plan mode"
|
|
1926
|
-
: "OpenCode sessies zijn nog niet gekoppeld"
|
|
1927
|
-
}
|
|
1928
|
-
>
|
|
1929
|
-
<Show
|
|
1930
|
-
when={submitTarget() === "opencode"}
|
|
1931
|
-
fallback={
|
|
1932
|
-
<>
|
|
1933
|
-
<Bot class="h-4 w-4" />
|
|
1934
|
-
OpenCode
|
|
1935
|
-
</>
|
|
1936
|
-
}
|
|
1937
|
-
>
|
|
1938
|
-
<LoaderCircle class="h-4 w-4 animate-spin" />
|
|
1939
|
-
OpenCode...
|
|
1940
|
-
</Show>
|
|
1941
|
-
</button>
|
|
1942
|
-
</div>
|
|
1943
1756
|
</div>
|
|
1944
1757
|
</aside>
|
|
1945
1758
|
</div>
|
|
@@ -2111,83 +1924,6 @@ function formatBytes(size: number) {
|
|
|
2111
1924
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
2112
1925
|
}
|
|
2113
1926
|
|
|
2114
|
-
function openPendingAgentSessionWindow() {
|
|
2115
|
-
return openPendingExternalWindow(
|
|
2116
|
-
"Feedback agent starten...",
|
|
2117
|
-
"De context wordt voorbereid en de agent chat wordt geopend.",
|
|
2118
|
-
);
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
function openAgentSessionWindow(
|
|
2122
|
-
session: { sessionUrl?: string },
|
|
2123
|
-
pendingWindow?: Window | null,
|
|
2124
|
-
) {
|
|
2125
|
-
if (typeof window === "undefined" || !session.sessionUrl) {
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
if (pendingWindow && !pendingWindow.closed) {
|
|
2130
|
-
pendingWindow.location.href = session.sessionUrl;
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
window.open(session.sessionUrl, "_blank", "noopener,noreferrer");
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
function openPendingExternalWindow(title: string, message: string) {
|
|
2138
|
-
if (typeof window === "undefined") {
|
|
2139
|
-
return null;
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
const pendingWindow = window.open("about:blank", "_blank");
|
|
2143
|
-
|
|
2144
|
-
if (!pendingWindow) {
|
|
2145
|
-
return null;
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
try {
|
|
2149
|
-
pendingWindow.opener = null;
|
|
2150
|
-
pendingWindow.document.title = title;
|
|
2151
|
-
pendingWindow.document.body.innerHTML = `<main style="font-family: system-ui, sans-serif; padding: 24px;"><h1 style="font-size: 18px;">${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></main>`;
|
|
2152
|
-
} catch {
|
|
2153
|
-
// Cross-browser popup handling can deny document writes; navigation still works.
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
|
-
return pendingWindow;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
function openExternalWindow(url: string | undefined, pendingWindow?: Window | null) {
|
|
2160
|
-
if (typeof window === "undefined" || !url) {
|
|
2161
|
-
return;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
if (pendingWindow && !pendingWindow.closed) {
|
|
2165
|
-
pendingWindow.location.href = url;
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
function closePendingWindow(pendingWindow?: Window | null) {
|
|
2173
|
-
try {
|
|
2174
|
-
if (pendingWindow && !pendingWindow.closed) {
|
|
2175
|
-
pendingWindow.close();
|
|
2176
|
-
}
|
|
2177
|
-
} catch {
|
|
2178
|
-
// Ignore popup cleanup errors.
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
function escapeHtml(value: string) {
|
|
2183
|
-
return value
|
|
2184
|
-
.replace(/&/g, "&")
|
|
2185
|
-
.replace(/</g, "<")
|
|
2186
|
-
.replace(/>/g, ">")
|
|
2187
|
-
.replace(/"/g, """)
|
|
2188
|
-
.replace(/'/g, "'");
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
1927
|
function formatDuration(durationMs: number | null) {
|
|
2192
1928
|
if (!durationMs) {
|
|
2193
1929
|
return "0s";
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
:where([data-feedback-locator-ui="true"]) {
|
|
2
|
+
--background: 0 0% 3%;
|
|
3
|
+
--foreground: 0 0% 96%;
|
|
4
|
+
--muted: 0 0% 9%;
|
|
5
|
+
--muted-foreground: 0 0% 66%;
|
|
6
|
+
--border: 0 0% 18%;
|
|
7
|
+
--primary: 45 96% 53%;
|
|
8
|
+
--primary-foreground: 30 70% 8%;
|
|
9
|
+
--destructive: 0 84% 60%;
|
|
10
|
+
--ring: 45 96% 53%;
|
|
11
|
+
font-family:
|
|
12
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:where([data-feedback-locator-ui="true"], [data-feedback-locator-ui="true"] *) {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:where([data-feedback-locator-ui="true"] button, [data-feedback-locator-ui="true"] textarea, [data-feedback-locator-ui="true"] input) {
|
|
20
|
+
font: inherit;
|
|
21
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const defaultFeedbackLocatorStyles = `
|
|
2
|
+
:host {
|
|
3
|
+
color-scheme: dark;
|
|
4
|
+
--background: 0 0% 3%;
|
|
5
|
+
--foreground: 0 0% 96%;
|
|
6
|
+
--muted: 0 0% 9%;
|
|
7
|
+
--muted-foreground: 0 0% 66%;
|
|
8
|
+
--border: 0 0% 18%;
|
|
9
|
+
--primary: 45 96% 53%;
|
|
10
|
+
--primary-foreground: 30 70% 8%;
|
|
11
|
+
--destructive: 0 84% 60%;
|
|
12
|
+
--ring: 45 96% 53%;
|
|
13
|
+
font-family:
|
|
14
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
[data-feedback-locator-ui="true"],
|
|
18
|
+
[data-feedback-locator-ui="true"] * {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
[data-feedback-locator-ui="true"] button,
|
|
23
|
+
[data-feedback-locator-ui="true"] textarea,
|
|
24
|
+
[data-feedback-locator-ui="true"] input {
|
|
25
|
+
font: inherit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[data-feedback-locator-ui="true"] button {
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[data-feedback-locator-ui="true"] button:disabled {
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
}
|
|
35
|
+
`;
|
package/src/types.ts
CHANGED
|
@@ -143,7 +143,24 @@ export type FeedbackLocatorLinearIssueReference = {
|
|
|
143
143
|
issueUrl: string;
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
export type FeedbackLocatorSubmitResult = FeedbackLocatorLinearIssueReference
|
|
146
|
+
export type FeedbackLocatorSubmitResult = Partial<FeedbackLocatorLinearIssueReference> & {
|
|
147
|
+
submissionId?: string;
|
|
148
|
+
appId?: string;
|
|
149
|
+
appKey?: string;
|
|
150
|
+
linearIssue?: FeedbackLocatorLinearIssueReference;
|
|
151
|
+
agentJob?: {
|
|
152
|
+
id: string;
|
|
153
|
+
provider: "codex" | "opencode";
|
|
154
|
+
status: string;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export type FeedbackLocatorHostedSubmitResult = FeedbackLocatorSubmitResult;
|
|
158
|
+
export type FeedbackLocatorCloseReason =
|
|
159
|
+
| "user"
|
|
160
|
+
| "submitted"
|
|
161
|
+
| "escape"
|
|
162
|
+
| "outside"
|
|
163
|
+
| "error";
|
|
147
164
|
|
|
148
165
|
export type FeedbackLocatorAgentSessionResult = {
|
|
149
166
|
sessionId: string;
|
|
@@ -174,6 +191,12 @@ export type FeedbackLocatorSourceCollector = (
|
|
|
174
191
|
target: HTMLElement,
|
|
175
192
|
) => FeedbackLocatorSourceMetadata | null;
|
|
176
193
|
|
|
194
|
+
export type FeedbackLocatorTheme = {
|
|
195
|
+
primary?: string;
|
|
196
|
+
primaryForeground?: string;
|
|
197
|
+
ring?: string;
|
|
198
|
+
};
|
|
199
|
+
|
|
177
200
|
export type FeedbackLocatorConfig = {
|
|
178
201
|
appKey: string;
|
|
179
202
|
appName: string;
|
|
@@ -191,6 +214,9 @@ export type FeedbackLocatorConfig = {
|
|
|
191
214
|
) => Promise<FeedbackLocatorOpenCodeThreadResult>;
|
|
192
215
|
contextProviders?: FeedbackLocatorContextProvider[];
|
|
193
216
|
hotkey?: FeedbackLocatorHotkey;
|
|
217
|
+
showFloatingButton?: boolean;
|
|
218
|
+
enableVideoRecording?: boolean;
|
|
219
|
+
theme?: FeedbackLocatorTheme;
|
|
194
220
|
sourceCollector?: FeedbackLocatorSourceCollector;
|
|
195
221
|
};
|
|
196
222
|
|
|
@@ -200,6 +226,9 @@ export type FeedbackLocator = {
|
|
|
200
226
|
FeedbackLocatorConfig,
|
|
201
227
|
| "contextProviders"
|
|
202
228
|
| "hotkey"
|
|
229
|
+
| "showFloatingButton"
|
|
230
|
+
| "enableVideoRecording"
|
|
231
|
+
| "theme"
|
|
203
232
|
| "sourceCollector"
|
|
204
233
|
| "startAgentSession"
|
|
205
234
|
| "startCodexThread"
|
package/src/web-component.tsx
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { render } from "solid-js/web";
|
|
2
2
|
import { FeedbackLocatorRoot } from "./solid";
|
|
3
|
-
import
|
|
3
|
+
import { defaultFeedbackLocatorStyles } from "./styles";
|
|
4
|
+
import type {
|
|
5
|
+
FeedbackLocator,
|
|
6
|
+
FeedbackLocatorCloseReason,
|
|
7
|
+
FeedbackLocatorHostedSubmitResult,
|
|
8
|
+
} from "./types";
|
|
4
9
|
|
|
5
10
|
export type FeedbackLocatorCustomElementOptions = {
|
|
6
11
|
tagName?: string;
|
|
@@ -10,6 +15,8 @@ export type FeedbackLocatorCustomElementOptions = {
|
|
|
10
15
|
|
|
11
16
|
export type FeedbackLocatorCustomElement = HTMLElement & {
|
|
12
17
|
locator: FeedbackLocator | null;
|
|
18
|
+
open: boolean;
|
|
19
|
+
showFloatingButton: boolean;
|
|
13
20
|
};
|
|
14
21
|
|
|
15
22
|
export const defaultFeedbackLocatorElementName = "d2d-feedbackkit";
|
|
@@ -26,9 +33,15 @@ export function defineFeedbackLocatorElement(
|
|
|
26
33
|
|
|
27
34
|
class FeedbackKitElement extends HTMLElement {
|
|
28
35
|
#locator: FeedbackLocator | null = null;
|
|
36
|
+
#open = false;
|
|
37
|
+
#showFloatingButton = true;
|
|
29
38
|
#dispose: (() => void) | undefined;
|
|
30
39
|
#mount: HTMLElement | undefined;
|
|
31
40
|
|
|
41
|
+
static get observedAttributes() {
|
|
42
|
+
return ["open", "show-floating-button"];
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
get locator() {
|
|
33
46
|
return this.#locator;
|
|
34
47
|
}
|
|
@@ -38,10 +51,51 @@ export function defineFeedbackLocatorElement(
|
|
|
38
51
|
this.#render();
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
get open() {
|
|
55
|
+
return this.#open;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
set open(open: boolean) {
|
|
59
|
+
const nextOpen = Boolean(open);
|
|
60
|
+
if (this.#open === nextOpen) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.#open = nextOpen;
|
|
65
|
+
this.#syncBooleanAttribute("open", nextOpen);
|
|
66
|
+
this.#dispatchOpenChange(nextOpen);
|
|
67
|
+
this.#render();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get showFloatingButton() {
|
|
71
|
+
return this.#showFloatingButton;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set showFloatingButton(showFloatingButton: boolean) {
|
|
75
|
+
const nextShowFloatingButton = Boolean(showFloatingButton);
|
|
76
|
+
if (this.#showFloatingButton === nextShowFloatingButton) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.#showFloatingButton = nextShowFloatingButton;
|
|
81
|
+
this.#syncBooleanAttribute("show-floating-button", nextShowFloatingButton);
|
|
82
|
+
this.#render();
|
|
83
|
+
}
|
|
84
|
+
|
|
41
85
|
connectedCallback() {
|
|
42
86
|
this.#render();
|
|
43
87
|
}
|
|
44
88
|
|
|
89
|
+
attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
|
|
90
|
+
if (name === "open") {
|
|
91
|
+
this.open = newValue !== null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (name === "show-floating-button") {
|
|
95
|
+
this.showFloatingButton = newValue !== null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
45
99
|
disconnectedCallback() {
|
|
46
100
|
this.#dispose?.();
|
|
47
101
|
this.#dispose = undefined;
|
|
@@ -57,7 +111,20 @@ export function defineFeedbackLocatorElement(
|
|
|
57
111
|
this.#mount = this.#mount ?? this.#createMount();
|
|
58
112
|
this.#mount.replaceChildren();
|
|
59
113
|
this.#dispose = render(
|
|
60
|
-
() =>
|
|
114
|
+
() => (
|
|
115
|
+
<FeedbackLocatorRoot
|
|
116
|
+
locator={this.#locator!}
|
|
117
|
+
open={this.#open}
|
|
118
|
+
showFloatingButton={this.#showFloatingButton}
|
|
119
|
+
onOpenChange={(open) => {
|
|
120
|
+
this.#open = open;
|
|
121
|
+
this.#syncBooleanAttribute("open", open);
|
|
122
|
+
this.#dispatchOpenChange(open);
|
|
123
|
+
}}
|
|
124
|
+
onClose={(reason) => this.#dispatchClose(reason)}
|
|
125
|
+
onSubmitted={(result) => this.#dispatchSubmitted(result)}
|
|
126
|
+
/>
|
|
127
|
+
),
|
|
61
128
|
this.#mount,
|
|
62
129
|
);
|
|
63
130
|
}
|
|
@@ -73,8 +140,9 @@ export function defineFeedbackLocatorElement(
|
|
|
73
140
|
: options.styles
|
|
74
141
|
? [options.styles]
|
|
75
142
|
: [];
|
|
143
|
+
const allStyles = [defaultFeedbackLocatorStyles, ...styles];
|
|
76
144
|
|
|
77
|
-
for (const styleText of
|
|
145
|
+
for (const styleText of allStyles) {
|
|
78
146
|
const style = document.createElement("style");
|
|
79
147
|
style.textContent = styleText;
|
|
80
148
|
root.append(style);
|
|
@@ -84,6 +152,46 @@ export function defineFeedbackLocatorElement(
|
|
|
84
152
|
root.append(mount);
|
|
85
153
|
return mount;
|
|
86
154
|
}
|
|
155
|
+
|
|
156
|
+
#syncBooleanAttribute(name: string, enabled: boolean) {
|
|
157
|
+
if (enabled && !this.hasAttribute(name)) {
|
|
158
|
+
this.setAttribute(name, "");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!enabled && this.hasAttribute(name)) {
|
|
162
|
+
this.removeAttribute(name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#dispatchOpenChange(open: boolean) {
|
|
167
|
+
this.dispatchEvent(
|
|
168
|
+
new CustomEvent("feedbackkit-open-change", {
|
|
169
|
+
bubbles: true,
|
|
170
|
+
composed: true,
|
|
171
|
+
detail: { open },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#dispatchClose(reason: FeedbackLocatorCloseReason) {
|
|
177
|
+
this.dispatchEvent(
|
|
178
|
+
new CustomEvent("feedbackkit-close", {
|
|
179
|
+
bubbles: true,
|
|
180
|
+
composed: true,
|
|
181
|
+
detail: { reason },
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#dispatchSubmitted(result: FeedbackLocatorHostedSubmitResult) {
|
|
187
|
+
this.dispatchEvent(
|
|
188
|
+
new CustomEvent("feedbackkit-submitted", {
|
|
189
|
+
bubbles: true,
|
|
190
|
+
composed: true,
|
|
191
|
+
detail: { result },
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
87
195
|
}
|
|
88
196
|
|
|
89
197
|
customElements.define(tagName, FeedbackKitElement);
|