yo-bug 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/cli.ts +17 -0
- package/bin/install.ts +34 -0
- package/bin/mcp.ts +2 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +19 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/install.d.ts +3 -0
- package/dist/bin/install.d.ts.map +1 -0
- package/dist/bin/install.js +28 -0
- package/dist/bin/install.js.map +1 -0
- package/dist/bin/mcp.d.ts +3 -0
- package/dist/bin/mcp.d.ts.map +1 -0
- package/dist/bin/mcp.js +3 -0
- package/dist/bin/mcp.js.map +1 -0
- package/dist/src/detect/dev-server.d.ts +11 -0
- package/dist/src/detect/dev-server.d.ts.map +1 -0
- package/dist/src/detect/dev-server.js +92 -0
- package/dist/src/detect/dev-server.js.map +1 -0
- package/dist/src/detect/spawn-dev.d.ts +4 -0
- package/dist/src/detect/spawn-dev.d.ts.map +1 -0
- package/dist/src/detect/spawn-dev.js +37 -0
- package/dist/src/detect/spawn-dev.js.map +1 -0
- package/dist/src/install/detect-ai-tool.d.ts +7 -0
- package/dist/src/install/detect-ai-tool.d.ts.map +1 -0
- package/dist/src/install/detect-ai-tool.js +38 -0
- package/dist/src/install/detect-ai-tool.js.map +1 -0
- package/dist/src/install/write-mcp-config.d.ts +6 -0
- package/dist/src/install/write-mcp-config.d.ts.map +1 -0
- package/dist/src/install/write-mcp-config.js +47 -0
- package/dist/src/install/write-mcp-config.js.map +1 -0
- package/dist/src/mcp/index.d.ts +2 -0
- package/dist/src/mcp/index.d.ts.map +1 -0
- package/dist/src/mcp/index.js +263 -0
- package/dist/src/mcp/index.js.map +1 -0
- package/dist/src/mcp/tools/checklist.d.ts +22 -0
- package/dist/src/mcp/tools/checklist.d.ts.map +1 -0
- package/dist/src/mcp/tools/checklist.js +58 -0
- package/dist/src/mcp/tools/checklist.js.map +1 -0
- package/dist/src/mcp/tools/get-feedback.d.ts +13 -0
- package/dist/src/mcp/tools/get-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/get-feedback.js +27 -0
- package/dist/src/mcp/tools/get-feedback.js.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts +20 -0
- package/dist/src/mcp/tools/list-feedbacks.d.ts.map +1 -0
- package/dist/src/mcp/tools/list-feedbacks.js +29 -0
- package/dist/src/mcp/tools/list-feedbacks.js.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts +8 -0
- package/dist/src/mcp/tools/resolve-feedback.d.ts.map +1 -0
- package/dist/src/mcp/tools/resolve-feedback.js +28 -0
- package/dist/src/mcp/tools/resolve-feedback.js.map +1 -0
- package/dist/src/mcp/tools/start-session.d.ts +11 -0
- package/dist/src/mcp/tools/start-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/start-session.js +56 -0
- package/dist/src/mcp/tools/start-session.js.map +1 -0
- package/dist/src/mcp/tools/stop-session.d.ts +6 -0
- package/dist/src/mcp/tools/stop-session.d.ts.map +1 -0
- package/dist/src/mcp/tools/stop-session.js +80 -0
- package/dist/src/mcp/tools/stop-session.js.map +1 -0
- package/dist/src/mcp/tools/test-history.d.ts +33 -0
- package/dist/src/mcp/tools/test-history.d.ts.map +1 -0
- package/dist/src/mcp/tools/test-history.js +66 -0
- package/dist/src/mcp/tools/test-history.js.map +1 -0
- package/dist/src/server/feedback-api.d.ts +4 -0
- package/dist/src/server/feedback-api.d.ts.map +1 -0
- package/dist/src/server/feedback-api.js +152 -0
- package/dist/src/server/feedback-api.js.map +1 -0
- package/dist/src/server/html-injector.d.ts +10 -0
- package/dist/src/server/html-injector.d.ts.map +1 -0
- package/dist/src/server/html-injector.js +34 -0
- package/dist/src/server/html-injector.js.map +1 -0
- package/dist/src/server/proxy-server.d.ts +8 -0
- package/dist/src/server/proxy-server.d.ts.map +1 -0
- package/dist/src/server/proxy-server.js +87 -0
- package/dist/src/server/proxy-server.js.map +1 -0
- package/dist/src/server/sdk-serve.d.ts +3 -0
- package/dist/src/server/sdk-serve.d.ts.map +1 -0
- package/dist/src/server/sdk-serve.js +20 -0
- package/dist/src/server/sdk-serve.js.map +1 -0
- package/dist/src/storage/store.d.ts +21 -0
- package/dist/src/storage/store.d.ts.map +1 -0
- package/dist/src/storage/store.js +138 -0
- package/dist/src/storage/store.js.map +1 -0
- package/dist/src/storage/types.d.ts +74 -0
- package/dist/src/storage/types.d.ts.map +1 -0
- package/dist/src/storage/types.js +2 -0
- package/dist/src/storage/types.js.map +1 -0
- package/dist/vibe-feedback.js +399 -0
- package/package.json +67 -0
- package/src/client/annotation-mode/canvas-editor.ts +178 -0
- package/src/client/annotation-mode/history.ts +32 -0
- package/src/client/annotation-mode/region-selector.ts +123 -0
- package/src/client/annotation-mode/screenshot.ts +17 -0
- package/src/client/annotation-mode/toolbar.ts +139 -0
- package/src/client/annotation-mode/tools/arrow.ts +57 -0
- package/src/client/annotation-mode/tools/base-tool.ts +25 -0
- package/src/client/annotation-mode/tools/circle.ts +37 -0
- package/src/client/annotation-mode/tools/freehand.ts +23 -0
- package/src/client/annotation-mode/tools/rect.ts +32 -0
- package/src/client/annotation-mode/tools/text.ts +93 -0
- package/src/client/api/client.ts +48 -0
- package/src/client/capture/action-recorder.ts +157 -0
- package/src/client/capture/console-interceptor.ts +65 -0
- package/src/client/capture/context-buffer.ts +23 -0
- package/src/client/capture/error-interceptor.ts +52 -0
- package/src/client/capture/network-interceptor.ts +143 -0
- package/src/client/core/event-bus.ts +20 -0
- package/src/client/core/i18n.ts +83 -0
- package/src/client/core/sdk.ts +373 -0
- package/src/client/core/shadow-host.ts +27 -0
- package/src/client/element-mode/highlighter.ts +79 -0
- package/src/client/element-mode/inspector.ts +73 -0
- package/src/client/element-mode/selector.ts +22 -0
- package/src/client/index.ts +10 -0
- package/src/client/styles/sdk.css.ts +222 -0
- package/src/client/ui/checklist-panel.ts +279 -0
- package/src/client/ui/feedback-panel.ts +149 -0
- package/src/client/ui/floating-button.ts +103 -0
- package/src/client/ui/toast.ts +17 -0
- package/src/client/ui/verify-panel.ts +111 -0
- package/src/detect/dev-server.ts +110 -0
- package/src/detect/spawn-dev.ts +50 -0
- package/src/install/detect-ai-tool.ts +49 -0
- package/src/install/write-mcp-config.ts +49 -0
- package/src/mcp/index.ts +327 -0
- package/src/mcp/tools/checklist.ts +61 -0
- package/src/mcp/tools/get-feedback.ts +34 -0
- package/src/mcp/tools/list-feedbacks.ts +37 -0
- package/src/mcp/tools/resolve-feedback.ts +34 -0
- package/src/mcp/tools/start-session.ts +65 -0
- package/src/mcp/tools/stop-session.ts +93 -0
- package/src/mcp/tools/test-history.ts +97 -0
- package/src/server/feedback-api.ts +164 -0
- package/src/server/html-injector.ts +41 -0
- package/src/server/proxy-server.ts +107 -0
- package/src/server/sdk-serve.ts +24 -0
- package/src/storage/store.ts +172 -0
- package/src/storage/types.ts +68 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const zhCN: Record<string, string> = {
|
|
2
|
+
// Floating button
|
|
3
|
+
'hint.firstTime': '\u53D1\u73B0\u95EE\u9898\uFF1F\u70B9\u6211\u6216\u6309 Alt+Q \u5FEB\u901F\u6807\u8BB0',
|
|
4
|
+
// Mode bar
|
|
5
|
+
'mode.quick': '\u5FEB\u901F\u6807\u8BB0',
|
|
6
|
+
'mode.quick.hint': '\u70B9\u51FB\u95EE\u9898\u5143\u7D20',
|
|
7
|
+
'mode.detail': '\u8BE6\u7EC6\u63CF\u8FF0',
|
|
8
|
+
'mode.detail.hint': '\u70B9\u51FB + \u586B\u5199',
|
|
9
|
+
'mode.screenshot': '\u622A\u56FE\u6807\u6CE8',
|
|
10
|
+
'mode.screenshot.hint': '\u6846\u9009\u533A\u57DF + \u753B',
|
|
11
|
+
// Feedback panel
|
|
12
|
+
'panel.element.title': '\u5143\u7D20\u53CD\u9988',
|
|
13
|
+
'panel.annotation.title': '\u622A\u56FE\u53CD\u9988',
|
|
14
|
+
'panel.type': '\u95EE\u9898\u7C7B\u578B',
|
|
15
|
+
'panel.description': '\u95EE\u9898\u63CF\u8FF0',
|
|
16
|
+
'panel.description.placeholder': '\u63CF\u8FF0\u4F60\u53D1\u73B0\u7684\u95EE\u9898...',
|
|
17
|
+
'panel.cancel': '\u53D6\u6D88',
|
|
18
|
+
'panel.submit': '\u63D0\u4EA4',
|
|
19
|
+
// Toast
|
|
20
|
+
'toast.flagged': '\u5DF2\u6807\u8BB0',
|
|
21
|
+
'toast.submitted': '\u53CD\u9988\u5DF2\u63D0\u4EA4',
|
|
22
|
+
'toast.failed': '\u63D0\u4EA4\u5931\u8D25',
|
|
23
|
+
'toast.screenshotFailed': '\u622A\u56FE\u5931\u8D25',
|
|
24
|
+
'toast.clickToFlag': '\u70B9\u51FB\u95EE\u9898\u5143\u7D20\u6765\u6807\u8BB0',
|
|
25
|
+
// Region selector
|
|
26
|
+
'region.hint': '\u62D6\u62FD\u9009\u62E9\u533A\u57DF \xB7 ESC \u53D6\u6D88',
|
|
27
|
+
// Verify
|
|
28
|
+
'verify.title': '\u9A8C\u8BC1\u4FEE\u590D',
|
|
29
|
+
'verify.fixed': '\u5DF2\u4FEE\u590D',
|
|
30
|
+
'verify.broken': '\u672A\u4FEE\u590D',
|
|
31
|
+
// Problem types
|
|
32
|
+
'type.bug': 'Bug',
|
|
33
|
+
'type.ui-issue': 'UI',
|
|
34
|
+
'type.performance': 'Performance',
|
|
35
|
+
'type.feature-request': 'Feature',
|
|
36
|
+
'type.other': 'Other',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const en: Record<string, string> = {
|
|
40
|
+
'hint.firstTime': 'Found a problem? Click me or press Alt+Q to flag it.',
|
|
41
|
+
'mode.quick': 'Quick',
|
|
42
|
+
'mode.quick.hint': 'Tap problem',
|
|
43
|
+
'mode.detail': 'Describe',
|
|
44
|
+
'mode.detail.hint': 'Tap + fill form',
|
|
45
|
+
'mode.screenshot': 'Screenshot',
|
|
46
|
+
'mode.screenshot.hint': 'Select area + draw',
|
|
47
|
+
'panel.element.title': 'Element Feedback',
|
|
48
|
+
'panel.annotation.title': 'Annotation Feedback',
|
|
49
|
+
'panel.type': 'Problem Type',
|
|
50
|
+
'panel.description': 'Description',
|
|
51
|
+
'panel.description.placeholder': 'Describe the issue...',
|
|
52
|
+
'panel.cancel': 'Cancel',
|
|
53
|
+
'panel.submit': 'Submit',
|
|
54
|
+
'toast.flagged': 'Flagged!',
|
|
55
|
+
'toast.submitted': 'Feedback submitted',
|
|
56
|
+
'toast.failed': 'Submit failed',
|
|
57
|
+
'toast.screenshotFailed': 'Screenshot failed',
|
|
58
|
+
'toast.clickToFlag': 'Click the problem element to flag it',
|
|
59
|
+
'region.hint': 'Drag to select area \xB7 ESC to cancel',
|
|
60
|
+
'verify.title': 'Verify Fix',
|
|
61
|
+
'verify.fixed': 'Fixed',
|
|
62
|
+
'verify.broken': 'Still broken',
|
|
63
|
+
'type.bug': 'Bug',
|
|
64
|
+
'type.ui-issue': 'UI',
|
|
65
|
+
'type.performance': 'Performance',
|
|
66
|
+
'type.feature-request': 'Feature',
|
|
67
|
+
'type.other': 'Other',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let currentLang: Record<string, string> = en;
|
|
71
|
+
|
|
72
|
+
export function initI18n(): void {
|
|
73
|
+
const htmlLang = document.documentElement.lang?.toLowerCase() || '';
|
|
74
|
+
if (htmlLang.startsWith('zh')) {
|
|
75
|
+
currentLang = zhCN;
|
|
76
|
+
} else {
|
|
77
|
+
currentLang = en;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function t(key: string): string {
|
|
82
|
+
return currentLang[key] || en[key] || key;
|
|
83
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { ShadowHost } from './shadow-host.js';
|
|
2
|
+
import { EventBus } from './event-bus.js';
|
|
3
|
+
import { FloatingButton } from '../ui/floating-button.js';
|
|
4
|
+
import { FeedbackPanel, type PanelResult } from '../ui/feedback-panel.js';
|
|
5
|
+
import { showToast } from '../ui/toast.js';
|
|
6
|
+
import { ChecklistPanel } from '../ui/checklist-panel.js';
|
|
7
|
+
import { VerifyPanel } from '../ui/verify-panel.js';
|
|
8
|
+
import { Highlighter } from '../element-mode/highlighter.js';
|
|
9
|
+
import { inspectElement, type ElementInfo } from '../element-mode/inspector.js';
|
|
10
|
+
import { captureScreenshot } from '../annotation-mode/screenshot.js';
|
|
11
|
+
import { RegionSelector } from '../annotation-mode/region-selector.js';
|
|
12
|
+
import { CanvasEditor } from '../annotation-mode/canvas-editor.js';
|
|
13
|
+
import { ContextBuffer } from '../capture/context-buffer.js';
|
|
14
|
+
import { ConsoleInterceptor, type ConsoleEntry } from '../capture/console-interceptor.js';
|
|
15
|
+
import { NetworkInterceptor, type NetworkEntry } from '../capture/network-interceptor.js';
|
|
16
|
+
import { ErrorInterceptor, type ErrorEntry } from '../capture/error-interceptor.js';
|
|
17
|
+
import { ApiClient, type FeedbackPayload } from '../api/client.js';
|
|
18
|
+
import { ActionRecorder } from '../capture/action-recorder.js';
|
|
19
|
+
import { t } from './i18n.js';
|
|
20
|
+
|
|
21
|
+
type Mode = 'flag' | 'detail' | 'annotate';
|
|
22
|
+
|
|
23
|
+
export class VibeFeedback {
|
|
24
|
+
private shadowHost: ShadowHost;
|
|
25
|
+
private eventBus: EventBus;
|
|
26
|
+
private fab: FloatingButton;
|
|
27
|
+
private panel: FeedbackPanel;
|
|
28
|
+
private highlighter: Highlighter;
|
|
29
|
+
private apiClient: ApiClient;
|
|
30
|
+
|
|
31
|
+
private actionRecorder = new ActionRecorder();
|
|
32
|
+
private consoleBuffer = new ContextBuffer<ConsoleEntry>(50);
|
|
33
|
+
private networkBuffer = new ContextBuffer<NetworkEntry>(50);
|
|
34
|
+
private errorBuffer = new ContextBuffer<ErrorEntry>(50);
|
|
35
|
+
private consoleInterceptor: ConsoleInterceptor;
|
|
36
|
+
private networkInterceptor: NetworkInterceptor;
|
|
37
|
+
private errorInterceptor: ErrorInterceptor;
|
|
38
|
+
|
|
39
|
+
private checklistPanel: ChecklistPanel | null = null;
|
|
40
|
+
private verifyPanel: VerifyPanel | null = null;
|
|
41
|
+
private active = false;
|
|
42
|
+
private mode: Mode | null = null;
|
|
43
|
+
private modeBar: HTMLDivElement | null = null;
|
|
44
|
+
private selectedElement: ElementInfo | null = null;
|
|
45
|
+
private annotationDataUrl: string | null = null;
|
|
46
|
+
private activeChecklistItemId: number | null = null;
|
|
47
|
+
private keyHandler: (e: KeyboardEvent) => void;
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.shadowHost = new ShadowHost();
|
|
51
|
+
this.eventBus = new EventBus();
|
|
52
|
+
this.apiClient = new ApiClient();
|
|
53
|
+
|
|
54
|
+
// Context capture
|
|
55
|
+
this.consoleInterceptor = new ConsoleInterceptor(this.consoleBuffer);
|
|
56
|
+
this.networkInterceptor = new NetworkInterceptor(this.networkBuffer);
|
|
57
|
+
this.errorInterceptor = new ErrorInterceptor(this.errorBuffer);
|
|
58
|
+
this.consoleInterceptor.install();
|
|
59
|
+
this.networkInterceptor.install();
|
|
60
|
+
this.errorInterceptor.install();
|
|
61
|
+
this.actionRecorder.start();
|
|
62
|
+
|
|
63
|
+
// Checklist + Verify panels
|
|
64
|
+
this.checklistPanel = new ChecklistPanel(
|
|
65
|
+
this.shadowHost.shadow,
|
|
66
|
+
(itemId) => this.onChecklistFail(itemId)
|
|
67
|
+
);
|
|
68
|
+
this.checklistPanel.startPolling();
|
|
69
|
+
this.verifyPanel = new VerifyPanel(this.shadowHost.shadow);
|
|
70
|
+
this.verifyPanel.startPolling();
|
|
71
|
+
|
|
72
|
+
// UI
|
|
73
|
+
this.fab = new FloatingButton(this.shadowHost.shadow, () => this.toggleModeBar());
|
|
74
|
+
this.highlighter = new Highlighter((el) => this.onElementSelected(el));
|
|
75
|
+
this.panel = new FeedbackPanel(
|
|
76
|
+
this.shadowHost.shadow,
|
|
77
|
+
(result) => this.onSubmit(result),
|
|
78
|
+
() => this.cancelFeedback()
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Global keyboard shortcuts — always active
|
|
82
|
+
this.keyHandler = (e: KeyboardEvent) => this.onKeyDown(e);
|
|
83
|
+
document.addEventListener('keydown', this.keyHandler);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ──────── Keyboard shortcuts ────────
|
|
87
|
+
private onKeyDown(e: KeyboardEvent): void {
|
|
88
|
+
// Don't intercept when typing in inputs
|
|
89
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
90
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
91
|
+
// Don't intercept inside our SDK panels
|
|
92
|
+
if (this.isSDKElement(e.target as Element)) return;
|
|
93
|
+
|
|
94
|
+
// All shortcuts use Alt+key (or Option+key on Mac) to avoid conflicts
|
|
95
|
+
if (e.altKey) {
|
|
96
|
+
switch (e.key) {
|
|
97
|
+
case 'q': case 'Q':
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
this.enterMode('flag');
|
|
100
|
+
return;
|
|
101
|
+
case 'd': case 'D':
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
this.enterMode('detail');
|
|
104
|
+
return;
|
|
105
|
+
case 's': case 'S':
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
this.enterMode('annotate');
|
|
108
|
+
return;
|
|
109
|
+
case 'x': case 'X':
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
if (this.active) this.exitTestMode();
|
|
112
|
+
else this.toggleModeBar();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Esc always exits test mode (universal convention)
|
|
117
|
+
if (e.key === 'Escape' && this.active) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
this.exitTestMode();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private isSDKElement(el: Element | null): boolean {
|
|
124
|
+
while (el) {
|
|
125
|
+
if (el.id === 'vibe-feedback-host') return true;
|
|
126
|
+
el = el.parentElement;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ──────── Mode management ────────
|
|
132
|
+
|
|
133
|
+
/** Enter a specific mode directly — skips mode bar */
|
|
134
|
+
private enterMode(mode: Mode): void {
|
|
135
|
+
if (!this.active) {
|
|
136
|
+
this.active = true;
|
|
137
|
+
this.fab.setActive(true);
|
|
138
|
+
}
|
|
139
|
+
this.mode = mode;
|
|
140
|
+
this.hideModeBar();
|
|
141
|
+
this.showModeBar();
|
|
142
|
+
// Highlight active button
|
|
143
|
+
const idx = mode === 'flag' ? 0 : mode === 'detail' ? 1 : 2;
|
|
144
|
+
this.modeBar?.querySelectorAll('.vf-mode-btn').forEach((btn, i) => {
|
|
145
|
+
btn.classList.toggle('active', i === idx);
|
|
146
|
+
});
|
|
147
|
+
this.activateMode();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Toggle mode bar visibility (FAB click) */
|
|
151
|
+
private toggleModeBar(): void {
|
|
152
|
+
if (this.active) {
|
|
153
|
+
this.exitTestMode();
|
|
154
|
+
} else {
|
|
155
|
+
this.active = true;
|
|
156
|
+
this.fab.setActive(true);
|
|
157
|
+
this.mode = null;
|
|
158
|
+
this.showModeBar();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private exitTestMode(): void {
|
|
163
|
+
this.active = false;
|
|
164
|
+
this.fab.setActive(false);
|
|
165
|
+
this.deactivateAll();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private showModeBar(): void {
|
|
169
|
+
if (this.modeBar) return;
|
|
170
|
+
|
|
171
|
+
this.modeBar = document.createElement('div');
|
|
172
|
+
this.modeBar.className = 'vf-mode-bar';
|
|
173
|
+
|
|
174
|
+
const modes: { mode: Mode; label: string; hint: string; key: string }[] = [
|
|
175
|
+
{ mode: 'flag', label: t('mode.quick'), hint: t('mode.quick.hint'), key: 'Alt+Q' },
|
|
176
|
+
{ mode: 'detail', label: t('mode.detail'), hint: t('mode.detail.hint'), key: 'Alt+D' },
|
|
177
|
+
{ mode: 'annotate', label: t('mode.screenshot'), hint: t('mode.screenshot.hint'), key: 'Alt+S' },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
for (const m of modes) {
|
|
181
|
+
const btn = document.createElement('button');
|
|
182
|
+
btn.className = `vf-mode-btn${this.mode === m.mode ? ' active' : ''}`;
|
|
183
|
+
btn.innerHTML = `
|
|
184
|
+
<div style="display:flex;align-items:center;gap:6px;font-size:13px">
|
|
185
|
+
${m.label}
|
|
186
|
+
<span style="font-size:10px;opacity:0.4;background:rgba(255,255,255,0.1);padding:1px 5px;border-radius:3px;">${m.key}</span>
|
|
187
|
+
</div>
|
|
188
|
+
<div style="font-size:10px;opacity:0.5;margin-top:1px">${m.hint}</div>
|
|
189
|
+
`;
|
|
190
|
+
btn.addEventListener('click', () => {
|
|
191
|
+
this.mode = m.mode;
|
|
192
|
+
this.modeBar!.querySelectorAll('.vf-mode-btn').forEach((b) => b.classList.remove('active'));
|
|
193
|
+
btn.classList.add('active');
|
|
194
|
+
this.activateMode();
|
|
195
|
+
});
|
|
196
|
+
this.modeBar.appendChild(btn);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.shadowHost.append(this.modeBar);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private hideModeBar(): void {
|
|
203
|
+
this.modeBar?.remove();
|
|
204
|
+
this.modeBar = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private activateMode(): void {
|
|
208
|
+
this.highlighter.deactivate();
|
|
209
|
+
if (!this.mode) return;
|
|
210
|
+
|
|
211
|
+
if (this.mode === 'flag' || this.mode === 'detail') {
|
|
212
|
+
this.highlighter.activate();
|
|
213
|
+
} else {
|
|
214
|
+
this.startAnnotation();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private deactivateAll(): void {
|
|
219
|
+
this.highlighter.deactivate();
|
|
220
|
+
this.panel.hide();
|
|
221
|
+
this.hideModeBar();
|
|
222
|
+
this.selectedElement = null;
|
|
223
|
+
this.annotationDataUrl = null;
|
|
224
|
+
this.activeChecklistItemId = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ──────── Element selected ────────
|
|
228
|
+
private onElementSelected(el: Element): void {
|
|
229
|
+
this.selectedElement = inspectElement(el);
|
|
230
|
+
|
|
231
|
+
if (this.mode === 'flag') {
|
|
232
|
+
this.quickFlag();
|
|
233
|
+
} else {
|
|
234
|
+
this.highlighter.deactivate();
|
|
235
|
+
this.hideModeBar();
|
|
236
|
+
this.panel.showForElement(this.selectedElement);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ──────── Quick flag ────────
|
|
241
|
+
private async quickFlag(): Promise<void> {
|
|
242
|
+
const payload = this.buildPayload('bug', '');
|
|
243
|
+
try {
|
|
244
|
+
const { id } = await this.apiClient.submitFeedback(payload);
|
|
245
|
+
showToast(this.shadowHost.shadow, `${t('toast.flagged')} (${id})`);
|
|
246
|
+
|
|
247
|
+
if (this.activeChecklistItemId !== null) {
|
|
248
|
+
await this.linkFeedbackToChecklist(id, this.activeChecklistItemId);
|
|
249
|
+
this.activeChecklistItemId = null;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
showToast(this.shadowHost.shadow, t('toast.failed'), true);
|
|
253
|
+
}
|
|
254
|
+
this.selectedElement = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ──────── Checklist integration ────────
|
|
258
|
+
private onChecklistFail(itemId: number): void {
|
|
259
|
+
this.activeChecklistItemId = itemId;
|
|
260
|
+
this.enterMode('flag');
|
|
261
|
+
showToast(this.shadowHost.shadow, t('toast.clickToFlag'));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async linkFeedbackToChecklist(feedbackId: string, checklistItemId: number): Promise<void> {
|
|
265
|
+
try {
|
|
266
|
+
await fetch(`${window.location.origin}/api/checklist/${checklistItemId}`, {
|
|
267
|
+
method: 'PATCH',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({ status: 'failed', feedback: `[linked:${feedbackId}]` }),
|
|
270
|
+
});
|
|
271
|
+
} catch {}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ──────── Annotation ────────
|
|
275
|
+
private async startAnnotation(): Promise<void> {
|
|
276
|
+
this.hideModeBar();
|
|
277
|
+
this.fab.setActive(false);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const fullScreenshot = await captureScreenshot();
|
|
281
|
+
const selector = new RegionSelector(
|
|
282
|
+
(rect) => {
|
|
283
|
+
const dpr = Math.min(window.devicePixelRatio, 2);
|
|
284
|
+
const cropCanvas = document.createElement('canvas');
|
|
285
|
+
cropCanvas.width = rect.w * dpr;
|
|
286
|
+
cropCanvas.height = rect.h * dpr;
|
|
287
|
+
const cropCtx = cropCanvas.getContext('2d')!;
|
|
288
|
+
cropCtx.drawImage(fullScreenshot,
|
|
289
|
+
rect.x * dpr, rect.y * dpr, rect.w * dpr, rect.h * dpr,
|
|
290
|
+
0, 0, rect.w * dpr, rect.h * dpr
|
|
291
|
+
);
|
|
292
|
+
const editor = new CanvasEditor(
|
|
293
|
+
cropCanvas,
|
|
294
|
+
(dataUrl) => {
|
|
295
|
+
this.annotationDataUrl = dataUrl;
|
|
296
|
+
this.fab.setActive(true);
|
|
297
|
+
this.panel.showForAnnotation(dataUrl);
|
|
298
|
+
},
|
|
299
|
+
() => { this.fab.setActive(true); this.showModeBar(); }
|
|
300
|
+
);
|
|
301
|
+
editor.show();
|
|
302
|
+
},
|
|
303
|
+
() => { this.fab.setActive(true); this.showModeBar(); }
|
|
304
|
+
);
|
|
305
|
+
selector.show();
|
|
306
|
+
} catch {
|
|
307
|
+
showToast(this.shadowHost.shadow, t('toast.screenshotFailed'), true);
|
|
308
|
+
this.fab.setActive(true);
|
|
309
|
+
this.showModeBar();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ──────── Payload ────────
|
|
314
|
+
private buildPayload(problemType: string, description: string): FeedbackPayload {
|
|
315
|
+
return {
|
|
316
|
+
mode: this.selectedElement ? 'element' : 'annotation',
|
|
317
|
+
problemType, description,
|
|
318
|
+
element: this.selectedElement || undefined,
|
|
319
|
+
pageUrl: window.location.href,
|
|
320
|
+
pageTitle: document.title,
|
|
321
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
322
|
+
userAgent: navigator.userAgent,
|
|
323
|
+
timestamp: Date.now(),
|
|
324
|
+
consoleErrors: this.consoleBuffer.getAll(),
|
|
325
|
+
networkErrors: this.networkBuffer.getAll(),
|
|
326
|
+
unhandledErrors: this.errorBuffer.getAll(),
|
|
327
|
+
actionSteps: this.actionRecorder.getSteps(),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ──────── Submit ────────
|
|
332
|
+
private async onSubmit(result: PanelResult): Promise<void> {
|
|
333
|
+
const payload = this.buildPayload(result.problemType, result.description);
|
|
334
|
+
try {
|
|
335
|
+
const { id } = await this.apiClient.submitFeedback(payload, this.annotationDataUrl || undefined);
|
|
336
|
+
showToast(this.shadowHost.shadow, `${t('toast.submitted')} (${id})`);
|
|
337
|
+
if (this.activeChecklistItemId !== null) {
|
|
338
|
+
await this.linkFeedbackToChecklist(id, this.activeChecklistItemId);
|
|
339
|
+
this.activeChecklistItemId = null;
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
showToast(this.shadowHost.shadow, t('toast.failed'), true);
|
|
343
|
+
}
|
|
344
|
+
this.panel.hide();
|
|
345
|
+
this.selectedElement = null;
|
|
346
|
+
this.annotationDataUrl = null;
|
|
347
|
+
this.showModeBar();
|
|
348
|
+
this.activateMode();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private cancelFeedback(): void {
|
|
352
|
+
this.panel.hide();
|
|
353
|
+
this.selectedElement = null;
|
|
354
|
+
this.annotationDataUrl = null;
|
|
355
|
+
this.activeChecklistItemId = null;
|
|
356
|
+
this.showModeBar();
|
|
357
|
+
this.activateMode();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ──────── Cleanup ────────
|
|
361
|
+
destroy(): void {
|
|
362
|
+
document.removeEventListener('keydown', this.keyHandler);
|
|
363
|
+
this.deactivateAll();
|
|
364
|
+
this.consoleInterceptor.uninstall();
|
|
365
|
+
this.networkInterceptor.uninstall();
|
|
366
|
+
this.errorInterceptor.uninstall();
|
|
367
|
+
this.actionRecorder.stop();
|
|
368
|
+
this.checklistPanel?.destroy();
|
|
369
|
+
this.verifyPanel?.destroy();
|
|
370
|
+
this.fab.destroy();
|
|
371
|
+
this.shadowHost.destroy();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SDK_CSS } from '../styles/sdk.css.js';
|
|
2
|
+
|
|
3
|
+
export class ShadowHost {
|
|
4
|
+
readonly host: HTMLDivElement;
|
|
5
|
+
readonly shadow: ShadowRoot;
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
this.host = document.createElement('div');
|
|
9
|
+
this.host.id = 'vibe-feedback-host';
|
|
10
|
+
this.host.style.cssText =
|
|
11
|
+
'position:fixed;z-index:2147483647;top:0;left:0;width:0;height:0;pointer-events:none;';
|
|
12
|
+
document.body.appendChild(this.host);
|
|
13
|
+
this.shadow = this.host.attachShadow({ mode: 'open' });
|
|
14
|
+
|
|
15
|
+
const style = document.createElement('style');
|
|
16
|
+
style.textContent = SDK_CSS;
|
|
17
|
+
this.shadow.appendChild(style);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
append(el: HTMLElement): void {
|
|
21
|
+
this.shadow.appendChild(el);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
destroy(): void {
|
|
25
|
+
this.host.remove();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class Highlighter {
|
|
2
|
+
private overlay: HTMLDivElement;
|
|
3
|
+
private currentTarget: Element | null = null;
|
|
4
|
+
private moveHandler: (e: MouseEvent) => void;
|
|
5
|
+
private clickHandler: (e: MouseEvent) => void;
|
|
6
|
+
private active = false;
|
|
7
|
+
|
|
8
|
+
constructor(private onSelect: (element: Element) => void) {
|
|
9
|
+
this.overlay = document.createElement('div');
|
|
10
|
+
this.overlay.style.cssText = `
|
|
11
|
+
position: fixed;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
z-index: 2147483646;
|
|
14
|
+
background: rgba(59,130,246,0.12);
|
|
15
|
+
border: 2px solid rgba(59,130,246,0.7);
|
|
16
|
+
border-radius: 3px;
|
|
17
|
+
transition: all 0.06s ease-out;
|
|
18
|
+
display: none;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
this.moveHandler = (e: MouseEvent) => this.onMouseMove(e);
|
|
22
|
+
this.clickHandler = (e: MouseEvent) => this.onClick(e);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
activate(): void {
|
|
26
|
+
if (this.active) return;
|
|
27
|
+
this.active = true;
|
|
28
|
+
document.body.appendChild(this.overlay);
|
|
29
|
+
document.addEventListener('mousemove', this.moveHandler, true);
|
|
30
|
+
document.addEventListener('click', this.clickHandler, true);
|
|
31
|
+
document.body.style.cursor = 'crosshair';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
deactivate(): void {
|
|
35
|
+
if (!this.active) return;
|
|
36
|
+
this.active = false;
|
|
37
|
+
this.overlay.style.display = 'none';
|
|
38
|
+
this.overlay.remove();
|
|
39
|
+
document.removeEventListener('mousemove', this.moveHandler, true);
|
|
40
|
+
document.removeEventListener('click', this.clickHandler, true);
|
|
41
|
+
document.body.style.cursor = '';
|
|
42
|
+
this.currentTarget = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private onMouseMove(e: MouseEvent): void {
|
|
46
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
47
|
+
if (!target || this.isSDKElement(target) || target === this.currentTarget) return;
|
|
48
|
+
|
|
49
|
+
this.currentTarget = target;
|
|
50
|
+
const rect = target.getBoundingClientRect();
|
|
51
|
+
this.overlay.style.display = 'block';
|
|
52
|
+
this.overlay.style.top = `${rect.top}px`;
|
|
53
|
+
this.overlay.style.left = `${rect.left}px`;
|
|
54
|
+
this.overlay.style.width = `${rect.width}px`;
|
|
55
|
+
this.overlay.style.height = `${rect.height}px`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private onClick(e: MouseEvent): void {
|
|
59
|
+
const target = document.elementFromPoint(e.clientX, e.clientY);
|
|
60
|
+
if (!target || this.isSDKElement(target)) return;
|
|
61
|
+
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
e.stopImmediatePropagation();
|
|
65
|
+
|
|
66
|
+
this.onSelect(target);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private isSDKElement(el: Element): boolean {
|
|
70
|
+
// Check if element is part of our SDK
|
|
71
|
+
let current: Element | null = el;
|
|
72
|
+
while (current) {
|
|
73
|
+
if (current.id === 'vibe-feedback-host') return true;
|
|
74
|
+
if ((current as HTMLElement).style?.zIndex === '2147483646') return true;
|
|
75
|
+
current = current.parentElement;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { generateSelector } from './selector.js';
|
|
2
|
+
|
|
3
|
+
export interface ElementInfo {
|
|
4
|
+
selector: string;
|
|
5
|
+
tagName: string;
|
|
6
|
+
textContent: string;
|
|
7
|
+
rect: { x: number; y: number; width: number; height: number };
|
|
8
|
+
computedStyles: Record<string, string>;
|
|
9
|
+
attributes: Record<string, string>;
|
|
10
|
+
componentName: string | null;
|
|
11
|
+
componentFile: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const STYLE_PROPS = [
|
|
15
|
+
'display', 'position', 'width', 'height', 'margin', 'padding',
|
|
16
|
+
'color', 'background-color', 'font-size', 'font-weight',
|
|
17
|
+
'border', 'opacity', 'cursor', 'visibility', 'overflow',
|
|
18
|
+
'flex-direction', 'align-items', 'justify-content', 'gap',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function inspectElement(el: Element): ElementInfo {
|
|
22
|
+
const rect = el.getBoundingClientRect();
|
|
23
|
+
const computed = window.getComputedStyle(el);
|
|
24
|
+
|
|
25
|
+
const styles: Record<string, string> = {};
|
|
26
|
+
for (const prop of STYLE_PROPS) {
|
|
27
|
+
styles[prop] = computed.getPropertyValue(prop);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const attrs: Record<string, string> = {};
|
|
31
|
+
for (const attr of el.attributes) {
|
|
32
|
+
if (attr.name.startsWith('data-') || ['class', 'href', 'src', 'type', 'name', 'placeholder'].includes(attr.name)) {
|
|
33
|
+
attrs[attr.name] = attr.value.slice(0, 100);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
selector: generateSelector(el),
|
|
39
|
+
tagName: el.tagName.toLowerCase(),
|
|
40
|
+
textContent: (el.textContent || '').trim().slice(0, 200),
|
|
41
|
+
rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
|
|
42
|
+
computedStyles: styles,
|
|
43
|
+
attributes: attrs,
|
|
44
|
+
componentName: getComponentName(el),
|
|
45
|
+
componentFile: null, // Source file detection is unreliable
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getComponentName(el: Element): string | null {
|
|
50
|
+
// React: __reactFiber$ or __reactInternalInstance$
|
|
51
|
+
const fiberKey = Object.keys(el).find(
|
|
52
|
+
(k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
|
|
53
|
+
);
|
|
54
|
+
if (fiberKey) {
|
|
55
|
+
let fiber = (el as any)[fiberKey];
|
|
56
|
+
while (fiber) {
|
|
57
|
+
if (typeof fiber.type === 'function') {
|
|
58
|
+
return fiber.type.displayName || fiber.type.name || null;
|
|
59
|
+
}
|
|
60
|
+
fiber = fiber.return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Vue 2
|
|
65
|
+
const vue2 = (el as any).__vue__;
|
|
66
|
+
if (vue2) return vue2.$options?.name || vue2.$options?._componentTag || null;
|
|
67
|
+
|
|
68
|
+
// Vue 3
|
|
69
|
+
const vue3 = (el as any).__vueParentComponent;
|
|
70
|
+
if (vue3) return vue3.type?.name || vue3.type?.__name || null;
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function generateSelector(el: Element): string {
|
|
2
|
+
// Priority: data-testid > id > nth-child path
|
|
3
|
+
const testId = el.getAttribute('data-testid');
|
|
4
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
5
|
+
|
|
6
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
7
|
+
|
|
8
|
+
const parts: string[] = [];
|
|
9
|
+
let current: Element | null = el;
|
|
10
|
+
while (current && current !== document.body && current !== document.documentElement) {
|
|
11
|
+
const parent = current.parentElement;
|
|
12
|
+
if (!parent) break;
|
|
13
|
+
const children = Array.from(parent.children);
|
|
14
|
+
const index = children.indexOf(current) + 1;
|
|
15
|
+
const tag = current.tagName.toLowerCase();
|
|
16
|
+
parts.unshift(`${tag}:nth-child(${index})`);
|
|
17
|
+
current = parent;
|
|
18
|
+
// Keep path reasonable
|
|
19
|
+
if (parts.length > 5) break;
|
|
20
|
+
}
|
|
21
|
+
return parts.join(' > ');
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { initI18n } from './core/i18n.js';
|
|
2
|
+
import { VibeFeedback } from './core/sdk.js';
|
|
3
|
+
|
|
4
|
+
// Detect language before anything renders
|
|
5
|
+
initI18n();
|
|
6
|
+
|
|
7
|
+
const instance = new VibeFeedback();
|
|
8
|
+
|
|
9
|
+
// Expose globally for potential programmatic use
|
|
10
|
+
(window as any).__vibeFeedback = instance;
|