shmakk 1.2.4 → 1.2.5
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/.env.example +11 -0
- package/README.md +75 -1
- package/docs/index.html +154 -16
- package/docs/mcp.md +78 -0
- package/docs/ssh.md +82 -0
- package/docs/vibedit-analysis.md +375 -0
- package/docs/vim.md +110 -0
- package/docs/voice.md +4 -0
- package/package.json +9 -5
- package/scripts/test-vibedit.js +45 -0
- package/scripts/vibedit-demo.sh +52 -0
- package/skills/shmakk-skill-creator.md +269 -0
- package/src/_check.js +7 -0
- package/src/_check_schema.js +5 -0
- package/src/_cleanup.js +18 -0
- package/src/_fix.js +9 -0
- package/src/_test_import.js +15 -0
- package/src/agent.js +11 -4
- package/src/browser-daemon.js +209 -0
- package/src/browser.js +10 -0
- package/src/cli/browserDaemon.js +60 -0
- package/src/cli/connectBrowser.js +137 -0
- package/src/cli.js +235 -8
- package/src/completions.js +8 -0
- package/src/control.js +273 -1
- package/src/core/browserConnector.js +523 -0
- package/src/electron.js +305 -0
- package/src/endpoints.js +74 -9
- package/src/index.js +24 -1
- package/src/llm.js +501 -61
- package/src/mobile.js +307 -0
- package/src/notify.js +51 -3
- package/src/orchestrator.js +35 -1
- package/src/pty.js +11 -6
- package/src/review.js +45 -11
- package/src/self-commands.js +153 -0
- package/src/session-convert.js +508 -0
- package/src/session-search.js +31 -0
- package/src/session.js +384 -46
- package/src/skills/browserActions.ts +984 -0
- package/src/skills.js +451 -24
- package/src/system-prompt.js +31 -25
- package/src/tools.js +81 -0
- package/src/vibedit/control.js +534 -0
- package/src/vibedit/electron.js +108 -0
- package/src/vibedit/files.js +171 -0
- package/src/vibedit/index.js +298 -0
- package/src/vibedit/overlay.js +1482 -0
- package/src/vibedit/prompts.js +245 -0
- package/src/vibedit/state.js +32 -0
- package/src/vim.js +410 -0
|
@@ -0,0 +1,1482 @@
|
|
|
1
|
+
// vibedit overlay - injected into every document load via Playwright addInitScript.
|
|
2
|
+
// Renders inside a shadow root so the host page styles never bleed in.
|
|
3
|
+
(() => {
|
|
4
|
+
if (window.__vibeditLoaded) return;
|
|
5
|
+
window.__vibeditLoaded = true;
|
|
6
|
+
const PORT = window.__VIBEDIT__ && window.__VIBEDIT__.port;
|
|
7
|
+
if (!PORT) return;
|
|
8
|
+
|
|
9
|
+
// addInitScript fires before the document has parsed anything, so
|
|
10
|
+
// document.documentElement and document.body may not exist yet.
|
|
11
|
+
function whenDomReady(fn) {
|
|
12
|
+
if (document.readyState === "interactive" || document.readyState === "complete") fn();
|
|
13
|
+
else document.addEventListener("DOMContentLoaded", fn, { once: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
whenDomReady(init);
|
|
17
|
+
|
|
18
|
+
function init() {
|
|
19
|
+
if (location.href === "about:blank" || !document.body) return;
|
|
20
|
+
|
|
21
|
+
const ICONS = {
|
|
22
|
+
spark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/></svg>',
|
|
23
|
+
pointer: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/></svg>',
|
|
24
|
+
save: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>',
|
|
25
|
+
record: '<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="6"/></svg>',
|
|
26
|
+
stop: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="1"/></svg>',
|
|
27
|
+
play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
|
|
28
|
+
trash: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
|
29
|
+
x: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
|
|
30
|
+
send: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>',
|
|
31
|
+
plus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
|
32
|
+
camera: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>',
|
|
33
|
+
automate: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const CSS = `
|
|
37
|
+
:host { all: initial; }
|
|
38
|
+
* { box-sizing: border-box; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
|
39
|
+
.puck {
|
|
40
|
+
position: fixed; right: 18px; bottom: 18px; z-index: 2147483646;
|
|
41
|
+
width: 46px; height: 46px; border-radius: 14px; border: 1px solid #2c2f38;
|
|
42
|
+
background: #16181d; color: #e8a33d; cursor: pointer;
|
|
43
|
+
display: flex; align-items: center; justify-content: center;
|
|
44
|
+
box-shadow: 0 6px 24px rgba(0,0,0,.45); transition: transform .15s ease;
|
|
45
|
+
}
|
|
46
|
+
.puck:hover { transform: scale(1.06); }
|
|
47
|
+
.puck svg { width: 22px; height: 22px; }
|
|
48
|
+
.puck.rec { color: #e25555; animation: pulse 1.2s infinite; }
|
|
49
|
+
@keyframes pulse { 50% { box-shadow: 0 0 0 8px rgba(226,85,85,.18); } }
|
|
50
|
+
|
|
51
|
+
.panel {
|
|
52
|
+
position: fixed; right: 18px; bottom: 76px; z-index: 2147483646;
|
|
53
|
+
width: 380px; max-height: min(720px, calc(100vh - 110px));
|
|
54
|
+
display: none; flex-direction: column;
|
|
55
|
+
background: #16181d; color: #e7e9ee; border: 1px solid #2c2f38;
|
|
56
|
+
border-radius: 16px; box-shadow: 0 16px 48px rgba(0,0,0,.5);
|
|
57
|
+
}
|
|
58
|
+
.panel.open { display: flex; }
|
|
59
|
+
|
|
60
|
+
.head { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid #23262e; flex: 0 0 auto; cursor: grab; user-select: none; }
|
|
61
|
+
.head.dragging { cursor: grabbing; }
|
|
62
|
+
.head .title { font-size: 13px; font-weight: 600; letter-spacing: .04em; color: #e8a33d; }
|
|
63
|
+
.head .model { font-size: 11px; color: #8b909d; margin-left: auto; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
64
|
+
.head .dot { width: 7px; height: 7px; border-radius: 50%; background: #5a5f6b; }
|
|
65
|
+
.head .dot.on { background: #58c789; }
|
|
66
|
+
|
|
67
|
+
.toolbar { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid #23262e; flex: 0 0 auto; overflow: visible; }
|
|
68
|
+
.btn {
|
|
69
|
+
display: inline-flex; align-items: center; gap: 5px; padding: 6px 9px;
|
|
70
|
+
border-radius: 8px; border: 1px solid #2c2f38; background: #1d2026; color: #cfd3dc;
|
|
71
|
+
font-size: 11.5px; cursor: pointer; transition: background .12s ease, color .12s ease, border-color .12s ease;
|
|
72
|
+
}
|
|
73
|
+
.btn:hover { background: #262a32; border-color: #3a3e49; }
|
|
74
|
+
.btn.active { background: #2c2417; border-color: #6b5523; color: #e8a33d; }
|
|
75
|
+
.btn.rec { background: #2c1717; border-color: #6b2323; color: #e25555; }
|
|
76
|
+
.btn.automation { background: #1d242c; border-color: #3a5068; color: #5b9bd5; }
|
|
77
|
+
.btn.danger { color: #c66; }
|
|
78
|
+
.btn.danger:hover { background: #2c1717; border-color: #6b2323; }
|
|
79
|
+
.btn svg { width: 13px; height: 13px; flex-shrink: 0; }
|
|
80
|
+
.btn.hasChanges { background: #2c2417; border-color: #6b5523; color: #e8a33d; }
|
|
81
|
+
|
|
82
|
+
.savewrap { position: relative; }
|
|
83
|
+
.savedrop { position: absolute; top: 100%; right: 0; margin-top: 6px; width: 320px; max-height: 260px; overflow-y: auto; background: #16181d; border: 1px solid #2c2f38; border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,.55); display: none; flex-direction: column; z-index: 2147483647; }
|
|
84
|
+
.savedrop.open { display: flex; }
|
|
85
|
+
.savedrop .drophead { font-size: 11px; color: #8b909d; padding: 8px 12px 4px; }
|
|
86
|
+
.savedrop .droprow { display: flex; align-items: center; gap: 8px; padding: 6px 12px; font-size: 11px; color: #cfd3dc; }
|
|
87
|
+
.savedrop .droprow + .droprow { border-top: 1px solid #23262e; }
|
|
88
|
+
.savedrop .droprow .sel { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, monospace; font-size: 10.5px; color: #8b909d; }
|
|
89
|
+
.savedrop .droprow .kind { color: #8b909d; font-size: 10px; text-transform: uppercase; flex-shrink: 0; }
|
|
90
|
+
.savedrop .droprow button { all: unset; cursor: pointer; color: #8b909d; display: inline-flex; flex-shrink: 0; }
|
|
91
|
+
.savedrop .droprow button:hover { color: #e25555; }
|
|
92
|
+
.savedrop .droprow button svg { width: 12px; height: 12px; }
|
|
93
|
+
.savedrop .dropact { padding: 8px 12px; border-top: 1px solid #23262e; display: flex; gap: 6px; justify-content: flex-end; }
|
|
94
|
+
|
|
95
|
+
.msgs { flex: 1 1 auto; min-height: 400px; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 10px; scrollbar-width: thin; scrollbar-color: #2c2f38 transparent; }
|
|
96
|
+
.msgs::-webkit-scrollbar { width: 5px; }
|
|
97
|
+
.msgs::-webkit-scrollbar-track { background: transparent; }
|
|
98
|
+
.msgs::-webkit-scrollbar-thumb { background: #2c2f38; border-radius: 3px; }
|
|
99
|
+
.msgs::-webkit-scrollbar-thumb:hover { background: #3a3e49; }
|
|
100
|
+
.msg-user { max-width: 92%; padding: 8px 11px; border-radius: 11px; font-size: 12.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; align-self: flex-end; background: #2c2417; color: #f1d9ab; border: 1px solid #463a1d; border-left: 3px solid #e8a33d; }
|
|
101
|
+
.msg-ai { max-width: 92%; padding: 8px 11px; border-radius: 11px; font-size: 12.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; align-self: flex-start; background: #1d2026; border: 1px solid #2c2f38; border-left: 3px solid #4da6e8; }
|
|
102
|
+
.msg-sys { max-width: 92%; padding: 2px 11px; border-radius: 11px; font-size: 11px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; align-self: center; color: #8b909d; background: none; border-left: none; }
|
|
103
|
+
.msg-context { position: relative; border: 1px solid rgba(255,255,255,.3); padding: 10px; color: #e8a33d; padding-right: 28px; }
|
|
104
|
+
.msg-context .dismiss { position: absolute; top: 6px; right: 8px; background: none; border: none; color: #6b7280; cursor: pointer; font-size: 16px; line-height: 1; padding: 2px 4px; border-radius: 4px; }
|
|
105
|
+
.msg-context .dismiss:hover { color: #e25555; background: rgba(255,255,255,.08); }
|
|
106
|
+
.msglabel { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; padding: 1px 5px; border-radius: 4px; margin-right: 4px; display: inline-block; vertical-align: middle; }
|
|
107
|
+
.msglabel-user { background: #463a1d; color: #e8a33d; }
|
|
108
|
+
.msglabel-ai { background: #1a2d3d; color: #4da6e8; }
|
|
109
|
+
.msglabel-context { background: #1d2620; color: #8bbf6a; }
|
|
110
|
+
.msglabel-instruction { background: #1d2a3d; color: #5b9bd5; }
|
|
111
|
+
.msg-flow { max-width: 100%; padding: 10px 12px; border-radius: 11px; font-size: 12.5px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; align-self: stretch; background: #1d2026; border: 1px solid #2c2f38; border-left: 3px solid transparent; display: flex; flex-direction: column; gap: 8px; }
|
|
112
|
+
.msg-flow img { width: 100%; max-height: 200px; object-fit: contain; border-radius: 9px; border: 1px solid #2c2f38; background: #0f1116; min-height: 60px; }
|
|
113
|
+
.msg-flow .scrub { display: flex; align-items: center; gap: 8px; }
|
|
114
|
+
.msg-flow input[type=range] { flex: 1; accent-color: #e8a33d; }
|
|
115
|
+
.msg-flow .pbtn { background: #1d2026; border: 1px solid #2c2f38; border-radius: 8px; color: #cfd3dc; padding: 5px 8px; cursor: pointer; display: inline-flex; }
|
|
116
|
+
.msg-flow .pbtn svg { width: 13px; height: 13px; }
|
|
117
|
+
.msg-flow .evt { font-size: 11px; color: #8b909d; min-height: 14px; }
|
|
118
|
+
|
|
119
|
+
.inspector { border-top: 1px solid #23262e; padding: 10px 12px; display: none; flex-direction: column; gap: 8px; flex: 0 1 auto; min-height: 400px; overflow-y: auto; }
|
|
120
|
+
.inspector.open { display: flex; }
|
|
121
|
+
.panel.editing #msgs { display: none; }
|
|
122
|
+
.panel.editing .inspector.open { flex: 1 1 auto; }
|
|
123
|
+
.chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
|
|
124
|
+
.chip { display: inline-flex; align-items: center; gap: 5px; background: #1d2026; border: 1px solid #2c2f38; border-radius: 7px; padding: 3px 7px; font-size: 11px; color: #cfd3dc; cursor: pointer; }
|
|
125
|
+
.chip:hover { border-color: #3a3e49; }
|
|
126
|
+
.chip.scoped { border-color: #6b5523; color: #e8a33d; }
|
|
127
|
+
.chip button { all: unset; cursor: pointer; color: #8b909d; display: inline-flex; }
|
|
128
|
+
.chip button:hover { color: #e25555; }
|
|
129
|
+
.chip button svg { width: 10px; height: 10px; }
|
|
130
|
+
.chipadd { width: 86px; background: #0f1116; border: 1px solid #2c2f38; border-radius: 7px; color: #e7e9ee; font-size: 11px; padding: 4px 7px; outline: none; }
|
|
131
|
+
.chipadd:focus { border-color: #6b5523; }
|
|
132
|
+
.scopesel { flex: 1; background: #0f1116; border: 1px solid #2c2f38; border-radius: 7px; color: #e7e9ee; font-size: 12px; padding: 5px 8px; outline: none; min-width: 0; }
|
|
133
|
+
.props { display: flex; flex-direction: column; gap: 5px; }
|
|
134
|
+
.props .prow { display: flex; gap: 6px; }
|
|
135
|
+
.props input { background: #0f1116; border: 1px solid #2c2f38; border-radius: 7px; color: #e7e9ee; font-size: 11.5px; padding: 4px 7px; outline: none; min-width: 0; }
|
|
136
|
+
.props input.pname { flex: 0 0 42%; }
|
|
137
|
+
.props input.pval { flex: 1; }
|
|
138
|
+
.props input.pcolor { flex: 0 0 28px; width: 28px; height: 28px; padding: 2px; border-radius: 6px; cursor: pointer; }
|
|
139
|
+
.props input:focus { border-color: #6b5523; }
|
|
140
|
+
.btn.small { padding: 5px 9px; font-size: 11px; align-self: flex-start; }
|
|
141
|
+
.inspector .sel { font-size: 11px; color: #e8a33d; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
142
|
+
.row { display: flex; gap: 8px; align-items: center; }
|
|
143
|
+
.row label { font-size: 11px; color: #8b909d; width: 38px; }
|
|
144
|
+
.row input[type=color] { width: 30px; height: 24px; border: 1px solid #2c2f38; border-radius: 6px; background: none; padding: 0; cursor: pointer; }
|
|
145
|
+
.row input[type=text], .row input[type=number] {
|
|
146
|
+
flex: 1; background: #0f1116; border: 1px solid #2c2f38; border-radius: 7px;
|
|
147
|
+
color: #e7e9ee; font-size: 12px; padding: 5px 8px; outline: none;
|
|
148
|
+
}
|
|
149
|
+
.row input:focus { border-color: #6b5523; }
|
|
150
|
+
.iconbtn { background: none; border: 1px solid #2c2f38; border-radius: 7px; color: #c66; padding: 4px 7px; cursor: pointer; display: inline-flex; }
|
|
151
|
+
.iconbtn svg { width: 13px; height: 13px; }
|
|
152
|
+
|
|
153
|
+
.composer { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid #23262e; flex: 0 0 auto; }
|
|
154
|
+
.composer textarea {
|
|
155
|
+
flex: 1; resize: none; height: 54px; background: #0f1116; color: #e7e9ee;
|
|
156
|
+
border: 1px solid #2c2f38; border-radius: 10px; padding: 8px 10px;
|
|
157
|
+
font-size: 12px; outline: none;
|
|
158
|
+
}
|
|
159
|
+
.composer textarea:focus { border-color: #6b5523; }
|
|
160
|
+
.composer textarea.automation { border-color: #3a5068; }
|
|
161
|
+
.composer textarea.automation:focus { border-color: #5b9bd5; }
|
|
162
|
+
.composer button {
|
|
163
|
+
background: #2c2417; border: 1px solid #6b5523; border-radius: 10px;
|
|
164
|
+
color: #e8a33d; cursor: pointer; padding: 8px 12px; display: inline-flex;
|
|
165
|
+
align-items: center; justify-content: center;
|
|
166
|
+
}
|
|
167
|
+
.composer button:hover { background: #463a1d; }
|
|
168
|
+
.composer button.automation { background: #1d242c; border-color: #3a5068; color: #5b9bd5; }
|
|
169
|
+
.composer button.automation:hover { background: #263648; }
|
|
170
|
+
|
|
171
|
+
.msg-instruction {
|
|
172
|
+
max-width: 92%; padding: 8px 11px; border-radius: 11px; font-size: 12.5px;
|
|
173
|
+
line-height: 1.45; white-space: pre-wrap; word-break: break-word;
|
|
174
|
+
align-self: flex-end;
|
|
175
|
+
background: #1d242c; color: #b8d4f0; border: 1px solid #3a5068;
|
|
176
|
+
border-left: 3px solid #5b9bd5;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.automation-banner {
|
|
180
|
+
padding: 6px 12px; background: #1d242c; border-bottom: 1px solid #3a5068;
|
|
181
|
+
color: #5b9bd5; font-size: 11px; font-weight: 600;
|
|
182
|
+
display: none; align-items: center; gap: 8px;
|
|
183
|
+
flex: 0 0 auto;
|
|
184
|
+
}
|
|
185
|
+
.automation-banner svg { width: 14px; height: 14px; }
|
|
186
|
+
.automation-banner .inst-count { color: #8ea4c2; font-weight: 400; margin-left: auto; }
|
|
187
|
+
.panel.automation .automation-banner { display: flex; }
|
|
188
|
+
.panel.automation .composer { border-top-color: #3a5068; }
|
|
189
|
+
font-size: 12.5px; outline: none;
|
|
190
|
+
}
|
|
191
|
+
.composer textarea:focus { border-color: #6b5523; }
|
|
192
|
+
.composer button {
|
|
193
|
+
width: 42px; border-radius: 10px; border: 1px solid #6b5523; background: #e8a33d;
|
|
194
|
+
color: #16181d; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
195
|
+
}
|
|
196
|
+
.composer button:disabled { opacity: .5; cursor: default; }
|
|
197
|
+
.composer button svg { width: 16px; height: 16px; }
|
|
198
|
+
.btn.hasShots { background: #2c2617; border-color: #6b5a23; color: #e8b84d; }
|
|
199
|
+
|
|
200
|
+
.hl { position: fixed; pointer-events: none; z-index: 2147483645; border: 1.5px dashed #e8a33d; border-radius: 3px; display: none; }
|
|
201
|
+
.hl.sel { border-style: solid; box-shadow: 0 0 0 3px rgba(232,163,61,.22); }
|
|
202
|
+
|
|
203
|
+
.shot-preview { padding: 6px; display: flex; flex-direction: column; gap: 5px; }
|
|
204
|
+
.shot-preview img { max-height: 200px; object-fit: contain; border-radius: 8px; }
|
|
205
|
+
.shot-actions { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
|
|
206
|
+
.hl.shot { border: 2px solid #4da6e8; background: rgba(77,166,232,.15); box-shadow: 0 0 0 1px rgba(77,166,232,.3); }
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
// ---- shadow host -------------------------------------------------------
|
|
210
|
+
function mount() {
|
|
211
|
+
const host = document.createElement("div");
|
|
212
|
+
host.id = "__vibedit_host";
|
|
213
|
+
document.documentElement.appendChild(host);
|
|
214
|
+
const root = host.attachShadow({ mode: "open" });
|
|
215
|
+
const style = document.createElement("style");
|
|
216
|
+
style.textContent = CSS;
|
|
217
|
+
root.appendChild(style);
|
|
218
|
+
|
|
219
|
+
root.innerHTML += `
|
|
220
|
+
<div class="puck" id="puck" title="vibedit">${ICONS.spark}</div>
|
|
221
|
+
<div class="panel" id="panel">
|
|
222
|
+
<div class="head">
|
|
223
|
+
<span class="dot" id="dot"></span>
|
|
224
|
+
<span class="title">vibedit</span>
|
|
225
|
+
<span class="model" id="model"></span>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="toolbar">
|
|
228
|
+
<button class="btn" id="editBtn" title="Toggle edit mode">${ICONS.pointer}</button>
|
|
229
|
+
<span class="savewrap"><button class="btn" id="saveBtn" title="Review changes">${ICONS.save}</button>
|
|
230
|
+
<div class="savedrop" id="savedrop"></div></span>
|
|
231
|
+
<button class="btn" id="flowBtn" title="Record userflow">${ICONS.record}</button>
|
|
232
|
+
<button class="btn" id="shotBtn" title="Screenshot area for context">${ICONS.camera}</button>
|
|
233
|
+
<button class="btn" id="automationBtn" title="Automation mode">${ICONS.automate}</button>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="automation-banner" id="automationBanner">${ICONS.automate} Automation mode active<span class="inst-count" id="instCount">0 instructions</span></div>
|
|
236
|
+
<div class="msgs" id="msgs"></div>
|
|
237
|
+
<div class="inspector" id="inspector">
|
|
238
|
+
<div class="sel" id="selPath"></div>
|
|
239
|
+
<div class="chips" id="chips"></div>
|
|
240
|
+
<div class="row">
|
|
241
|
+
<label>Scope</label>
|
|
242
|
+
<select class="scopesel" id="scopeSel"></select>
|
|
243
|
+
<button class="iconbtn" id="delBtn" title="Delete element">${ICONS.trash}</button>
|
|
244
|
+
<button class="iconbtn" id="deselBtn" title="Deselect" style="color:#8b909d">${ICONS.x}</button>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="row" id="textRow"><label>Text</label><input type="text" id="inText"></div>
|
|
247
|
+
<div class="row" id="insertRow">
|
|
248
|
+
<label>Add</label>
|
|
249
|
+
<select class="scopesel" id="insertTag"><option value="p">p</option><option value="div">div</option><option value="span">span</option><option value="button">button</option><option value="h2">h2</option><option value="li">li</option></select>
|
|
250
|
+
<button class="btn small" id="addChild">${ICONS.plus}<span>Inside</span></button>
|
|
251
|
+
<button class="btn small" id="addAfter">${ICONS.plus}<span>After</span></button>
|
|
252
|
+
</div>
|
|
253
|
+
<div class="row">
|
|
254
|
+
<label>Color</label><input type="color" id="inColor">
|
|
255
|
+
<label style="width:auto">Bg</label><input type="color" id="inBg">
|
|
256
|
+
<label style="width:auto">Size</label><input type="number" id="inSize" min="6" max="160" style="width:64px">
|
|
257
|
+
</div>
|
|
258
|
+
<div class="props" id="props"></div>
|
|
259
|
+
<button class="btn small" id="addProp">${ICONS.plus}<span>Add CSS property</span></button>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="composer">
|
|
262
|
+
<textarea id="chatText" placeholder="Ask for a change... e.g. make the header dark blue"></textarea>
|
|
263
|
+
<button id="chatSend">${ICONS.send}</button>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="hl" id="hoverHl"></div>
|
|
267
|
+
<div class="hl sel" id="selHl"></div>
|
|
268
|
+
<div class="hl shot" id="shotHl"></div>`;
|
|
269
|
+
|
|
270
|
+
return { host, root };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---- state -------------------------------------------------------------
|
|
274
|
+
let ws = null, wsOpen = false;
|
|
275
|
+
let editMode = false;
|
|
276
|
+
let selected = null;
|
|
277
|
+
let screenshotMode = false, justShot = false;
|
|
278
|
+
let shotStartX = 0, shotStartY = 0;
|
|
279
|
+
let recordingFlow = false;
|
|
280
|
+
let automationMode = false;
|
|
281
|
+
let automationInstructions = [];
|
|
282
|
+
let session = null; // { id, base, shots, events }
|
|
283
|
+
let flowMsg = null; // inline flow message DOM element
|
|
284
|
+
let pendingScreenshots = []; // array of { id, data } — base64 JPEGs waiting to be included in chat
|
|
285
|
+
let screenshotIdCounter = 0;
|
|
286
|
+
let pendingScreenshotMsgs = []; // DOM elements for the preview rows
|
|
287
|
+
const changes = new Map(); // selector -> { selector, before, removed? }
|
|
288
|
+
|
|
289
|
+
const { root } = mount();
|
|
290
|
+
const $ = (id) => root.getElementById(id);
|
|
291
|
+
const ui = {
|
|
292
|
+
puck: $("puck"), panel: $("panel"), dot: $("dot"), model: $("model"),
|
|
293
|
+
editBtn: $("editBtn"), saveBtn: $("saveBtn"), savedrop: $("savedrop"), flowBtn: $("flowBtn"),
|
|
294
|
+
msgs: $("msgs"), inspector: $("inspector"), selPath: $("selPath"),
|
|
295
|
+
chips: $("chips"), scopeSel: $("scopeSel"), textRow: $("textRow"), props: $("props"), addProp: $("addProp"),
|
|
296
|
+
insertRow: $("insertRow"), insertTag: $("insertTag"), addChild: $("addChild"), addAfter: $("addAfter"),
|
|
297
|
+
inText: $("inText"), inColor: $("inColor"), inBg: $("inBg"), inSize: $("inSize"),
|
|
298
|
+
delBtn: $("delBtn"), deselBtn: $("deselBtn"),
|
|
299
|
+
chatText: $("chatText"), chatSend: $("chatSend"), shotBtn: $("shotBtn"), automationBtn: $("automationBtn"),
|
|
300
|
+
automationBanner: $("automationBanner"), instCount: $("instCount"),
|
|
301
|
+
hoverHl: $("hoverHl"), selHl: $("selHl"), shotHl: $("shotHl")
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// ---- drag puck ----------------------------------------------------------
|
|
305
|
+
(function makePuckDraggable() {
|
|
306
|
+
let dragging = false, offX = 0, offY = 0;
|
|
307
|
+
|
|
308
|
+
function onStart(e) {
|
|
309
|
+
if (ui.panel.classList.contains("open")) return; // don't drag puck when panel is open
|
|
310
|
+
const rect = ui.puck.getBoundingClientRect();
|
|
311
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
312
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
313
|
+
offX = clientX - rect.left;
|
|
314
|
+
offY = clientY - rect.top;
|
|
315
|
+
ui.puck.style.left = rect.left + "px";
|
|
316
|
+
ui.puck.style.top = rect.top + "px";
|
|
317
|
+
ui.puck.style.right = "";
|
|
318
|
+
ui.puck.style.bottom = "";
|
|
319
|
+
dragging = true;
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function onMove(e) {
|
|
324
|
+
if (!dragging) return;
|
|
325
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
326
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
327
|
+
ui.puck.style.left = Math.max(0, clientX - offX) + "px";
|
|
328
|
+
ui.puck.style.top = Math.max(0, clientY - offY) + "px";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function onEnd() { dragging = false; }
|
|
332
|
+
|
|
333
|
+
ui.puck.addEventListener("mousedown", onStart);
|
|
334
|
+
ui.puck.addEventListener("touchstart", onStart, { passive: false });
|
|
335
|
+
document.addEventListener("mousemove", onMove);
|
|
336
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
337
|
+
document.addEventListener("mouseup", onEnd);
|
|
338
|
+
document.addEventListener("touchend", onEnd);
|
|
339
|
+
})();
|
|
340
|
+
|
|
341
|
+
// ---- drag panel ---------------------------------------------------------
|
|
342
|
+
(function makePanelDraggable() {
|
|
343
|
+
const head = ui.panel.querySelector(".head");
|
|
344
|
+
let dragging = false, offX = 0, offY = 0;
|
|
345
|
+
|
|
346
|
+
function onStart(e) {
|
|
347
|
+
if (e.target.closest && (e.target.closest("button") || e.target.closest("input") || e.target.closest("select"))) return;
|
|
348
|
+
const rect = ui.panel.getBoundingClientRect();
|
|
349
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
350
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
351
|
+
offX = clientX - rect.left;
|
|
352
|
+
offY = clientY - rect.top;
|
|
353
|
+
ui.panel.style.left = rect.left + "px";
|
|
354
|
+
ui.panel.style.top = Math.max(0, rect.top) + "px";
|
|
355
|
+
ui.panel.style.right = "";
|
|
356
|
+
ui.panel.style.bottom = "";
|
|
357
|
+
head.classList.add("dragging");
|
|
358
|
+
dragging = true;
|
|
359
|
+
e.preventDefault();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function onMove(e) {
|
|
363
|
+
if (!dragging) return;
|
|
364
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
365
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
366
|
+
const maxX = window.innerWidth - ui.panel.offsetWidth;
|
|
367
|
+
const maxY = window.innerHeight - ui.panel.offsetHeight;
|
|
368
|
+
ui.panel.style.left = Math.min(maxX, Math.max(0, clientX - offX)) + "px";
|
|
369
|
+
ui.panel.style.top = Math.min(maxY, Math.max(0, clientY - offY)) + "px";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function onEnd() {
|
|
373
|
+
if (!dragging) return;
|
|
374
|
+
head.classList.remove("dragging");
|
|
375
|
+
dragging = false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
head.addEventListener("mousedown", onStart);
|
|
379
|
+
head.addEventListener("touchstart", onStart, { passive: false });
|
|
380
|
+
document.addEventListener("mousemove", onMove);
|
|
381
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
382
|
+
document.addEventListener("mouseup", onEnd);
|
|
383
|
+
document.addEventListener("touchend", onEnd);
|
|
384
|
+
})();
|
|
385
|
+
|
|
386
|
+
// ---- helpers -----------------------------------------------------------
|
|
387
|
+
function isOurs(el) {
|
|
388
|
+
return el && (el.getRootNode() === root || el.id === "__vibedit_host" || el.closest && el.closest("#__vibedit_host"));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function cssPath(el) {
|
|
392
|
+
if (!(el instanceof Element)) return "";
|
|
393
|
+
const parts = [];
|
|
394
|
+
while (el && el.nodeType === 1 && el !== document.body) {
|
|
395
|
+
if (el.id) { parts.unshift(`#${CSS_escape(el.id)}`); break; }
|
|
396
|
+
let part = el.tagName.toLowerCase();
|
|
397
|
+
const cls = [...el.classList].filter((c) => !/^(hover|active|focus)/.test(c)).slice(0, 2);
|
|
398
|
+
if (cls.length) part += "." + cls.map(CSS_escape).join(".");
|
|
399
|
+
const parent = el.parentElement;
|
|
400
|
+
if (parent) {
|
|
401
|
+
const same = [...parent.children].filter((s) => s.tagName === el.tagName);
|
|
402
|
+
if (same.length > 1) part += `:nth-of-type(${same.indexOf(el) + 1})`;
|
|
403
|
+
}
|
|
404
|
+
parts.unshift(part);
|
|
405
|
+
el = el.parentElement;
|
|
406
|
+
}
|
|
407
|
+
return parts.join(" > ") || "body";
|
|
408
|
+
}
|
|
409
|
+
function CSS_escape(s) { return s.replace(/([^\w-])/g, "\\$1"); }
|
|
410
|
+
|
|
411
|
+
function prunedDOM(limit = 9000) {
|
|
412
|
+
const clone = document.body.cloneNode(true);
|
|
413
|
+
clone.querySelectorAll("script, style, noscript, svg, #__vibedit_host, link, meta").forEach((n) => n.remove());
|
|
414
|
+
clone.querySelectorAll("*").forEach((n) => {
|
|
415
|
+
[...n.attributes].forEach((a) => {
|
|
416
|
+
if (/^(data-v-|data-react|data-emotion|on)/.test(a.name)) n.removeAttribute(a.name);
|
|
417
|
+
else if (a.value.length > 120) n.setAttribute(a.name, a.value.slice(0, 120) + "...");
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
let html = clone.innerHTML.replace(/\n\s*\n/g, "\n").replace(/ +/g, " ");
|
|
421
|
+
return html.slice(0, limit);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function trackBefore(el) {
|
|
425
|
+
const selector = cssPath(el);
|
|
426
|
+
if (!changes.has(selector)) {
|
|
427
|
+
changes.set(selector, {
|
|
428
|
+
kind: "dom", selector, before: el.outerHTML, beforeText: directText(el), el,
|
|
429
|
+
parent: el.parentNode, next: el.nextSibling
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
updateCount();
|
|
433
|
+
return changes.get(selector);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function markTextChange(el) {
|
|
437
|
+
const rec = trackBefore(el);
|
|
438
|
+
rec.afterText = directText(el);
|
|
439
|
+
rec.after = el.outerHTML;
|
|
440
|
+
updateCount();
|
|
441
|
+
saveLocalStateSoon();
|
|
442
|
+
return rec;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function setCaretAtEnd(el) {
|
|
446
|
+
try {
|
|
447
|
+
el.focus({ preventScroll: true });
|
|
448
|
+
const range = document.createRange();
|
|
449
|
+
range.selectNodeContents(el);
|
|
450
|
+
range.collapse(false);
|
|
451
|
+
const sel = window.getSelection();
|
|
452
|
+
sel.removeAllRanges();
|
|
453
|
+
sel.addRange(range);
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function updateCount() {
|
|
458
|
+
ui.saveBtn.classList.toggle("hasChanges", changes.size > 0);
|
|
459
|
+
if (!changes.size) ui.savedrop.classList.remove("open");
|
|
460
|
+
saveLocalStateSoon();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function revertChange(key) {
|
|
464
|
+
const c = changes.get(key);
|
|
465
|
+
if (!c) return;
|
|
466
|
+
if (c.kind === "css") {
|
|
467
|
+
cssRules.delete(c.selector.slice(1)); // strip leading dot
|
|
468
|
+
rebuildStyles();
|
|
469
|
+
} else {
|
|
470
|
+
const wasSelected = selected && (selected === c.el || (c.el && c.el.contains(selected)));
|
|
471
|
+
if (c.removed) {
|
|
472
|
+
const tpl = document.createElement("template");
|
|
473
|
+
tpl.innerHTML = c.before.trim();
|
|
474
|
+
c.parent && c.parent.insertBefore(tpl.content, c.next || null);
|
|
475
|
+
} else if (c.el && c.el.isConnected) {
|
|
476
|
+
c.el.outerHTML = c.before;
|
|
477
|
+
} else {
|
|
478
|
+
const el = document.querySelector(c.selector);
|
|
479
|
+
if (el) el.outerHTML = c.before;
|
|
480
|
+
}
|
|
481
|
+
if (wasSelected) deselect();
|
|
482
|
+
}
|
|
483
|
+
changes.delete(key);
|
|
484
|
+
updateCount();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function addMsg(kind, text) {
|
|
488
|
+
const div = document.createElement("div");
|
|
489
|
+
div.className = `msg-${kind}`;
|
|
490
|
+
if (kind === "user") {
|
|
491
|
+
div.innerHTML = `<span class="msglabel-user">You</span> ${esc(text)}`;
|
|
492
|
+
} else if (kind === "ai") {
|
|
493
|
+
div.innerHTML = `<span class="msglabel-ai">AI</span> ${esc(text)}`;
|
|
494
|
+
} else if (kind === "instruction") {
|
|
495
|
+
div.innerHTML = `<span class="msglabel-instruction">Instruction</span> ${esc(text)}`;
|
|
496
|
+
} else {
|
|
497
|
+
if (/^Context: /.test(text)) {
|
|
498
|
+
div.classList.add("msg-context");
|
|
499
|
+
div.innerHTML = `<span class="msglabel-context">Context</span> ${esc(text.slice("Context: ".length))}<button class="dismiss" title="Remove context">×</button>`;
|
|
500
|
+
div.querySelector(".dismiss").addEventListener("click", () => div.remove());
|
|
501
|
+
} else {
|
|
502
|
+
div.textContent = text;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
ui.msgs.appendChild(div);
|
|
506
|
+
ui.msgs.scrollTop = ui.msgs.scrollHeight;
|
|
507
|
+
return div;
|
|
508
|
+
}
|
|
509
|
+
function esc(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
510
|
+
|
|
511
|
+
function addScreenshotPreview(id, data) {
|
|
512
|
+
const div = document.createElement("div");
|
|
513
|
+
div.className = "msg-sys shot-preview";
|
|
514
|
+
div.setAttribute("data-shot-id", id);
|
|
515
|
+
div.innerHTML = `<div style="position:relative;display:inline-block;max-width:280px"><img src="data:image/jpeg;base64,${data}" style="width:100%;border-radius:8px;display:block"><button class="btn danger shot-discard" title="Remove screenshot" style="position:absolute;top:4px;right:4px;padding:3px 6px;border-radius:6px;background:rgba(22,24,29,.85)">${ICONS.x}</button></div>`;
|
|
516
|
+
ui.msgs.appendChild(div);
|
|
517
|
+
ui.msgs.scrollTop = ui.msgs.scrollHeight;
|
|
518
|
+
pendingScreenshotMsgs.push(div);
|
|
519
|
+
div.querySelector(".shot-discard").addEventListener("click", () => discardScreenshot(id));
|
|
520
|
+
updateShotCount();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function discardScreenshot(id) {
|
|
524
|
+
pendingScreenshots = pendingScreenshots.filter((s) => s.id !== id);
|
|
525
|
+
const el = ui.msgs.querySelector(`.shot-preview[data-shot-id="${id}"]`);
|
|
526
|
+
if (el) el.remove();
|
|
527
|
+
pendingScreenshotMsgs = pendingScreenshotMsgs.filter((d) => d.getAttribute("data-shot-id") !== String(id));
|
|
528
|
+
updateShotCount();
|
|
529
|
+
saveLocalStateSoon();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function clearAllScreenshots() {
|
|
533
|
+
for (const el of pendingScreenshotMsgs) el.remove();
|
|
534
|
+
pendingScreenshots = [];
|
|
535
|
+
pendingScreenshotMsgs = [];
|
|
536
|
+
updateShotCount();
|
|
537
|
+
saveLocalStateSoon();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function updateShotCount() {
|
|
541
|
+
ui.shotBtn.classList.toggle("hasShots", pendingScreenshots.length > 0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ---- websocket ---------------------------------------------------------
|
|
545
|
+
function connect() {
|
|
546
|
+
ws = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
|
547
|
+
ws.onopen = () => {
|
|
548
|
+
wsOpen = true;
|
|
549
|
+
ui.dot.classList.add("on");
|
|
550
|
+
send({ type: "pageStateGet" });
|
|
551
|
+
};
|
|
552
|
+
ws.onclose = () => { wsOpen = false; ui.dot.classList.remove("on"); setTimeout(connect, 1500); };
|
|
553
|
+
ws.onmessage = (e) => {
|
|
554
|
+
let msg; try { msg = JSON.parse(e.data); } catch { return; }
|
|
555
|
+
handleServer(msg);
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function send(obj) { if (wsOpen) ws.send(JSON.stringify(obj)); }
|
|
559
|
+
|
|
560
|
+
let stateSaveTimer = null;
|
|
561
|
+
function saveLocalStateSoon() {
|
|
562
|
+
clearTimeout(stateSaveTimer);
|
|
563
|
+
stateSaveTimer = setTimeout(saveLocalState, 120);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function saveLocalState() {
|
|
567
|
+
if (!wsOpen) return;
|
|
568
|
+
send({
|
|
569
|
+
type: "pageStateSet",
|
|
570
|
+
state: {
|
|
571
|
+
panelOpen: ui.panel.classList.contains("open"),
|
|
572
|
+
automationMode,
|
|
573
|
+
automationInstructions,
|
|
574
|
+
pendingScreenshots: pendingScreenshots.map((s) => ({ id: s.id, size: (s.data || "").length })),
|
|
575
|
+
currentFlowSession: session ? {
|
|
576
|
+
id: session.id,
|
|
577
|
+
base: session.base,
|
|
578
|
+
shots: session.shots,
|
|
579
|
+
events: session.events,
|
|
580
|
+
} : null,
|
|
581
|
+
visualChanges: [...changes.values()].map((c) => ({
|
|
582
|
+
kind: c.kind,
|
|
583
|
+
selector: c.selector,
|
|
584
|
+
before: c.before,
|
|
585
|
+
beforeText: c.beforeText || "",
|
|
586
|
+
after: c.kind === "css" ? (c.after || "") : (c.removed ? "" : (c.el && c.el.isConnected ? c.el.outerHTML : "")),
|
|
587
|
+
afterText: c.afterText || "",
|
|
588
|
+
addedHTML: c.addedHTML || "",
|
|
589
|
+
removed: !!c.removed,
|
|
590
|
+
})),
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function restoreLocalState(saved) {
|
|
596
|
+
if (!saved || typeof saved !== "object") return;
|
|
597
|
+
ui.panel.classList.toggle("open", !!saved.panelOpen);
|
|
598
|
+
if (Array.isArray(saved.automationInstructions)) {
|
|
599
|
+
automationInstructions = saved.automationInstructions.filter((s) => typeof s === "string");
|
|
600
|
+
}
|
|
601
|
+
setAutomationMode(!!saved.automationMode);
|
|
602
|
+
if (saved.currentFlowSession && saved.currentFlowSession.id) {
|
|
603
|
+
session = saved.currentFlowSession;
|
|
604
|
+
showFlowInline();
|
|
605
|
+
}
|
|
606
|
+
if (Array.isArray(saved.visualChanges)) {
|
|
607
|
+
changes.clear();
|
|
608
|
+
for (const c of saved.visualChanges) {
|
|
609
|
+
if (!c || !c.selector) continue;
|
|
610
|
+
let el = null;
|
|
611
|
+
try { el = document.querySelector(c.selector); } catch {}
|
|
612
|
+
changes.set(c.selector, {
|
|
613
|
+
kind: c.kind || "dom",
|
|
614
|
+
selector: c.selector,
|
|
615
|
+
before: c.before || "",
|
|
616
|
+
beforeText: c.beforeText || "",
|
|
617
|
+
after: c.after || "",
|
|
618
|
+
afterText: c.afterText || "",
|
|
619
|
+
addedHTML: c.addedHTML || "",
|
|
620
|
+
removed: !!c.removed,
|
|
621
|
+
el,
|
|
622
|
+
parent: el ? el.parentNode : null,
|
|
623
|
+
next: el ? el.nextSibling : null,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
updateCount();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function handleServer(msg) {
|
|
631
|
+
if (msg.type === "hello") {
|
|
632
|
+
ui.model.textContent = msg.model + (msg.vision ? " (vision)" : "");
|
|
633
|
+
} else if (msg.type === "pageState") {
|
|
634
|
+
restoreLocalState(msg.state);
|
|
635
|
+
} else if (msg.type === "status") {
|
|
636
|
+
addMsg("sys", msg.text);
|
|
637
|
+
} else if (msg.type === "chatResult") {
|
|
638
|
+
if (msg.reply) addMsg("ai", msg.reply);
|
|
639
|
+
applyOps(msg.ops || []);
|
|
640
|
+
} else if (msg.type === "saveResult") {
|
|
641
|
+
addMsg(msg.ok ? "ai" : "sys", msg.summary + (msg.failed && msg.failed.length ? `\nFailed: ${msg.failed.join(", ")}` : ""));
|
|
642
|
+
if (!msg.ok && msg.modelOutput) addMsg("sys", "Model output (debug): " + msg.modelOutput);
|
|
643
|
+
if (msg.ok) { changes.clear(); updateCount(); saveLocalStateSoon(); }
|
|
644
|
+
} else if (msg.type === "flowStarted") {
|
|
645
|
+
addMsg("sys", "Recording userflow. Click and scroll as usual, then press the button again to stop.");
|
|
646
|
+
} else if (msg.type === "flowStopped") {
|
|
647
|
+
session = msg;
|
|
648
|
+
const evCount = msg.events.filter((e) => e.kind !== "shot").length;
|
|
649
|
+
addMsg("sys", `Recording stopped. ${evCount} interaction${evCount !== 1 ? "s" : ""} captured.`);
|
|
650
|
+
showFlowInline();
|
|
651
|
+
saveLocalStateSoon();
|
|
652
|
+
} else if (msg.type === "automationResult") {
|
|
653
|
+
if (msg.ok) {
|
|
654
|
+
addMsg("ai", `Automation script generated: ${msg.summary}\nSaved to: ${msg.file}`);
|
|
655
|
+
if (msg.notes) addMsg("sys", msg.notes);
|
|
656
|
+
automationInstructions = [];
|
|
657
|
+
updateInstCount();
|
|
658
|
+
} else {
|
|
659
|
+
addMsg("sys", "Automation failed: " + (msg.summary || "parse error"));
|
|
660
|
+
if (msg.modelOutput) addMsg("sys", "Model output (debug): " + msg.modelOutput);
|
|
661
|
+
}
|
|
662
|
+
} else if (msg.type === "error") {
|
|
663
|
+
addMsg("sys", "Error: " + msg.text);
|
|
664
|
+
} else if (msg.type === "screenshotResult") {
|
|
665
|
+
const id = ++screenshotIdCounter;
|
|
666
|
+
pendingScreenshots.push({ id, data: msg.data });
|
|
667
|
+
addScreenshotPreview(id, msg.data);
|
|
668
|
+
saveLocalStateSoon();
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ---- AI ops on live DOM --------------------------------------------------
|
|
673
|
+
function applyOps(ops) {
|
|
674
|
+
let applied = 0;
|
|
675
|
+
const done = [];
|
|
676
|
+
for (const op of ops) {
|
|
677
|
+
let el;
|
|
678
|
+
try { el = document.querySelector(op.selector); } catch { el = null; }
|
|
679
|
+
if (!el || isOurs(el)) continue;
|
|
680
|
+
const rec = trackBefore(el);
|
|
681
|
+
if (op.action === "setText") { el.textContent = op.value ?? ""; done.push("set text of " + op.selector); }
|
|
682
|
+
else if (op.action === "setHTML") { el.innerHTML = op.value ?? ""; done.push("set HTML of " + op.selector); }
|
|
683
|
+
else if (op.action === "setStyle" && op.style) { for (const [k, v] of Object.entries(op.style)) el.style.setProperty(toKebab(k), v); done.push("styled " + op.selector); }
|
|
684
|
+
else if (op.action === "setAttr") { el.setAttribute(op.name, op.value ?? ""); done.push("set attr " + op.name + " on " + op.selector); }
|
|
685
|
+
else if (op.action === "remove") { rec.removed = true; el.remove(); done.push("removed " + op.selector); }
|
|
686
|
+
else continue;
|
|
687
|
+
applied++;
|
|
688
|
+
}
|
|
689
|
+
if (ops.length && !applied) addMsg("sys", "The model returned selectors that do not exist on this page.");
|
|
690
|
+
else if (applied) addMsg("sys", "Applied: " + done.join(", "));
|
|
691
|
+
}
|
|
692
|
+
function toKebab(s) { return s.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); }
|
|
693
|
+
|
|
694
|
+
// ---- edit mode -----------------------------------------------------------
|
|
695
|
+
function setEditMode(on) {
|
|
696
|
+
editMode = on;
|
|
697
|
+
ui.editBtn.classList.toggle("active", on);
|
|
698
|
+
ui.editBtn.title = on ? "Edit mode on" : "Edit mode";
|
|
699
|
+
ui.panel.classList.toggle("editing", on);
|
|
700
|
+
if (on) { ui.inspector.classList.add("open"); ui.shotBtn.style.display = "none"; }
|
|
701
|
+
if (!on) { ui.hoverHl.style.display = "none"; deselect(); ui.shotBtn.style.display = ""; }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function setScreenshotMode(on) {
|
|
705
|
+
screenshotMode = on;
|
|
706
|
+
ui.shotBtn.classList.toggle("active", on);
|
|
707
|
+
if (editMode && on) setEditMode(false);
|
|
708
|
+
if (!on) { ui.shotHl.style.display = "none"; }
|
|
709
|
+
ui.puck.style.display = on ? "none" : ""; // hide puck during drag
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function setAutomationMode(on) {
|
|
713
|
+
automationMode = on;
|
|
714
|
+
ui.automationBtn.classList.toggle("automation", on);
|
|
715
|
+
ui.automationBtn.title = on ? "Automation mode on" : "Automation mode";
|
|
716
|
+
ui.panel.classList.toggle("automation", on);
|
|
717
|
+
ui.chatText.classList.toggle("automation", on);
|
|
718
|
+
ui.chatSend.classList.toggle("automation", on);
|
|
719
|
+
ui.chatText.placeholder = on ? "Describe what to automate... e.g. fill login form and click submit" : "Ask for a change... e.g. make the header dark blue";
|
|
720
|
+
if (on && editMode) setEditMode(false);
|
|
721
|
+
if (on) { updateInstCount(); }
|
|
722
|
+
saveLocalStateSoon();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function updateInstCount() {
|
|
726
|
+
ui.instCount.textContent = automationInstructions.length + " instruction" + (automationInstructions.length !== 1 ? "s" : "");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function positionHl(box, el) {
|
|
730
|
+
const r = el.getBoundingClientRect();
|
|
731
|
+
Object.assign(box.style, { display: "block", left: r.left - 2 + "px", top: r.top - 2 + "px", width: r.width + 2 + "px", height: r.height + 2 + "px" });
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function select(el) {
|
|
735
|
+
deselect(false);
|
|
736
|
+
selected = el;
|
|
737
|
+
scope = "el";
|
|
738
|
+
trackBefore(el);
|
|
739
|
+
positionHl(ui.selHl, el);
|
|
740
|
+
ui.inspector.classList.add("open");
|
|
741
|
+
ui.selPath.textContent = cssPath(el);
|
|
742
|
+
const cs = getComputedStyle(el);
|
|
743
|
+
ui.inText.value = directText(el);
|
|
744
|
+
ui.inColor.value = rgbToHex(cs.color);
|
|
745
|
+
ui.inBg.value = rgbToHex(cs.backgroundColor);
|
|
746
|
+
ui.inSize.value = parseInt(cs.fontSize, 10) || "";
|
|
747
|
+
refreshInspector();
|
|
748
|
+
el.setAttribute("contenteditable", "true");
|
|
749
|
+
el.setAttribute("spellcheck", "false");
|
|
750
|
+
el.addEventListener("input", onLiveType);
|
|
751
|
+
setCaretAtEnd(el);
|
|
752
|
+
addMsg("sys", "Context: " + cssPath(el));
|
|
753
|
+
send({ type: "context", selected: el.outerHTML.slice(0, 2000), selector: cssPath(el) });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function deselect(hide = true) {
|
|
757
|
+
if (selected) {
|
|
758
|
+
selected.removeAttribute("contenteditable");
|
|
759
|
+
selected.removeAttribute("spellcheck");
|
|
760
|
+
selected.removeEventListener("input", onLiveType);
|
|
761
|
+
}
|
|
762
|
+
selected = null;
|
|
763
|
+
if (hide) { ui.selHl.style.display = "none"; ui.inspector.classList.remove("open"); }
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function onLiveType() {
|
|
767
|
+
if (!selected) return;
|
|
768
|
+
ui.inText.value = directText(selected);
|
|
769
|
+
markTextChange(selected);
|
|
770
|
+
positionHl(ui.selHl, selected);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function directText(el) {
|
|
774
|
+
return [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.textContent).join("").trim() || el.textContent.trim().slice(0, 200);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function insertElement(where) {
|
|
778
|
+
if (!selected) return;
|
|
779
|
+
const tag = /^(p|div|span|button|h2|li)$/.test(ui.insertTag.value) ? ui.insertTag.value : "p";
|
|
780
|
+
const parent = where === "inside" ? selected : selected.parentElement;
|
|
781
|
+
if (!parent) return;
|
|
782
|
+
const rec = trackBefore(parent);
|
|
783
|
+
const el = document.createElement(tag);
|
|
784
|
+
el.textContent = "New text";
|
|
785
|
+
el.setAttribute("data-vibedit-added", "true");
|
|
786
|
+
if (where === "inside") selected.appendChild(el);
|
|
787
|
+
else selected.insertAdjacentElement("afterend", el);
|
|
788
|
+
rec.after = parent.outerHTML;
|
|
789
|
+
rec.added = true;
|
|
790
|
+
rec.addedHTML = el.outerHTML;
|
|
791
|
+
rec.afterText = directText(parent);
|
|
792
|
+
updateCount();
|
|
793
|
+
saveLocalStateSoon();
|
|
794
|
+
select(el);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function rgbToHex(rgb) {
|
|
798
|
+
const m = rgb && rgb.match(/(\d+)[, ]+(\d+)[, ]+(\d+)/);
|
|
799
|
+
if (!m) return "#000000";
|
|
800
|
+
return "#" + [m[1], m[2], m[3]].map((v) => (+v).toString(16).padStart(2, "0")).join("");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
document.addEventListener("mousemove", (e) => {
|
|
804
|
+
if (screenshotMode && shotStartX !== undefined) {
|
|
805
|
+
const x = Math.min(shotStartX, e.clientX);
|
|
806
|
+
const y = Math.min(shotStartY, e.clientY);
|
|
807
|
+
const w = Math.abs(e.clientX - shotStartX);
|
|
808
|
+
const h = Math.abs(e.clientY - shotStartY);
|
|
809
|
+
Object.assign(ui.shotHl.style, { display: "block", left: x + "px", top: y + "px", width: w + "px", height: h + "px" });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (!editMode || isOurs(e.target)) { if (editMode) ui.hoverHl.style.display = "none"; return; }
|
|
813
|
+
positionHl(ui.hoverHl, e.target);
|
|
814
|
+
}, true);
|
|
815
|
+
|
|
816
|
+
document.addEventListener("mousedown", (e) => {
|
|
817
|
+
if (!screenshotMode || isOurs(e.target)) return;
|
|
818
|
+
e.preventDefault();
|
|
819
|
+
shotStartX = e.clientX;
|
|
820
|
+
shotStartY = e.clientY;
|
|
821
|
+
}, true);
|
|
822
|
+
|
|
823
|
+
document.addEventListener("click", (e) => {
|
|
824
|
+
if (justShot) { justShot = false; return; }
|
|
825
|
+
if (recordingFlow && !isOurs(e.target)) {
|
|
826
|
+
send({ type: "flowEvent", ev: { kind: "click", selector: cssPath(e.target), text: (e.target.textContent || "").trim().slice(0, 80), x: e.clientX, y: e.clientY } });
|
|
827
|
+
}
|
|
828
|
+
if (!editMode || isOurs(e.target)) return;
|
|
829
|
+
if (selected && (e.target === selected || selected.contains(e.target))) {
|
|
830
|
+
positionHl(ui.selHl, selected);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
e.preventDefault(); e.stopPropagation();
|
|
834
|
+
select(e.target);
|
|
835
|
+
}, true);
|
|
836
|
+
|
|
837
|
+
document.addEventListener("mouseup", (e) => {
|
|
838
|
+
if (!screenshotMode || isOurs(e.target) || shotStartX === undefined) return;
|
|
839
|
+
const x1 = Math.min(shotStartX, e.clientX);
|
|
840
|
+
const y1 = Math.min(shotStartY, e.clientY);
|
|
841
|
+
const x2 = Math.max(shotStartX, e.clientX);
|
|
842
|
+
const y2 = Math.max(shotStartY, e.clientY);
|
|
843
|
+
const w = x2 - x1;
|
|
844
|
+
const h = y2 - y1;
|
|
845
|
+
shotStartX = undefined;
|
|
846
|
+
if (w < 5 || h < 5) return; // too small, ignore
|
|
847
|
+
setScreenshotMode(false);
|
|
848
|
+
justShot = true;
|
|
849
|
+
addMsg("sys", "Screenshot capturing...");
|
|
850
|
+
send({ type: "screenshot", x: Math.round(x1), y: Math.round(y1), width: Math.round(w), height: Math.round(h) });
|
|
851
|
+
}, true);
|
|
852
|
+
|
|
853
|
+
let scrollT = null;
|
|
854
|
+
document.addEventListener("scroll", () => {
|
|
855
|
+
if (selected) positionHl(ui.selHl, selected);
|
|
856
|
+
if (!recordingFlow) return;
|
|
857
|
+
clearTimeout(scrollT);
|
|
858
|
+
scrollT = setTimeout(() => send({ type: "flowEvent", ev: { kind: "scroll", y: Math.round(window.scrollY) } }), 250);
|
|
859
|
+
}, true);
|
|
860
|
+
|
|
861
|
+
document.addEventListener("input", (e) => {
|
|
862
|
+
if (recordingFlow && !isOurs(e.target) && /^(input|textarea|select)$/i.test(e.target.tagName)) {
|
|
863
|
+
send({ type: "flowEvent", ev: { kind: "input", selector: cssPath(e.target) } });
|
|
864
|
+
}
|
|
865
|
+
}, true);
|
|
866
|
+
|
|
867
|
+
// ---- CSS value autocomplete map -------------------------------------------
|
|
868
|
+
const CSS_VALUES = {
|
|
869
|
+
display: ["flex", "grid", "block", "inline", "inline-block", "inline-flex", "inline-grid", "none", "contents", "table", "table-row", "table-cell"],
|
|
870
|
+
position: ["static", "relative", "absolute", "fixed", "sticky"],
|
|
871
|
+
"flex-direction": ["row", "column", "row-reverse", "column-reverse"],
|
|
872
|
+
"flex-wrap": ["nowrap", "wrap", "wrap-reverse"],
|
|
873
|
+
"align-items": ["stretch", "flex-start", "flex-end", "center", "baseline"],
|
|
874
|
+
"align-self": ["auto", "stretch", "flex-start", "flex-end", "center", "baseline"],
|
|
875
|
+
"align-content": ["stretch", "flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"],
|
|
876
|
+
"justify-content": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"],
|
|
877
|
+
"justify-items": ["stretch", "start", "end", "center"],
|
|
878
|
+
"justify-self": ["auto", "stretch", "start", "end", "center"],
|
|
879
|
+
"text-align": ["left", "center", "right", "justify", "start", "end"],
|
|
880
|
+
"vertical-align": ["baseline", "top", "middle", "bottom", "text-top", "text-bottom", "sub", "super"],
|
|
881
|
+
"font-weight": ["normal", "bold", "lighter", "bolder", "100", "200", "300", "400", "500", "600", "700", "800", "900"],
|
|
882
|
+
"font-style": ["normal", "italic", "oblique"],
|
|
883
|
+
"text-transform": ["none", "uppercase", "lowercase", "capitalize"],
|
|
884
|
+
"text-decoration": ["none", "underline", "overline", "line-through", "underline overline"],
|
|
885
|
+
"white-space": ["normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"],
|
|
886
|
+
"overflow": ["visible", "hidden", "scroll", "auto", "clip"],
|
|
887
|
+
"overflow-x": ["visible", "hidden", "scroll", "auto", "clip"],
|
|
888
|
+
"overflow-y": ["visible", "hidden", "scroll", "auto", "clip"],
|
|
889
|
+
"visibility": ["visible", "hidden", "collapse"],
|
|
890
|
+
"cursor": ["default", "pointer", "text", "move", "not-allowed", "grab", "grabbing", "crosshair", "zoom-in", "zoom-out", "help", "wait", "progress", "cell", "col-resize", "row-resize", "ew-resize", "ns-resize", "none"],
|
|
891
|
+
"pointer-events": ["auto", "none"],
|
|
892
|
+
"box-sizing": ["content-box", "border-box"],
|
|
893
|
+
"object-fit": ["fill", "contain", "cover", "none", "scale-down"],
|
|
894
|
+
"object-position": ["top", "center", "bottom", "left", "right", "top left", "top center", "top right", "center left", "center center", "center right", "bottom left", "bottom center", "bottom right"],
|
|
895
|
+
"border-style": ["none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset", "hidden"],
|
|
896
|
+
"outline-style": ["none", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"],
|
|
897
|
+
"resize": ["none", "both", "horizontal", "vertical"],
|
|
898
|
+
"user-select": ["auto", "none", "text", "all", "contain"],
|
|
899
|
+
"word-break": ["normal", "break-all", "keep-all", "break-word"],
|
|
900
|
+
"text-overflow": ["clip", "ellipsis"],
|
|
901
|
+
"mix-blend-mode": ["normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion"],
|
|
902
|
+
"background-size": ["auto", "cover", "contain"],
|
|
903
|
+
"background-repeat": ["repeat", "no-repeat", "repeat-x", "repeat-y", "space", "round"],
|
|
904
|
+
"background-attachment": ["scroll", "fixed", "local"],
|
|
905
|
+
"background-position": ["top", "center", "bottom", "left", "right", "top left", "top center", "top right", "center left", "center center", "center right", "bottom left", "bottom center", "bottom right"],
|
|
906
|
+
isolation: ["auto", "isolate"],
|
|
907
|
+
"z-index": ["0", "1", "10", "100", "auto"],
|
|
908
|
+
"border-collapse": ["collapse", "separate"],
|
|
909
|
+
"float": ["none", "left", "right"],
|
|
910
|
+
clear: ["none", "left", "right", "both"],
|
|
911
|
+
"grid-template-columns": ["repeat(auto-fill, minmax(200px, 1fr))", "1fr", "auto"],
|
|
912
|
+
"grid-template-rows": ["auto", "1fr"],
|
|
913
|
+
gap: ["0", "4px", "8px", "12px", "16px", "24px", "1rem"],
|
|
914
|
+
"border-radius": ["0", "4px", "8px", "12px", "16px", "50%", "9999px"],
|
|
915
|
+
opacity: ["0", "0.1", "0.25", "0.5", "0.75", "0.9", "1"],
|
|
916
|
+
"transition-timing-function": ["ease", "linear", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end"],
|
|
917
|
+
transform: ["none"],
|
|
918
|
+
"animation-fill-mode": ["none", "forwards", "backwards", "both"],
|
|
919
|
+
"animation-direction": ["normal", "reverse", "alternate", "alternate-reverse"],
|
|
920
|
+
"animation-timing-function": ["ease", "linear", "ease-in", "ease-out", "ease-in-out"],
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const COLOR_PROPS = new Set([
|
|
924
|
+
"color", "background-color", "border-color", "border-top-color", "border-right-color",
|
|
925
|
+
"border-bottom-color", "border-left-color", "outline-color", "text-decoration-color",
|
|
926
|
+
"caret-color", "accent-color", "column-rule-color", "fill", "stroke",
|
|
927
|
+
"background", "border", "border-top", "border-right", "border-bottom", "border-left",
|
|
928
|
+
"outline", "column-rule", "box-shadow", "text-shadow",
|
|
929
|
+
]);
|
|
930
|
+
|
|
931
|
+
function isColorProp(prop) { return COLOR_PROPS.has(prop); }
|
|
932
|
+
|
|
933
|
+
function isColorValue(val) {
|
|
934
|
+
if (!val) return false;
|
|
935
|
+
return /^\s*#([0-9a-fA-F]{3,8})\s*$/.test(val) ||
|
|
936
|
+
/^\s*rgb\s*\(/.test(val) ||
|
|
937
|
+
/^\s*hsl\s*\(/.test(val) ||
|
|
938
|
+
/^\s*rgba\s*\(/.test(val) ||
|
|
939
|
+
/^\s*hsla\s*\(/.test(val) ||
|
|
940
|
+
/^\s*(transparent|currentColor|inherit|initial|unset)\s*$/.test(val) ||
|
|
941
|
+
/^\s*(red|blue|green|white|black|yellow|orange|purple|pink|brown|gray|grey|cyan|magenta|maroon|navy|teal|olive|silver|gold|coral|salmon|turquoise|violet|indigo|crimson|tomato|chocolate|beige|ivory|lavender|wheat|khaki|tan|plum|orchid|azure|mint|aqua|lime|rose|peach|skyblue|cornflowerblue|royalblue|steelblue|darkblue|darkgreen|darkred|darkorange|lightblue|lightgreen|lightgray|lightgrey|darkgray|darkgrey|dimgray|dimgrey|whitesmoke)\s*$/.test(val);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function parseColor(val) {
|
|
945
|
+
if (!val) return null;
|
|
946
|
+
val = val.trim();
|
|
947
|
+
if (/^#([0-9a-fA-F]{3,8})$/.test(val)) return val.length === 4 ? val.replace(/^#(.)(.)(.)$/, "#$1$1$2$2$3$3") : val.padEnd(7, "0");
|
|
948
|
+
// map common named colors to hex
|
|
949
|
+
const named = { red:"#ff0000", blue:"#0000ff", green:"#008000", white:"#ffffff", black:"#000000", yellow:"#ffff00", orange:"#ffa500", purple:"#800080", pink:"#ffc0cb", brown:"#a52a2a", gray:"#808080", grey:"#808080", cyan:"#00ffff", magenta:"#ff00ff", maroon:"#800000", navy:"#000080", teal:"#008080", olive:"#808000", silver:"#c0c0c0", gold:"#ffd700", coral:"#ff7f50", salmon:"#fa8072", turquoise:"#40e0d0", violet:"#ee82ee", indigo:"#4b0082", crimson:"#dc143c", tomato:"#ff6347", beige:"#f5f5dc", ivory:"#fffff0", lavender:"#e6e6fa", wheat:"#f5deb3", khaki:"#f0e68c", tan:"#d2b48c", plum:"#dda0dd", orchid:"#da70d6", azure:"#f0ffff", mint:"#f5fffa", aqua:"#00ffff", lime:"#00ff00", skyblue:"#87ceeb", lightblue:"#add8e6", lightgreen:"#90ee90", lightgray:"#d3d3d3", darkgray:"#a9a9a9", transparent:"#000000", currentColor:"#000000", };
|
|
950
|
+
if (named[val.toLowerCase()]) return named[val.toLowerCase()];
|
|
951
|
+
// try to extract from rgb/hsl
|
|
952
|
+
const m = val.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
953
|
+
if (m) { return "#" + m.slice(1,4).map(n => parseInt(n).toString(16).padStart(2,"0")).join(""); }
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function suggestValues(prop) {
|
|
958
|
+
const exact = CSS_VALUES[prop];
|
|
959
|
+
if (exact) return exact;
|
|
960
|
+
// try shorthand/longhand matching
|
|
961
|
+
for (const [k, v] of Object.entries(CSS_VALUES)) {
|
|
962
|
+
if (k.includes(prop) || prop.includes(k)) return v;
|
|
963
|
+
}
|
|
964
|
+
return [];
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function numericCssValue(value) {
|
|
968
|
+
const m = String(value || "").trim().match(/^(-?\d+(?:\.\d+)?)([a-z%]*)$/i);
|
|
969
|
+
if (!m) return null;
|
|
970
|
+
return { n: Number(m[1]), unit: m[2] || "" };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function stepCssValue(value, direction, event) {
|
|
974
|
+
const parsed = numericCssValue(value);
|
|
975
|
+
if (!parsed || Number.isNaN(parsed.n)) return null;
|
|
976
|
+
const step = event.shiftKey ? 10 : event.altKey ? 0.1 : 1;
|
|
977
|
+
const next = parsed.n + direction * step;
|
|
978
|
+
const fixed = Math.abs(step) < 1 ? Number(next.toFixed(2)) : next;
|
|
979
|
+
return `${fixed}${parsed.unit}`;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// scope is "el" (inline styles on the selected element) or a class name
|
|
983
|
+
// (edits a .class rule applied to every element with that class).
|
|
984
|
+
let scope = "el";
|
|
985
|
+
const cssRules = new Map(); // class -> Map(prop -> value)
|
|
986
|
+
let styleEl = null;
|
|
987
|
+
|
|
988
|
+
function ensureStyleEl() {
|
|
989
|
+
if (!styleEl || !styleEl.isConnected) {
|
|
990
|
+
styleEl = document.createElement("style");
|
|
991
|
+
styleEl.id = "__vibedit_style";
|
|
992
|
+
document.head.appendChild(styleEl);
|
|
993
|
+
}
|
|
994
|
+
return styleEl;
|
|
995
|
+
}
|
|
996
|
+
function rebuildStyles() {
|
|
997
|
+
ensureStyleEl().textContent = [...cssRules.entries()]
|
|
998
|
+
.filter(([, m]) => m.size)
|
|
999
|
+
.map(([cls, m]) => `.${CSS_escape(cls)} { ${[...m.entries()].map(([p, v]) => `${p}: ${v} !important`).join("; ")} }`)
|
|
1000
|
+
.join("\n");
|
|
1001
|
+
}
|
|
1002
|
+
function serializeRule(cls) {
|
|
1003
|
+
const m = cssRules.get(cls) || new Map();
|
|
1004
|
+
return `.${cls} {\n${[...m.entries()].map(([p, v]) => ` ${p}: ${v};`).join("\n")}\n}`;
|
|
1005
|
+
}
|
|
1006
|
+
function existingRuleText(cls) {
|
|
1007
|
+
const out = [];
|
|
1008
|
+
const re = new RegExp(`\\.${cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\w-])`);
|
|
1009
|
+
const visit = (rule) => {
|
|
1010
|
+
if (rule.selectorText && re.test(rule.selectorText)) out.push(rule.cssText);
|
|
1011
|
+
if (rule.cssRules && rule.cssRules.length) for (const r of rule.cssRules) visit(r);
|
|
1012
|
+
};
|
|
1013
|
+
for (const sheet of document.styleSheets) {
|
|
1014
|
+
if (sheet.ownerNode && sheet.ownerNode.id === "__vibedit_style") continue;
|
|
1015
|
+
let rules; try { rules = sheet.cssRules; } catch { continue; } // cross-origin
|
|
1016
|
+
for (const r of rules || []) visit(r);
|
|
1017
|
+
}
|
|
1018
|
+
return out.join("\n");
|
|
1019
|
+
}
|
|
1020
|
+
function existingDecls(cls) {
|
|
1021
|
+
const m = new Map();
|
|
1022
|
+
for (const block of existingRuleText(cls).matchAll(/\{([^}]*)\}/g)) {
|
|
1023
|
+
for (const decl of block[1].split(";")) {
|
|
1024
|
+
const i = decl.indexOf(":");
|
|
1025
|
+
if (i > 0) m.set(decl.slice(0, i).trim(), decl.slice(i + 1).trim());
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return m;
|
|
1029
|
+
}
|
|
1030
|
+
function trackCss(cls) {
|
|
1031
|
+
const key = "css:" + cls;
|
|
1032
|
+
if (!changes.has(key)) {
|
|
1033
|
+
changes.set(key, { kind: "css", selector: "." + cls, before: existingRuleText(cls) || "(no existing rule)" });
|
|
1034
|
+
}
|
|
1035
|
+
changes.get(key).after = serializeRule(cls);
|
|
1036
|
+
updateCount();
|
|
1037
|
+
}
|
|
1038
|
+
function setProp(prop, value) {
|
|
1039
|
+
prop = prop.trim();
|
|
1040
|
+
if (!prop) return;
|
|
1041
|
+
if (scope === "el") {
|
|
1042
|
+
if (!selected) return;
|
|
1043
|
+
trackBefore(selected);
|
|
1044
|
+
if (value === "") selected.style.removeProperty(prop);
|
|
1045
|
+
else selected.style.setProperty(prop, value);
|
|
1046
|
+
} else {
|
|
1047
|
+
if (!cssRules.has(scope)) cssRules.set(scope, new Map());
|
|
1048
|
+
const m = cssRules.get(scope);
|
|
1049
|
+
if (value === "") {
|
|
1050
|
+
// removing a declaration that exists in the project CSS needs an override
|
|
1051
|
+
if (existingDecls(scope).has(prop)) m.set(prop, "unset");
|
|
1052
|
+
else m.delete(prop);
|
|
1053
|
+
} else m.set(prop, value);
|
|
1054
|
+
rebuildStyles();
|
|
1055
|
+
trackCss(scope);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function propsForScope() {
|
|
1059
|
+
if (scope === "el") {
|
|
1060
|
+
const m = new Map();
|
|
1061
|
+
if (selected) for (let i = 0; i < selected.style.length; i++) {
|
|
1062
|
+
const p = selected.style[i];
|
|
1063
|
+
m.set(p, selected.style.getPropertyValue(p));
|
|
1064
|
+
}
|
|
1065
|
+
return m;
|
|
1066
|
+
}
|
|
1067
|
+
const m = existingDecls(scope);
|
|
1068
|
+
const ov = cssRules.get(scope);
|
|
1069
|
+
if (ov) for (const [p, v] of ov) m.set(p, v);
|
|
1070
|
+
return m;
|
|
1071
|
+
}
|
|
1072
|
+
const COMMON_CSS = [
|
|
1073
|
+
"color", "background", "background-color", "background-image", "background-size",
|
|
1074
|
+
"border", "border-radius", "border-color", "border-width", "border-style",
|
|
1075
|
+
"padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
|
|
1076
|
+
"margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
|
|
1077
|
+
"width", "max-width", "min-width", "height", "max-height", "min-height",
|
|
1078
|
+
"display", "position", "top", "right", "bottom", "left", "z-index",
|
|
1079
|
+
"flex", "flex-direction", "flex-wrap", "flex-grow", "flex-shrink", "flex-basis",
|
|
1080
|
+
"align-items", "justify-content", "align-self", "justify-self", "gap",
|
|
1081
|
+
"grid", "grid-template-columns", "grid-template-rows", "grid-column", "grid-row",
|
|
1082
|
+
"font-family", "font-size", "font-weight", "font-style", "line-height",
|
|
1083
|
+
"text-align", "text-decoration", "text-transform", "letter-spacing", "word-spacing",
|
|
1084
|
+
"opacity", "visibility", "overflow", "overflow-x", "overflow-y",
|
|
1085
|
+
"box-shadow", "text-shadow", "filter", "backdrop-filter",
|
|
1086
|
+
"transform", "transition", "animation", "cursor", "pointer-events",
|
|
1087
|
+
"white-space", "word-break", "text-overflow", "object-fit", "object-position",
|
|
1088
|
+
"outline", "outline-color", "outline-width", "outline-style", "outline-offset",
|
|
1089
|
+
"box-sizing", "aspect-ratio", "user-select", "scroll-behavior",
|
|
1090
|
+
"list-style", "content", "clip-path", "mask", "mask-image",
|
|
1091
|
+
"inset", "inset-block", "inset-inline", "place-items", "place-content",
|
|
1092
|
+
"accent-color", "caret-color", "scrollbar-width", "scrollbar-color",
|
|
1093
|
+
"writing-mode", "direction", "unicode-bidi", "text-orientation",
|
|
1094
|
+
"mix-blend-mode", "isolation", "backface-visibility", "perspective",
|
|
1095
|
+
"will-change", "contain", "container-type", "container-name",
|
|
1096
|
+
"font-variant", "font-stretch", "font-optical-sizing",
|
|
1097
|
+
"tab-size", "hyphens", "overflow-wrap", "line-clamp",
|
|
1098
|
+
"rotate", "scale", "translate", "offset", "offset-path", "offset-distance",
|
|
1099
|
+
"resize", "touch-action", "overscroll-behavior", "scroll-snap-type", "scroll-snap-align",
|
|
1100
|
+
"shape-outside", "shape-margin", "image-rendering", "color-scheme",
|
|
1101
|
+
"columns", "column-gap", "row-gap", "order", "flex-flow",
|
|
1102
|
+
"align-content", "justify-items", "justify-self",
|
|
1103
|
+
"grid-auto-columns", "grid-auto-rows", "grid-auto-flow",
|
|
1104
|
+
"grid-template-areas", "grid-area", "grid-column-start", "grid-column-end",
|
|
1105
|
+
"grid-row-start", "grid-row-end",
|
|
1106
|
+
"border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius",
|
|
1107
|
+
"border-top", "border-right", "border-bottom", "border-left",
|
|
1108
|
+
];
|
|
1109
|
+
|
|
1110
|
+
function addPropRow(p = "", v = "") {
|
|
1111
|
+
const row = document.createElement("div");
|
|
1112
|
+
row.className = "prow";
|
|
1113
|
+
row.innerHTML = `<input class="pname" placeholder="property"><input class="pval" placeholder="value"><input type="color" class="pcolor" title="Color picker"><button class="iconbtn" title="Remove">${ICONS.x}</button>`;
|
|
1114
|
+
const [pn, pv, pc] = row.querySelectorAll("input");
|
|
1115
|
+
pn.value = p; pv.value = v;
|
|
1116
|
+
|
|
1117
|
+
const commit = () => { if (pn.value.trim() && pv.value.trim()) setProp(pn.value, pv.value); };
|
|
1118
|
+
|
|
1119
|
+
// --- color picker integration ---
|
|
1120
|
+
pc.style.display = "none";
|
|
1121
|
+
const syncColorPicker = () => {
|
|
1122
|
+
const prop = pn.value.trim();
|
|
1123
|
+
const val = pv.value.trim();
|
|
1124
|
+
if (isColorProp(prop) || isColorValue(val)) {
|
|
1125
|
+
pc.style.display = "";
|
|
1126
|
+
const c = parseColor(val);
|
|
1127
|
+
if (c && pc.value !== c) pc.value = c;
|
|
1128
|
+
} else {
|
|
1129
|
+
pc.style.display = "none";
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
pc.addEventListener("input", () => {
|
|
1133
|
+
pv.value = pc.value;
|
|
1134
|
+
commit();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// --- wired autocomplete (arrow keys + tab/enter to select) ---
|
|
1138
|
+
function wireAutocomplete(input, getSuggestions) {
|
|
1139
|
+
let idx = -1;
|
|
1140
|
+
input.addEventListener("keydown", (e) => {
|
|
1141
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
1142
|
+
if (input === pv && numericCssValue(input.value)) {
|
|
1143
|
+
e.preventDefault();
|
|
1144
|
+
const next = stepCssValue(input.value, e.key === "ArrowUp" ? 1 : -1, e);
|
|
1145
|
+
if (next != null) {
|
|
1146
|
+
input.value = next;
|
|
1147
|
+
syncColorPicker();
|
|
1148
|
+
commit();
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
e.preventDefault();
|
|
1153
|
+
const sug = getSuggestions();
|
|
1154
|
+
if (!sug.length) { idx = -1; return; }
|
|
1155
|
+
if (idx === -1) idx = e.key === "ArrowDown" ? -1 : sug.length;
|
|
1156
|
+
idx = (idx + (e.key === "ArrowDown" ? 1 : -1) + sug.length) % sug.length;
|
|
1157
|
+
input.value = sug[idx];
|
|
1158
|
+
} else if (e.key === "Tab" || e.key === "Enter") {
|
|
1159
|
+
if (idx >= 0) {
|
|
1160
|
+
e.preventDefault();
|
|
1161
|
+
const sug = getSuggestions();
|
|
1162
|
+
if (sug[idx]) input.value = sug[idx];
|
|
1163
|
+
idx = -1;
|
|
1164
|
+
}
|
|
1165
|
+
if (e.key === "Enter") commit();
|
|
1166
|
+
} else {
|
|
1167
|
+
idx = -1;
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
wireAutocomplete(pn, () => {
|
|
1173
|
+
const v = pn.value.trim().toLowerCase();
|
|
1174
|
+
return v ? COMMON_CSS.filter((s) => s.startsWith(v)).slice(0, 20) : COMMON_CSS.slice(0, 20);
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
wireAutocomplete(pv, () => suggestValues(pn.value.trim()));
|
|
1178
|
+
|
|
1179
|
+
// --- wire events ---
|
|
1180
|
+
pn.addEventListener("change", commit);
|
|
1181
|
+
pn.addEventListener("input", () => { syncColorPicker(); if (pv.value.trim()) commit(); });
|
|
1182
|
+
pv.addEventListener("change", commit);
|
|
1183
|
+
pv.addEventListener("input", () => { syncColorPicker(); commit(); });
|
|
1184
|
+
row.querySelector("button").addEventListener("click", () => {
|
|
1185
|
+
if (pn.value.trim()) setProp(pn.value, "");
|
|
1186
|
+
row.remove();
|
|
1187
|
+
});
|
|
1188
|
+
ui.props.appendChild(row);
|
|
1189
|
+
if (p) { syncColorPicker(); }
|
|
1190
|
+
if (!p) pn.focus();
|
|
1191
|
+
}
|
|
1192
|
+
function renderProps() {
|
|
1193
|
+
ui.props.innerHTML = "";
|
|
1194
|
+
for (const [p, v] of propsForScope()) addPropRow(p, v);
|
|
1195
|
+
}
|
|
1196
|
+
function renderChips() {
|
|
1197
|
+
ui.chips.innerHTML = "";
|
|
1198
|
+
if (!selected) return;
|
|
1199
|
+
for (const cls of [...selected.classList]) {
|
|
1200
|
+
const chip = document.createElement("span");
|
|
1201
|
+
chip.className = "chip" + (scope === cls ? " scoped" : "");
|
|
1202
|
+
chip.innerHTML = `<span>.${cls}</span><button title="Remove class from element">${ICONS.x}</button>`;
|
|
1203
|
+
chip.querySelector("button").addEventListener("click", (e) => {
|
|
1204
|
+
e.stopPropagation();
|
|
1205
|
+
trackBefore(selected);
|
|
1206
|
+
selected.classList.remove(cls);
|
|
1207
|
+
if (scope === cls) scope = "el";
|
|
1208
|
+
refreshInspector();
|
|
1209
|
+
});
|
|
1210
|
+
chip.addEventListener("click", () => { scope = cls; refreshInspector(); });
|
|
1211
|
+
ui.chips.appendChild(chip);
|
|
1212
|
+
}
|
|
1213
|
+
const add = document.createElement("input");
|
|
1214
|
+
add.className = "chipadd";
|
|
1215
|
+
add.placeholder = "add class";
|
|
1216
|
+
add.addEventListener("keydown", (e) => {
|
|
1217
|
+
if (e.key === "Enter" && add.value.trim()) {
|
|
1218
|
+
trackBefore(selected);
|
|
1219
|
+
selected.classList.add(add.value.trim().replace(/^\./, ""));
|
|
1220
|
+
refreshInspector();
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
ui.chips.appendChild(add);
|
|
1224
|
+
}
|
|
1225
|
+
function renderScope() {
|
|
1226
|
+
ui.scopeSel.innerHTML = "";
|
|
1227
|
+
const optEl = document.createElement("option");
|
|
1228
|
+
optEl.value = "el";
|
|
1229
|
+
optEl.textContent = "This element only";
|
|
1230
|
+
ui.scopeSel.appendChild(optEl);
|
|
1231
|
+
if (selected) for (const cls of selected.classList) {
|
|
1232
|
+
const o = document.createElement("option");
|
|
1233
|
+
o.value = cls;
|
|
1234
|
+
o.textContent = `.${cls} (every element with this class)`;
|
|
1235
|
+
ui.scopeSel.appendChild(o);
|
|
1236
|
+
}
|
|
1237
|
+
ui.scopeSel.value = scope !== "el" && selected && selected.classList.contains(scope) ? scope : "el";
|
|
1238
|
+
if (ui.scopeSel.value === "el") scope = "el";
|
|
1239
|
+
}
|
|
1240
|
+
function refreshInspector() {
|
|
1241
|
+
renderChips();
|
|
1242
|
+
renderScope();
|
|
1243
|
+
renderProps();
|
|
1244
|
+
ui.textRow.style.display = scope === "el" ? "flex" : "none";
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ---- inspector wiring ------------------------------------------------------
|
|
1248
|
+
ui.scopeSel.addEventListener("change", () => { scope = ui.scopeSel.value; refreshInspector(); });
|
|
1249
|
+
ui.addProp.addEventListener("click", () => addPropRow());
|
|
1250
|
+
ui.addChild.addEventListener("click", () => insertElement("inside"));
|
|
1251
|
+
ui.addAfter.addEventListener("click", () => insertElement("after"));
|
|
1252
|
+
ui.inText.addEventListener("input", () => {
|
|
1253
|
+
if (!selected) return;
|
|
1254
|
+
trackBefore(selected);
|
|
1255
|
+
const tn = [...selected.childNodes].find((n) => n.nodeType === 3 && n.textContent.trim());
|
|
1256
|
+
if (tn) tn.textContent = ui.inText.value;
|
|
1257
|
+
else if (selected.children.length === 0) selected.textContent = ui.inText.value;
|
|
1258
|
+
markTextChange(selected);
|
|
1259
|
+
positionHl(ui.selHl, selected);
|
|
1260
|
+
});
|
|
1261
|
+
ui.inColor.addEventListener("input", () => { setProp("color", ui.inColor.value); renderProps(); });
|
|
1262
|
+
ui.inBg.addEventListener("input", () => { setProp("background-color", ui.inBg.value); renderProps(); });
|
|
1263
|
+
ui.inSize.addEventListener("input", () => { if (ui.inSize.value) { setProp("font-size", ui.inSize.value + "px"); renderProps(); } });
|
|
1264
|
+
ui.delBtn.addEventListener("click", () => {
|
|
1265
|
+
if (!selected) return;
|
|
1266
|
+
const rec = changes.get(cssPath(selected)) || trackBefore(selected);
|
|
1267
|
+
rec.removed = true;
|
|
1268
|
+
selected.remove();
|
|
1269
|
+
deselect();
|
|
1270
|
+
});
|
|
1271
|
+
ui.deselBtn.addEventListener("click", () => deselect());
|
|
1272
|
+
|
|
1273
|
+
// ---- toolbar ----------------------------------------------------------------
|
|
1274
|
+
ui.puck.addEventListener("click", () => {
|
|
1275
|
+
ui.panel.classList.toggle("open");
|
|
1276
|
+
saveLocalStateSoon();
|
|
1277
|
+
});
|
|
1278
|
+
ui.editBtn.addEventListener("click", () => setEditMode(!editMode));
|
|
1279
|
+
ui.shotBtn.addEventListener("click", () => setScreenshotMode(!screenshotMode));
|
|
1280
|
+
|
|
1281
|
+
function renderSaveDrop() {
|
|
1282
|
+
const entries = [...changes];
|
|
1283
|
+
if (!entries.length) { ui.savedrop.classList.remove("open"); return; }
|
|
1284
|
+
const domCount = entries.filter(([,c]) => c.kind !== "css").length;
|
|
1285
|
+
const cssCount = entries.filter(([,c]) => c.kind === "css").length;
|
|
1286
|
+
let html = `<div class="drophead">${entries.length} change${entries.length !== 1 ? "s" : ""} — ${domCount} element${domCount !== 1 ? "s" : ""}, ${cssCount} CSS rule${cssCount !== 1 ? "s" : ""}</div>`;
|
|
1287
|
+
for (const [key, c] of entries) {
|
|
1288
|
+
const sel = c.kind === "css" ? c.selector : `…${c.selector.slice(-50)}`;
|
|
1289
|
+
html += `<div class="droprow"><span class="kind">${c.kind === "css" ? "CSS" : "DOM"}</span><span class="sel" title="${c.selector}">${sel}</span><button data-discard="${key}" title="Discard this change">${ICONS.trash}</button></div>`;
|
|
1290
|
+
}
|
|
1291
|
+
html += `<div class="dropact"><button class="btn danger" id="dropDiscardAll" title="Discard all">${ICONS.trash} Discard all</button><button class="btn active" id="dropSave">${ICONS.save} Save to source</button></div>`;
|
|
1292
|
+
ui.savedrop.innerHTML = html;
|
|
1293
|
+
|
|
1294
|
+
// Discard single
|
|
1295
|
+
ui.savedrop.querySelectorAll("[data-discard]").forEach((btn) => {
|
|
1296
|
+
btn.addEventListener("click", (e) => {
|
|
1297
|
+
e.stopPropagation();
|
|
1298
|
+
revertChange(btn.dataset.discard);
|
|
1299
|
+
if (!changes.size) { ui.savedrop.classList.remove("open"); document.removeEventListener("click", onClickOutside, true); }
|
|
1300
|
+
else renderSaveDrop();
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
// Discard all
|
|
1304
|
+
ui.savedrop.querySelector("#dropDiscardAll").addEventListener("click", () => {
|
|
1305
|
+
for (const key of [...changes.keys()]) revertChange(key);
|
|
1306
|
+
ui.savedrop.classList.remove("open");
|
|
1307
|
+
document.removeEventListener("click", onClickOutside, true);
|
|
1308
|
+
});
|
|
1309
|
+
// Save
|
|
1310
|
+
ui.savedrop.querySelector("#dropSave").addEventListener("click", () => {
|
|
1311
|
+
doSave();
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function doSave() {
|
|
1316
|
+
deselect();
|
|
1317
|
+
const list = [...changes.values()].map((c) => c.kind === "css"
|
|
1318
|
+
? { kind: "css", selector: c.selector, before: c.before, after: c.after || "" }
|
|
1319
|
+
: {
|
|
1320
|
+
kind: "dom",
|
|
1321
|
+
selector: c.selector,
|
|
1322
|
+
before: c.before,
|
|
1323
|
+
after: c.removed ? "" : (c.after || (c.el && c.el.isConnected ? c.el.outerHTML : "")),
|
|
1324
|
+
beforeText: c.beforeText || "",
|
|
1325
|
+
afterText: c.afterText || "",
|
|
1326
|
+
addedHTML: c.addedHTML || "",
|
|
1327
|
+
added: !!c.added,
|
|
1328
|
+
}
|
|
1329
|
+
).filter((c) => c.after !== c.before && !(c.kind === "css" && !c.after));
|
|
1330
|
+
if (!list.length) { addMsg("sys", "All changes were reverted, nothing to save."); changes.clear(); updateCount(); saveLocalStateSoon(); return; }
|
|
1331
|
+
addMsg("user", `Save ${list.length} change(s) to source`);
|
|
1332
|
+
send({ type: "save", changes: list, url: location.href, dom: prunedDOM(6000) });
|
|
1333
|
+
ui.savedrop.classList.remove("open");
|
|
1334
|
+
document.removeEventListener("click", onClickOutside, true);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function onClickOutside(e) {
|
|
1338
|
+
if (!ui.savedrop.contains(e.target) && e.target !== ui.saveBtn) {
|
|
1339
|
+
ui.savedrop.classList.remove("open");
|
|
1340
|
+
document.removeEventListener("click", onClickOutside, true);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
ui.saveBtn.addEventListener("click", (e) => {
|
|
1345
|
+
e.stopPropagation();
|
|
1346
|
+
if (!changes.size) { addMsg("sys", "Nothing to save yet."); return; }
|
|
1347
|
+
if (ui.savedrop.classList.contains("open")) { ui.savedrop.classList.remove("open"); document.removeEventListener("click", onClickOutside, true); return; }
|
|
1348
|
+
renderSaveDrop();
|
|
1349
|
+
ui.savedrop.classList.add("open");
|
|
1350
|
+
document.addEventListener("click", onClickOutside, true);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
ui.flowBtn.addEventListener("click", () => {
|
|
1354
|
+
recordingFlow = !recordingFlow;
|
|
1355
|
+
ui.flowBtn.classList.toggle("rec", recordingFlow);
|
|
1356
|
+
ui.flowBtn.innerHTML = recordingFlow ? ICONS.stop : ICONS.record;
|
|
1357
|
+
ui.flowBtn.title = recordingFlow ? "Stop recording" : "Record userflow";
|
|
1358
|
+
ui.puck.classList.toggle("rec", recordingFlow);
|
|
1359
|
+
if (recordingFlow) { send({ type: "flowStart" }); }
|
|
1360
|
+
else send({ type: "flowStop" });
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
ui.automationBtn.addEventListener("click", () => {
|
|
1364
|
+
setAutomationMode(!automationMode);
|
|
1365
|
+
if (!automationMode) {
|
|
1366
|
+
// On disable: optionally send accumulated instructions
|
|
1367
|
+
automationInstructions = [];
|
|
1368
|
+
updateInstCount();
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
// ---- chat ---------------------------------------------------------------------
|
|
1373
|
+
function sendChat() {
|
|
1374
|
+
const text = ui.chatText.value.trim();
|
|
1375
|
+
if (!text) return;
|
|
1376
|
+
ui.chatText.value = "";
|
|
1377
|
+
const payload = {
|
|
1378
|
+
text,
|
|
1379
|
+
url: location.href, title: document.title,
|
|
1380
|
+
dom: prunedDOM(), selected: selected ? selected.outerHTML : null
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
if (automationMode) {
|
|
1384
|
+
// Automation mode: accumulate instructions locally, optionally send with flow
|
|
1385
|
+
automationInstructions.push(text);
|
|
1386
|
+
updateInstCount();
|
|
1387
|
+
addMsg("instruction", text);
|
|
1388
|
+
payload.type = "automation";
|
|
1389
|
+
payload.instructions = automationInstructions;
|
|
1390
|
+
saveLocalStateSoon();
|
|
1391
|
+
} else {
|
|
1392
|
+
addMsg("user", text);
|
|
1393
|
+
payload.type = "chat";
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (session) { payload.flowEvents = session.events; payload.flowId = session.id; session = null; removeFlowMsg(); saveLocalStateSoon(); }
|
|
1397
|
+
if (pendingScreenshots.length) { payload.screenshots = pendingScreenshots.map((s) => s.data); clearAllScreenshots(); }
|
|
1398
|
+
send(payload);
|
|
1399
|
+
}
|
|
1400
|
+
ui.chatSend.addEventListener("click", sendChat);
|
|
1401
|
+
ui.chatText.addEventListener("keydown", (e) => {
|
|
1402
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendChat(); }
|
|
1403
|
+
});
|
|
1404
|
+
document.addEventListener("keydown", (e) => {
|
|
1405
|
+
if (e.key === "Escape" && screenshotMode) { setScreenshotMode(false); shotStartX = undefined; }
|
|
1406
|
+
}, true);
|
|
1407
|
+
|
|
1408
|
+
// ---- playback --------------------------------------------------------------------
|
|
1409
|
+
function showFlowInline() {
|
|
1410
|
+
if (!session) return;
|
|
1411
|
+
removeFlowMsg();
|
|
1412
|
+
flowMsg = document.createElement("div");
|
|
1413
|
+
flowMsg.className = "msg-flow";
|
|
1414
|
+
flowMsg.innerHTML =
|
|
1415
|
+
`<img id="flowFrame" alt="Frame"><div class="scrub">` +
|
|
1416
|
+
`<button class="pbtn" id="flowPlay">${ICONS.play}</button>` +
|
|
1417
|
+
`<input type="range" id="flowScrub" min="0" max="${Math.max(0, session.shots - 1)}" value="0">` +
|
|
1418
|
+
`<span class="evt" id="flowFrameNo">1/${session.shots}</span></div>` +
|
|
1419
|
+
`<div class="evt" id="flowEvtLine"></div>` +
|
|
1420
|
+
`<div style="display:flex;justify-content:space-between;align-items:center">` +
|
|
1421
|
+
`<span style="font-size:11px;color:#8b909d">${session.events.filter((e) => e.kind !== "shot").length} interactions recorded</span>` +
|
|
1422
|
+
`<button class="btn danger" id="flowDiscard" title="Discard recording">${ICONS.trash}</button></div>`;
|
|
1423
|
+
ui.msgs.appendChild(flowMsg);
|
|
1424
|
+
ui.msgs.scrollTop = ui.msgs.scrollHeight;
|
|
1425
|
+
|
|
1426
|
+
const frame = flowMsg.querySelector("#flowFrame");
|
|
1427
|
+
const scrub = flowMsg.querySelector("#flowScrub");
|
|
1428
|
+
const frameNo = flowMsg.querySelector("#flowFrameNo");
|
|
1429
|
+
const evtLine = flowMsg.querySelector("#flowEvtLine");
|
|
1430
|
+
const playBtn = flowMsg.querySelector("#flowPlay");
|
|
1431
|
+
const discard = flowMsg.querySelector("#flowDiscard");
|
|
1432
|
+
let playTimer = null;
|
|
1433
|
+
|
|
1434
|
+
function showFrame(n) {
|
|
1435
|
+
frame.src = `${session.base}shot-${n}.jpg`;
|
|
1436
|
+
frameNo.textContent = `${n + 1}/${session.shots}`;
|
|
1437
|
+
const shot = session.events.find((e) => e.kind === "shot" && e.n === n);
|
|
1438
|
+
if (shot) {
|
|
1439
|
+
const near = session.events
|
|
1440
|
+
.filter((e) => e.kind !== "shot" && Math.abs(e.t - shot.t) < 1600)
|
|
1441
|
+
.map((e) => e.kind === "click" ? `click "${(e.text || e.selector || "").slice(0, 40)}"` : e.kind)
|
|
1442
|
+
.join(", ");
|
|
1443
|
+
evtLine.textContent = near || " ";
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
showFrame(0);
|
|
1447
|
+
|
|
1448
|
+
scrub.addEventListener("input", () => showFrame(+scrub.value));
|
|
1449
|
+
playBtn.addEventListener("click", () => {
|
|
1450
|
+
if (playTimer) { clearInterval(playTimer); playTimer = null; playBtn.innerHTML = ICONS.play; return; }
|
|
1451
|
+
playBtn.innerHTML = ICONS.stop;
|
|
1452
|
+
playTimer = setInterval(() => {
|
|
1453
|
+
let n = +scrub.value + 1;
|
|
1454
|
+
if (n >= session.shots) { clearInterval(playTimer); playTimer = null; playBtn.innerHTML = ICONS.play; return; }
|
|
1455
|
+
scrub.value = String(n);
|
|
1456
|
+
showFrame(n);
|
|
1457
|
+
}, 700);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
discard.addEventListener("click", () => {
|
|
1461
|
+
if (!session) return;
|
|
1462
|
+
if (playTimer) clearInterval(playTimer);
|
|
1463
|
+
send({ type: "flowDiscard", id: session.id });
|
|
1464
|
+
session = null;
|
|
1465
|
+
removeFlowMsg();
|
|
1466
|
+
addMsg("sys", "Recording discarded.");
|
|
1467
|
+
saveLocalStateSoon();
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function removeFlowMsg() {
|
|
1472
|
+
if (flowMsg) { flowMsg.remove(); flowMsg = null; }
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// keep highlights aligned on resize
|
|
1476
|
+
window.addEventListener("resize", () => { if (selected) positionHl(ui.selHl, selected); });
|
|
1477
|
+
window.addEventListener("beforeunload", saveLocalState);
|
|
1478
|
+
|
|
1479
|
+
connect();
|
|
1480
|
+
addMsg("sys", "Connected page. Toggle Edit mode to click elements, or just ask in chat.");
|
|
1481
|
+
} // init
|
|
1482
|
+
})();
|