ghostfill 0.1.2 → 0.2.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.
Potentially problematic release.
This version of ghostfill might be problematic. Click here for more details.
- package/README.md +100 -10
- package/dist/chunk-VMCU3BNJ.mjs +275 -0
- package/dist/chunk-VMCU3BNJ.mjs.map +1 -0
- package/dist/index.d.mts +6 -41
- package/dist/index.d.ts +6 -41
- package/dist/index.js +476 -225
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -364
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +12 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +165 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +15 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-B3DGbZyx.d.mts +83 -0
- package/dist/types-B3DGbZyx.d.ts +83 -0
- package/package.json +6 -1
package/dist/index.mjs
CHANGED
|
@@ -1,261 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
// src/detector.ts
|
|
9
|
-
var INPUT_SELECTORS = [
|
|
10
|
-
"input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=image])",
|
|
11
|
-
"textarea",
|
|
12
|
-
"select",
|
|
13
|
-
// Custom dropdown triggers (Headless UI Listbox, Radix, etc.)
|
|
14
|
-
"button[role=combobox]",
|
|
15
|
-
"[role=combobox]:not(input)",
|
|
16
|
-
"button[aria-haspopup=listbox]"
|
|
17
|
-
].join(", ");
|
|
18
|
-
function findLabel(el) {
|
|
19
|
-
if (el.id) {
|
|
20
|
-
const label = document.querySelector(
|
|
21
|
-
`label[for="${CSS.escape(el.id)}"]`
|
|
22
|
-
);
|
|
23
|
-
if (label?.textContent?.trim()) return label.textContent.trim();
|
|
24
|
-
}
|
|
25
|
-
const parentLabel = el.closest("label");
|
|
26
|
-
if (parentLabel) {
|
|
27
|
-
const clone = parentLabel.cloneNode(true);
|
|
28
|
-
clone.querySelectorAll("input, textarea, select").forEach((c) => c.remove());
|
|
29
|
-
const text = clone.textContent?.trim();
|
|
30
|
-
if (text) return text;
|
|
31
|
-
}
|
|
32
|
-
const ariaLabel = el.getAttribute("aria-label");
|
|
33
|
-
if (ariaLabel?.trim()) return ariaLabel.trim();
|
|
34
|
-
const labelledBy = el.getAttribute("aria-labelledby");
|
|
35
|
-
if (labelledBy) {
|
|
36
|
-
const parts = labelledBy.split(/\s+/).map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
37
|
-
if (parts.length) return parts.join(" ");
|
|
38
|
-
}
|
|
39
|
-
if ("placeholder" in el) {
|
|
40
|
-
const ph = el.placeholder;
|
|
41
|
-
if (ph?.trim()) return ph.trim();
|
|
42
|
-
}
|
|
43
|
-
const title = el.getAttribute("title");
|
|
44
|
-
if (title?.trim()) return title.trim();
|
|
45
|
-
const prev = el.previousElementSibling;
|
|
46
|
-
if (prev && !["INPUT", "TEXTAREA", "SELECT"].includes(prev.tagName)) {
|
|
47
|
-
const prevText = prev.textContent?.trim();
|
|
48
|
-
if (prevText && prevText.length < 60) return prevText;
|
|
49
|
-
}
|
|
50
|
-
const parent = el.parentElement;
|
|
51
|
-
if (parent) {
|
|
52
|
-
for (const child of Array.from(parent.children)) {
|
|
53
|
-
if (child === el) break;
|
|
54
|
-
if (["LABEL", "SPAN", "P", "H1", "H2", "H3", "H4", "H5", "H6", "LEGEND", "DIV"].includes(child.tagName)) {
|
|
55
|
-
const text = child.textContent?.trim();
|
|
56
|
-
if (text && text.length < 60 && !child.querySelector("input, textarea, select")) {
|
|
57
|
-
return text;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (el.id) return el.id.replace(/[_\-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
|
|
63
|
-
const name = el.getAttribute("name");
|
|
64
|
-
if (name) return name.replace(/[_\-[\]]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").trim();
|
|
65
|
-
return "unknown";
|
|
66
|
-
}
|
|
67
|
-
function detectFields(container) {
|
|
68
|
-
const elements = container.querySelectorAll(INPUT_SELECTORS);
|
|
69
|
-
const fields = [];
|
|
70
|
-
elements.forEach((el) => {
|
|
71
|
-
if (el.disabled) return;
|
|
72
|
-
if (el.readOnly) return;
|
|
73
|
-
if (el.offsetParent === null && el.type !== "hidden") return;
|
|
74
|
-
const isCustomDropdown = el.getAttribute("role") === "combobox" && !(el instanceof HTMLInputElement) || el.getAttribute("aria-haspopup") === "listbox";
|
|
75
|
-
if (isCustomDropdown) {
|
|
76
|
-
const listboxId = el.getAttribute("aria-controls");
|
|
77
|
-
let listbox = listboxId ? document.getElementById(listboxId) : null;
|
|
78
|
-
if (!listbox) {
|
|
79
|
-
listbox = el.parentElement?.querySelector("[role=listbox]") || null;
|
|
80
|
-
}
|
|
81
|
-
const options = [];
|
|
82
|
-
if (listbox) {
|
|
83
|
-
listbox.querySelectorAll("[role=option]").forEach((opt) => {
|
|
84
|
-
const text = opt.textContent?.trim();
|
|
85
|
-
if (text) options.push(text);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
const buttonText = el.textContent?.trim() || "";
|
|
89
|
-
const looksLikePlaceholder = buttonText.toLowerCase().startsWith("select") || buttonText === "";
|
|
90
|
-
const field2 = {
|
|
91
|
-
element: el,
|
|
92
|
-
type: "select",
|
|
93
|
-
name: el.id || el.getAttribute("name") || "",
|
|
94
|
-
label: findLabel(el),
|
|
95
|
-
required: el.getAttribute("aria-required") === "true",
|
|
96
|
-
currentValue: looksLikePlaceholder ? "" : buttonText,
|
|
97
|
-
options: options.length > 0 ? options : void 0
|
|
98
|
-
};
|
|
99
|
-
fields.push(field2);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
const field = {
|
|
103
|
-
element: el,
|
|
104
|
-
type: el instanceof HTMLSelectElement ? "select" : el.type || "text",
|
|
105
|
-
name: el.name || el.id || "",
|
|
106
|
-
label: findLabel(el),
|
|
107
|
-
required: el.required || el.getAttribute("aria-required") === "true",
|
|
108
|
-
currentValue: el.value
|
|
109
|
-
};
|
|
110
|
-
if (el instanceof HTMLSelectElement) {
|
|
111
|
-
field.options = Array.from(el.options).filter((opt) => opt.value && !opt.disabled).map((opt) => opt.textContent?.trim() || opt.value);
|
|
112
|
-
}
|
|
113
|
-
if ("min" in el && el.min) {
|
|
114
|
-
field.min = el.min;
|
|
115
|
-
}
|
|
116
|
-
if ("max" in el && el.max) {
|
|
117
|
-
field.max = el.max;
|
|
118
|
-
}
|
|
119
|
-
if ("pattern" in el && el.pattern) {
|
|
120
|
-
field.pattern = el.pattern;
|
|
121
|
-
}
|
|
122
|
-
fields.push(field);
|
|
123
|
-
});
|
|
124
|
-
return fields;
|
|
125
|
-
}
|
|
126
|
-
function describeFields(fields) {
|
|
127
|
-
return fields.map((f, i) => {
|
|
128
|
-
let desc = `[${i}] "${f.label}" (type: ${f.type}`;
|
|
129
|
-
if (f.required) desc += ", required";
|
|
130
|
-
if (f.options?.length) desc += `, options: [${f.options.join(", ")}]`;
|
|
131
|
-
if (f.min) desc += `, min: ${f.min}`;
|
|
132
|
-
if (f.max) desc += `, max: ${f.max}`;
|
|
133
|
-
if (f.pattern) desc += `, pattern: ${f.pattern}`;
|
|
134
|
-
if (f.currentValue) desc += `, current: "${f.currentValue}"`;
|
|
135
|
-
desc += ")";
|
|
136
|
-
return desc;
|
|
137
|
-
}).join("\n");
|
|
138
|
-
}
|
|
139
|
-
function extractBlockContext(container) {
|
|
140
|
-
const parts = [];
|
|
141
|
-
const pageTitle = document.title;
|
|
142
|
-
if (pageTitle) parts.push(`Page: ${pageTitle}`);
|
|
143
|
-
const headings = container.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
144
|
-
headings.forEach((h) => {
|
|
145
|
-
const text = h.textContent?.trim();
|
|
146
|
-
if (text) parts.push(`Heading: ${text}`);
|
|
147
|
-
});
|
|
148
|
-
if (headings.length === 0) {
|
|
149
|
-
let prev = container;
|
|
150
|
-
while (prev) {
|
|
151
|
-
prev = prev.previousElementSibling;
|
|
152
|
-
if (prev && /^H[1-6]$/.test(prev.tagName)) {
|
|
153
|
-
const text = prev.textContent?.trim();
|
|
154
|
-
if (text) parts.push(`Section: ${text}`);
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const parent = container.closest("section, article, form, div[class]");
|
|
159
|
-
if (parent) {
|
|
160
|
-
const parentH = parent.querySelector("h1, h2, h3, h4, h5, h6");
|
|
161
|
-
if (parentH?.textContent?.trim()) {
|
|
162
|
-
parts.push(`Section: ${parentH.textContent.trim()}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
const labels = container.querySelectorAll("label, legend, .label, p, span");
|
|
167
|
-
const seen = /* @__PURE__ */ new Set();
|
|
168
|
-
labels.forEach((el) => {
|
|
169
|
-
const text = el.textContent?.trim();
|
|
170
|
-
if (text && text.length > 2 && text.length < 100 && !seen.has(text)) {
|
|
171
|
-
seen.add(text);
|
|
172
|
-
parts.push(text);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
return parts.slice(0, 20).join("\n");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// src/ai.ts
|
|
179
|
-
var SYSTEM_PROMPT = `You are a form-filling assistant. Given a list of form fields, page context, and an optional user prompt, generate realistic fake data to fill ALL fields.
|
|
180
|
-
|
|
181
|
-
Rules:
|
|
182
|
-
- Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys
|
|
183
|
-
- You MUST fill EVERY field \u2014 do not skip any
|
|
184
|
-
- Match the field type (email \u2192 valid email, phone \u2192 valid phone with country code, date \u2192 YYYY-MM-DD, datetime-local \u2192 YYYY-MM-DDTHH:MM, etc.)
|
|
185
|
-
- For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written
|
|
186
|
-
- For checkboxes: add a "checked" boolean (true or false)
|
|
187
|
-
- For radio buttons: only fill one per group, use the option value
|
|
188
|
-
- Respect min/max constraints and patterns
|
|
189
|
-
- Generate contextually coherent data (same person's name, matching city/state/zip, etc.)
|
|
190
|
-
- Use the page context to infer what kind of data makes sense (e.g. a "Create Project" form \u2192 project-related data)
|
|
191
|
-
- If no user prompt is given, infer appropriate data from the field labels and page context
|
|
192
|
-
- Do NOT wrap the JSON in markdown code blocks \u2014 return raw JSON only`;
|
|
193
|
-
async function generateFillData(fields, userPrompt, settings, systemPrompt, blockContext) {
|
|
194
|
-
const fieldDescription = describeFields(fields);
|
|
195
|
-
const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
|
|
196
|
-
let userContent = `Form fields:
|
|
197
|
-
${fieldDescription}`;
|
|
198
|
-
if (blockContext) {
|
|
199
|
-
userContent += `
|
|
200
|
-
|
|
201
|
-
Page context:
|
|
202
|
-
${blockContext}`;
|
|
203
|
-
}
|
|
204
|
-
if (userPrompt) {
|
|
205
|
-
userContent += `
|
|
206
|
-
|
|
207
|
-
User instructions: ${userPrompt}`;
|
|
208
|
-
} else {
|
|
209
|
-
userContent += `
|
|
210
|
-
|
|
211
|
-
No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
|
|
212
|
-
}
|
|
213
|
-
const messages = [
|
|
214
|
-
{
|
|
215
|
-
role: "system",
|
|
216
|
-
content: systemPrompt ? `${systemPrompt}
|
|
217
|
-
|
|
218
|
-
${SYSTEM_PROMPT}` : SYSTEM_PROMPT
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
role: "user",
|
|
222
|
-
content: userContent
|
|
223
|
-
}
|
|
224
|
-
];
|
|
225
|
-
const body = {
|
|
226
|
-
model: provider.model,
|
|
227
|
-
messages,
|
|
228
|
-
temperature: 0.7
|
|
229
|
-
};
|
|
230
|
-
if (settings.provider === "openai") {
|
|
231
|
-
body.response_format = { type: "json_object" };
|
|
232
|
-
}
|
|
233
|
-
const response = await fetch(`${provider.baseURL}/chat/completions`, {
|
|
234
|
-
method: "POST",
|
|
235
|
-
headers: {
|
|
236
|
-
"Content-Type": "application/json",
|
|
237
|
-
Authorization: `Bearer ${settings.apiKey}`
|
|
238
|
-
},
|
|
239
|
-
body: JSON.stringify(body)
|
|
240
|
-
});
|
|
241
|
-
if (!response.ok) {
|
|
242
|
-
const error = await response.text();
|
|
243
|
-
throw new Error(`API error (${response.status}): ${error}`);
|
|
244
|
-
}
|
|
245
|
-
const data = await response.json();
|
|
246
|
-
const content = data.choices?.[0]?.message?.content;
|
|
247
|
-
if (!content) {
|
|
248
|
-
throw new Error("No content in API response");
|
|
249
|
-
}
|
|
250
|
-
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
|
|
251
|
-
const jsonStr = jsonMatch[1].trim();
|
|
252
|
-
const parsed = JSON.parse(jsonStr);
|
|
253
|
-
const arr = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
|
|
254
|
-
if (!Array.isArray(arr)) {
|
|
255
|
-
throw new Error("AI response is not an array of field fills");
|
|
256
|
-
}
|
|
257
|
-
return arr;
|
|
258
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
PROVIDERS,
|
|
3
|
+
describeFields,
|
|
4
|
+
detectFields,
|
|
5
|
+
generateFillData
|
|
6
|
+
} from "./chunk-VMCU3BNJ.mjs";
|
|
259
7
|
|
|
260
8
|
// src/faker.ts
|
|
261
9
|
var FIRST_NAMES = ["James", "Sarah", "Michael", "Emma", "Robert", "Olivia", "David", "Sophia", "Daniel", "Isabella", "Ahmed", "Fatima", "Carlos", "Yuki", "Priya"];
|
|
@@ -343,7 +91,7 @@ function generateFakeData(fields) {
|
|
|
343
91
|
const context = { firstName, lastName, email, company };
|
|
344
92
|
return fields.map((field, index) => {
|
|
345
93
|
if (field.type === "checkbox") {
|
|
346
|
-
return { index, value: "true", checked:
|
|
94
|
+
return { index, value: "true", checked: true };
|
|
347
95
|
}
|
|
348
96
|
const value = generateForField(field, context);
|
|
349
97
|
return { index, value };
|
|
@@ -447,7 +195,7 @@ async function fillFields(fields, fillData) {
|
|
|
447
195
|
`input[type="radio"][name="${CSS.escape(radio.name)}"]`
|
|
448
196
|
);
|
|
449
197
|
for (const r of group) {
|
|
450
|
-
if (r.value === item.value ||
|
|
198
|
+
if (r.value === item.value || findLabel(r) === item.value) {
|
|
451
199
|
const nativeCheckedSetter = Object.getOwnPropertyDescriptor(
|
|
452
200
|
HTMLInputElement.prototype,
|
|
453
201
|
"checked"
|
|
@@ -516,7 +264,7 @@ async function fillFields(fields, fillData) {
|
|
|
516
264
|
}
|
|
517
265
|
return { filled, errors };
|
|
518
266
|
}
|
|
519
|
-
function
|
|
267
|
+
function findLabel(el) {
|
|
520
268
|
if (el.id) {
|
|
521
269
|
const label = document.querySelector(
|
|
522
270
|
`label[for="${CSS.escape(el.id)}"]`
|
|
@@ -630,16 +378,53 @@ function startSelection(onSelect, onCancel, ghostfillRoot, highlightColor = "#63
|
|
|
630
378
|
var STORAGE_KEY = "ghostfill_settings";
|
|
631
379
|
var POS_KEY = "ghostfill_pos";
|
|
632
380
|
var FAB_POS_KEY = "ghostfill_fab_pos";
|
|
633
|
-
function
|
|
381
|
+
function isProvider(value) {
|
|
382
|
+
return value === "openai" || value === "xai" || value === "moonshot";
|
|
383
|
+
}
|
|
384
|
+
function defaultSettings(provider) {
|
|
385
|
+
return {
|
|
386
|
+
apiKey: "",
|
|
387
|
+
provider,
|
|
388
|
+
highlightColor: "#6366f1",
|
|
389
|
+
theme: "dark",
|
|
390
|
+
useAI: false,
|
|
391
|
+
presets: [],
|
|
392
|
+
activePresetId: null
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function sanitizePresets(value) {
|
|
396
|
+
if (!Array.isArray(value)) return [];
|
|
397
|
+
return value.flatMap((item) => {
|
|
398
|
+
if (typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.name === "string" && typeof item.prompt === "string") {
|
|
399
|
+
return [item];
|
|
400
|
+
}
|
|
401
|
+
return [];
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function loadSettings(provider) {
|
|
634
405
|
try {
|
|
635
406
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
636
|
-
if (raw)
|
|
407
|
+
if (raw) {
|
|
408
|
+
const parsed = JSON.parse(raw);
|
|
409
|
+
return {
|
|
410
|
+
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : "",
|
|
411
|
+
provider: isProvider(parsed.provider) ? parsed.provider : provider,
|
|
412
|
+
highlightColor: typeof parsed.highlightColor === "string" ? parsed.highlightColor : "#6366f1",
|
|
413
|
+
theme: parsed.theme === "light" ? "light" : "dark",
|
|
414
|
+
useAI: parsed.useAI === true,
|
|
415
|
+
presets: sanitizePresets(parsed.presets),
|
|
416
|
+
activePresetId: typeof parsed.activePresetId === "string" ? parsed.activePresetId : null
|
|
417
|
+
};
|
|
418
|
+
}
|
|
637
419
|
} catch {
|
|
638
420
|
}
|
|
639
|
-
return
|
|
421
|
+
return defaultSettings(provider);
|
|
640
422
|
}
|
|
641
423
|
function saveSettings(s) {
|
|
642
|
-
|
|
424
|
+
try {
|
|
425
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
643
428
|
}
|
|
644
429
|
function loadPosition() {
|
|
645
430
|
try {
|
|
@@ -650,13 +435,16 @@ function loadPosition() {
|
|
|
650
435
|
return null;
|
|
651
436
|
}
|
|
652
437
|
function savePosition(x, y) {
|
|
653
|
-
|
|
438
|
+
try {
|
|
439
|
+
localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
654
442
|
}
|
|
655
443
|
function cleanError(err) {
|
|
656
444
|
const raw = err instanceof Error ? err.message : String(err);
|
|
657
445
|
const match = raw.match(/"message"\s*:\s*"([^"]+)"/);
|
|
658
446
|
if (match) return match[1];
|
|
659
|
-
const stripped = raw.replace(/^
|
|
447
|
+
const stripped = raw.replace(/^AI API error \(\d+\):\s*/, "");
|
|
660
448
|
try {
|
|
661
449
|
const parsed = JSON.parse(stripped);
|
|
662
450
|
if (parsed?.error?.message) return parsed.error.message;
|
|
@@ -721,7 +509,7 @@ var CSS2 = `
|
|
|
721
509
|
align-items: center;
|
|
722
510
|
gap: 2px;
|
|
723
511
|
background: #18181b;
|
|
724
|
-
border-radius:
|
|
512
|
+
border-radius: 22px;
|
|
725
513
|
padding: 5px 6px;
|
|
726
514
|
box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06);
|
|
727
515
|
user-select: none;
|
|
@@ -756,20 +544,39 @@ var CSS2 = `
|
|
|
756
544
|
|
|
757
545
|
.gf-fab {
|
|
758
546
|
position: fixed; z-index: 2147483646;
|
|
759
|
-
width:
|
|
760
|
-
background: #18181b; color: #
|
|
547
|
+
width: 48px; height: 48px; border-radius: 50%; border: none;
|
|
548
|
+
background: #18181b; color: #e4e4e7; cursor: grab;
|
|
761
549
|
display: none; align-items: center; justify-content: center;
|
|
762
550
|
pointer-events: auto;
|
|
763
|
-
box-shadow: 0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(
|
|
764
|
-
transition:
|
|
551
|
+
box-shadow: 0 0 20px rgba(99,102,241,0.3), 0 0 40px rgba(99,102,241,0.1), 0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(99,102,241,0.15);
|
|
552
|
+
transition: color 0.2s, box-shadow 0.3s;
|
|
553
|
+
}
|
|
554
|
+
.gf-fab > svg {
|
|
555
|
+
filter: drop-shadow(0 0 4px rgba(99,102,241,0.5));
|
|
556
|
+
transition: transform 0.2s;
|
|
557
|
+
}
|
|
558
|
+
.gf-fab:hover {
|
|
559
|
+
color: #fff;
|
|
560
|
+
box-shadow: 0 0 30px rgba(99,102,241,0.5), 0 0 60px rgba(99,102,241,0.2), 0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(99,102,241,0.3);
|
|
561
|
+
}
|
|
562
|
+
.gf-fab:hover > svg {
|
|
563
|
+
animation: gf-ghost-wobble 1.5s ease-in-out infinite;
|
|
765
564
|
}
|
|
766
|
-
.gf-fab:hover { color: #a78bfa; }
|
|
767
|
-
.gf-fab:hover > svg { animation: gf-float 1.2s ease-in-out infinite; }
|
|
768
565
|
.gf-fab.visible { display: flex; }
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
566
|
+
|
|
567
|
+
@keyframes gf-ghost-wobble {
|
|
568
|
+
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
569
|
+
25% { transform: translate(1px, -2px) rotate(4deg); }
|
|
570
|
+
50% { transform: translate(0, -3px) rotate(-1deg); }
|
|
571
|
+
75% { transform: translate(-1px, -1px) rotate(-4deg); }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Success flash on filled block */
|
|
575
|
+
@keyframes gf-fill-success {
|
|
576
|
+
0% { box-shadow: 0 0 0 0 rgba(99,102,241,0.4); }
|
|
577
|
+
30% { box-shadow: 0 0 0 6px rgba(99,102,241,0.2); }
|
|
578
|
+
60% { box-shadow: 0 0 0 12px rgba(52,211,153,0.15); }
|
|
579
|
+
100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); }
|
|
773
580
|
}
|
|
774
581
|
|
|
775
582
|
.gf-popover {
|
|
@@ -777,7 +584,7 @@ var CSS2 = `
|
|
|
777
584
|
background: #1a1a1a; border-radius: 16px; pointer-events: auto;
|
|
778
585
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
|
|
779
586
|
display: none; flex-direction: column; overflow: hidden;
|
|
780
|
-
|
|
587
|
+
width: 280px;
|
|
781
588
|
}
|
|
782
589
|
.gf-popover.open { display: flex; }
|
|
783
590
|
|
|
@@ -852,7 +659,7 @@ var CSS2 = `
|
|
|
852
659
|
display: flex; align-items: center; justify-content: center; gap: 5px;
|
|
853
660
|
}
|
|
854
661
|
.gf-save-btn:hover, .gf-fill-btn:hover { background: #4f46e5; }
|
|
855
|
-
.gf-fill-btn:disabled { background:
|
|
662
|
+
.gf-fill-btn:disabled { background: #6366f1; opacity: 0.5; cursor: not-allowed; }
|
|
856
663
|
|
|
857
664
|
.gf-pop-body::-webkit-scrollbar { width: 6px; }
|
|
858
665
|
.gf-pop-body::-webkit-scrollbar-track { background: transparent; }
|
|
@@ -978,26 +785,42 @@ var CSS2 = `
|
|
|
978
785
|
}
|
|
979
786
|
.gf-preset-chip.add:hover { color: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.3); }
|
|
980
787
|
|
|
981
|
-
/* Preset
|
|
982
|
-
.gf-preset-list { display: flex; flex-
|
|
983
|
-
.gf-preset-
|
|
984
|
-
display: flex; align-items: center;
|
|
985
|
-
padding: 4px
|
|
788
|
+
/* Preset pills in settings */
|
|
789
|
+
.gf-preset-list { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
790
|
+
.gf-preset-pill {
|
|
791
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
792
|
+
padding: 4px 10px; border-radius: 20px;
|
|
793
|
+
font-size: 12px; font-weight: 500; cursor: pointer;
|
|
794
|
+
border: 1px solid; transition: all 0.15s;
|
|
795
|
+
}
|
|
796
|
+
.gf-preset-pill .gf-pp-name {
|
|
797
|
+
cursor: pointer; transition: opacity 0.15s;
|
|
798
|
+
}
|
|
799
|
+
.gf-preset-pill .gf-pp-name:hover { opacity: 0.7; }
|
|
800
|
+
.gf-preset-pill .gf-pp-x {
|
|
801
|
+
background: none; border: none; cursor: pointer;
|
|
802
|
+
font-size: 13px; line-height: 1; opacity: 0.4; transition: opacity 0.15s, color 0.15s;
|
|
803
|
+
padding: 0; margin-left: 2px; font-family: inherit;
|
|
804
|
+
}
|
|
805
|
+
.gf-preset-pill .gf-pp-x:hover { opacity: 1; color: #f87171; }
|
|
806
|
+
|
|
807
|
+
/* Preset edit overlay \u2014 takes over the entire settings panel */
|
|
808
|
+
.gf-preset-overlay {
|
|
809
|
+
position: absolute; inset: 0;
|
|
810
|
+
background: #1a1a1a; border-radius: 16px;
|
|
811
|
+
display: none; flex-direction: column;
|
|
812
|
+
z-index: 5;
|
|
813
|
+
}
|
|
814
|
+
.gf-preset-overlay[style*="display: flex"], .gf-preset-overlay[style*="display:flex"] {
|
|
815
|
+
display: flex;
|
|
986
816
|
}
|
|
987
|
-
.gf-preset-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
|
|
817
|
+
.gf-preset-overlay-body {
|
|
818
|
+
flex: 1; display: flex; flex-direction: column;
|
|
819
|
+
padding: 0 16px 16px; gap: 10px; overflow-y: auto;
|
|
991
820
|
}
|
|
992
|
-
.gf-preset-
|
|
993
|
-
|
|
994
|
-
/* Preset add form */
|
|
995
|
-
.gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
|
|
996
|
-
.gf-preset-form-row { display: flex; gap: 4px; }
|
|
997
|
-
.gf-preset-form-row .gf-input { flex: 1; }
|
|
998
|
-
.gf-preset-form-actions { display: flex; gap: 4px; justify-content: flex-end; }
|
|
821
|
+
.gf-preset-form-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
|
999
822
|
.gf-preset-form-btn {
|
|
1000
|
-
padding:
|
|
823
|
+
padding: 6px 14px; border: none; border-radius: 8px; font-size: 12px; font-weight: 500;
|
|
1001
824
|
cursor: pointer; font-family: inherit; transition: background 0.15s;
|
|
1002
825
|
}
|
|
1003
826
|
.gf-preset-form-btn.save { background: #6366f1; color: white; }
|
|
@@ -1005,22 +828,50 @@ var CSS2 = `
|
|
|
1005
828
|
.gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
|
|
1006
829
|
.gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
|
|
1007
830
|
|
|
831
|
+
/* Cycle dots (vertical indicator like Agentation) */
|
|
832
|
+
.gf-cycle-dots {
|
|
833
|
+
display: flex; flex-direction: column; gap: 2px; margin-left: 4px;
|
|
834
|
+
}
|
|
835
|
+
.gf-cycle-dot {
|
|
836
|
+
width: 3px; height: 3px; border-radius: 50%;
|
|
837
|
+
background: rgba(255,255,255,0.2); transition: background 0.2s, transform 0.2s;
|
|
838
|
+
transform: scale(0.67);
|
|
839
|
+
}
|
|
840
|
+
.gf-cycle-dot.active { background: #fff; transform: scale(1); }
|
|
841
|
+
|
|
1008
842
|
/* Help badge */
|
|
1009
843
|
.gf-help {
|
|
844
|
+
position: relative;
|
|
1010
845
|
display: inline-flex; align-items: center; justify-content: center;
|
|
1011
846
|
width: 14px; height: 14px; border-radius: 50%;
|
|
1012
847
|
background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
|
|
1013
848
|
cursor: help; flex-shrink: 0;
|
|
1014
849
|
}
|
|
850
|
+
.gf-help-tip {
|
|
851
|
+
display: none; position: absolute; bottom: calc(100% + 6px); left: 50%;
|
|
852
|
+
transform: translateX(-50%); padding: 6px 10px;
|
|
853
|
+
background: #383838; color: rgba(255,255,255,0.7);
|
|
854
|
+
font-size: 11px; font-weight: 400; line-height: 1.4;
|
|
855
|
+
border-radius: 8px; white-space: normal; width: 180px; text-align: left;
|
|
856
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 100;
|
|
857
|
+
}
|
|
858
|
+
.gf-help-tip.show { display: block; }
|
|
859
|
+
|
|
860
|
+
.gf-note {
|
|
861
|
+
font-size: 11px;
|
|
862
|
+
color: rgba(255,255,255,0.5);
|
|
863
|
+
line-height: 1.5;
|
|
864
|
+
}
|
|
1015
865
|
`;
|
|
1016
866
|
function createOverlay2(options) {
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
867
|
+
const aiConfig = options.ai || null;
|
|
868
|
+
const saved = loadSettings(aiConfig?.provider || "openai");
|
|
869
|
+
if (options.apiKey) {
|
|
870
|
+
console.warn(
|
|
871
|
+
"[ghostfill] Browser API keys are ignored. Configure init({ ai: ... }) and keep provider keys on your backend."
|
|
872
|
+
);
|
|
1021
873
|
}
|
|
1022
|
-
|
|
1023
|
-
if (options.baseURL && !saved.baseURL) saved.baseURL = options.baseURL;
|
|
874
|
+
const backendLabel = aiConfig ? aiConfig.requestFillData ? "Custom secure handler" : aiConfig.endpoint || "/api/ghostfill" : "Configure init({ ai: ... }) to enable AI.";
|
|
1024
875
|
const host = document.createElement("div");
|
|
1025
876
|
host.id = "ghostfill-root";
|
|
1026
877
|
host.style.cssText = "display:contents;";
|
|
@@ -1070,7 +921,7 @@ function createOverlay2(options) {
|
|
|
1070
921
|
dotWarn.className = "gf-dot-warn";
|
|
1071
922
|
btnSettings.style.position = "relative";
|
|
1072
923
|
btnSettings.appendChild(dotWarn);
|
|
1073
|
-
|
|
924
|
+
dotWarn.style.display = "none";
|
|
1074
925
|
const divider1 = document.createElement("span");
|
|
1075
926
|
divider1.className = "gf-divider";
|
|
1076
927
|
const divider2 = document.createElement("span");
|
|
@@ -1177,11 +1028,12 @@ function createOverlay2(options) {
|
|
|
1177
1028
|
];
|
|
1178
1029
|
const settingsPop = document.createElement("div");
|
|
1179
1030
|
settingsPop.className = "gf-popover";
|
|
1031
|
+
settingsPop.style.position = "fixed";
|
|
1180
1032
|
settingsPop.innerHTML = `
|
|
1181
1033
|
<div class="gf-pop-header">
|
|
1182
1034
|
<h3><span class="gf-slash">/</span>ghostfill</h3>
|
|
1183
1035
|
<div class="gf-header-right">
|
|
1184
|
-
<span class="gf-version">v0.1.
|
|
1036
|
+
<span class="gf-version">v0.1.3</span>
|
|
1185
1037
|
<button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
|
|
1186
1038
|
${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
|
|
1187
1039
|
</button>
|
|
@@ -1208,10 +1060,15 @@ function createOverlay2(options) {
|
|
|
1208
1060
|
<div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
|
|
1209
1061
|
<div style="display:flex;align-items:center;gap:4px">
|
|
1210
1062
|
<label class="gf-label" style="margin:0">Provider</label>
|
|
1211
|
-
<span class="gf-help" id="gf-s-help"
|
|
1063
|
+
<span class="gf-help" id="gf-s-help">?<span class="gf-help-tip" id="gf-s-help-tip"></span></span>
|
|
1212
1064
|
</div>
|
|
1213
|
-
<div class="gf-picker" id="gf-s-provider-picker">
|
|
1065
|
+
<div class="gf-picker" id="gf-s-provider-picker" tabindex="0">
|
|
1214
1066
|
<span class="gf-picker-value" id="gf-s-provider-label">${PROVIDERS[saved.provider]?.label || "OpenAI"}</span>
|
|
1067
|
+
<div class="gf-cycle-dots" id="gf-s-provider-dots">
|
|
1068
|
+
<span class="gf-cycle-dot"></span>
|
|
1069
|
+
<span class="gf-cycle-dot"></span>
|
|
1070
|
+
<span class="gf-cycle-dot"></span>
|
|
1071
|
+
</div>
|
|
1215
1072
|
</div>
|
|
1216
1073
|
</div>
|
|
1217
1074
|
<div class="gf-field">
|
|
@@ -1222,39 +1079,64 @@ function createOverlay2(options) {
|
|
|
1222
1079
|
<div class="gf-sep"></div>
|
|
1223
1080
|
<div class="gf-field">
|
|
1224
1081
|
<div style="display:flex;align-items:center;justify-content:space-between">
|
|
1225
|
-
<
|
|
1082
|
+
<div style="display:flex;align-items:center;gap:4px">
|
|
1083
|
+
<label class="gf-label" style="margin:0">Presets</label>
|
|
1084
|
+
<span class="gf-help" id="gf-s-presets-help">?<span class="gf-help-tip">Saved prompt templates that add context when filling. Select a preset in the Fill panel to use it automatically.</span></span>
|
|
1085
|
+
</div>
|
|
1226
1086
|
<button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
|
|
1227
1087
|
</div>
|
|
1228
1088
|
<div class="gf-preset-list" id="gf-s-preset-list"></div>
|
|
1229
|
-
<div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
|
|
1230
|
-
<input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
|
|
1231
|
-
<textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
|
|
1232
|
-
<div class="gf-preset-form-actions">
|
|
1233
|
-
<button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
|
|
1234
|
-
<button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
|
|
1235
|
-
</div>
|
|
1236
|
-
</div>
|
|
1237
1089
|
</div>
|
|
1238
1090
|
<button class="gf-save-btn" id="gf-s-save">Save</button>
|
|
1239
1091
|
</div>
|
|
1092
|
+
<!-- Preset edit overlay \u2014 takes over entire panel -->
|
|
1093
|
+
<div class="gf-preset-overlay" id="gf-s-preset-form" style="display:none">
|
|
1094
|
+
<div class="gf-pop-header">
|
|
1095
|
+
<h3 id="gf-s-preset-form-title">New Preset</h3>
|
|
1096
|
+
</div>
|
|
1097
|
+
<div class="gf-preset-overlay-body">
|
|
1098
|
+
<div class="gf-field">
|
|
1099
|
+
<label class="gf-label">Name</label>
|
|
1100
|
+
<input class="gf-input" id="gf-s-preset-name" placeholder="e.g. D365, Healthcare, E-commerce" />
|
|
1101
|
+
</div>
|
|
1102
|
+
<div class="gf-field" style="flex:1;display:flex;flex-direction:column">
|
|
1103
|
+
<label class="gf-label">Prompt</label>
|
|
1104
|
+
<textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Describe the context for this preset... e.g. Generate data for a Microsoft Dynamics 365 Customer Engagement implementation. Use CRM terminology, consulting project names, and Microsoft partner context." style="flex:1;min-height:120px;resize:none"></textarea>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div class="gf-preset-form-actions">
|
|
1107
|
+
<button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
|
|
1108
|
+
<button class="gf-preset-form-btn save" id="gf-s-preset-save">Save Preset</button>
|
|
1109
|
+
</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
</div>
|
|
1240
1112
|
`;
|
|
1241
1113
|
shadow.appendChild(settingsPop);
|
|
1242
1114
|
const sKeyInput = settingsPop.querySelector("#gf-s-key");
|
|
1243
1115
|
const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
|
|
1244
1116
|
const sAISection = settingsPop.querySelector("#gf-s-ai-section");
|
|
1245
1117
|
const sHelpEl = settingsPop.querySelector("#gf-s-help");
|
|
1118
|
+
sKeyInput.value = saved.apiKey || "";
|
|
1246
1119
|
const sSaveBtn = settingsPop.querySelector("#gf-s-save");
|
|
1247
1120
|
const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
|
|
1248
1121
|
const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
|
|
1249
1122
|
const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
|
|
1250
1123
|
const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
|
|
1251
|
-
|
|
1124
|
+
const sProviderDots = settingsPop.querySelector("#gf-s-provider-dots");
|
|
1125
|
+
const sHelpTip = settingsPop.querySelector("#gf-s-help-tip");
|
|
1126
|
+
const sPresetsHelp = settingsPop.querySelector("#gf-s-presets-help");
|
|
1252
1127
|
const providerOrder = ["openai", "xai", "moonshot"];
|
|
1253
|
-
let selectedProvider = saved.provider || "openai";
|
|
1128
|
+
let selectedProvider = saved.provider || aiConfig?.provider || "openai";
|
|
1129
|
+
function updateProviderDots() {
|
|
1130
|
+
const idx = providerOrder.indexOf(selectedProvider);
|
|
1131
|
+
sProviderDots.querySelectorAll(".gf-cycle-dot").forEach((dot, i) => {
|
|
1132
|
+
dot.classList.toggle("active", i === idx);
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1254
1135
|
function updateProviderDisplay() {
|
|
1255
1136
|
const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
|
|
1256
1137
|
sPickerLabel.textContent = `${p.label} (${p.model})`;
|
|
1257
|
-
|
|
1138
|
+
sHelpTip.textContent = p.helpText;
|
|
1139
|
+
updateProviderDots();
|
|
1258
1140
|
}
|
|
1259
1141
|
updateProviderDisplay();
|
|
1260
1142
|
sPickerEl.addEventListener("click", () => {
|
|
@@ -1262,6 +1144,24 @@ function createOverlay2(options) {
|
|
|
1262
1144
|
selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
|
|
1263
1145
|
updateProviderDisplay();
|
|
1264
1146
|
});
|
|
1147
|
+
sHelpEl.addEventListener("click", (e) => {
|
|
1148
|
+
e.stopPropagation();
|
|
1149
|
+
sHelpTip.classList.toggle("show");
|
|
1150
|
+
});
|
|
1151
|
+
sPresetsHelp.addEventListener("click", (e) => {
|
|
1152
|
+
e.stopPropagation();
|
|
1153
|
+
sPresetsHelp.querySelector(".gf-help-tip").classList.toggle("show");
|
|
1154
|
+
});
|
|
1155
|
+
shadow.addEventListener("click", () => {
|
|
1156
|
+
sHelpTip.classList.remove("show");
|
|
1157
|
+
sPresetsHelp.querySelector(".gf-help-tip")?.classList.remove("show");
|
|
1158
|
+
});
|
|
1159
|
+
sPickerEl.addEventListener("keydown", (e) => {
|
|
1160
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1161
|
+
e.preventDefault();
|
|
1162
|
+
sPickerEl.click();
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1265
1165
|
sUseAIToggle.addEventListener("change", () => {
|
|
1266
1166
|
sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
|
|
1267
1167
|
});
|
|
@@ -1278,18 +1178,27 @@ function createOverlay2(options) {
|
|
|
1278
1178
|
currentTheme = theme;
|
|
1279
1179
|
const isDark = theme === "dark";
|
|
1280
1180
|
sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
|
|
1281
|
-
const bg = isDark ? "#
|
|
1282
|
-
const bgInput = isDark ? "
|
|
1283
|
-
const border = isDark ? "
|
|
1284
|
-
const text = isDark ? "#
|
|
1285
|
-
const textMuted = isDark ? "
|
|
1286
|
-
const textDim = isDark ? "
|
|
1287
|
-
const btnHoverBg = isDark ? "#27272a" : "#
|
|
1181
|
+
const bg = isDark ? "#1a1a1a" : "#ffffff";
|
|
1182
|
+
const bgInput = isDark ? "rgba(255,255,255,0.06)" : "#f4f4f5";
|
|
1183
|
+
const border = isDark ? "rgba(255,255,255,0.07)" : "#d4d4d8";
|
|
1184
|
+
const text = isDark ? "#fff" : "#18181b";
|
|
1185
|
+
const textMuted = isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
|
|
1186
|
+
const textDim = isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.35)";
|
|
1187
|
+
const btnHoverBg = isDark ? "#27272a" : "#e4e4e7";
|
|
1188
|
+
const btnActiveBg = isDark ? "#3f3f46" : "#d4d4d8";
|
|
1189
|
+
const presetItemBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)";
|
|
1190
|
+
const presetItemText = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
|
|
1191
|
+
const presetBtnColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.25)";
|
|
1192
|
+
const helpBg = isDark ? "#3f3f46" : "#d4d4d8";
|
|
1193
|
+
const helpColor = isDark ? "#a1a1aa" : "#52525b";
|
|
1194
|
+
const pickerColor = isDark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.75)";
|
|
1288
1195
|
for (const pop of [settingsPop, promptPop]) {
|
|
1289
1196
|
pop.style.background = bg;
|
|
1290
|
-
pop.style.boxShadow = isDark ? "0
|
|
1197
|
+
pop.style.boxShadow = isDark ? "0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08)" : "0 4px 20px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.08)";
|
|
1291
1198
|
pop.querySelectorAll(".gf-pop-header h3").forEach((el) => el.style.color = text);
|
|
1199
|
+
pop.querySelectorAll(".gf-pop-header").forEach((el) => el.style.borderBottomColor = border);
|
|
1292
1200
|
pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
|
|
1201
|
+
pop.querySelectorAll(".gf-note").forEach((el) => el.style.color = textMuted);
|
|
1293
1202
|
pop.querySelectorAll(".gf-input").forEach((el) => {
|
|
1294
1203
|
el.style.background = bgInput;
|
|
1295
1204
|
el.style.borderColor = border;
|
|
@@ -1303,31 +1212,34 @@ function createOverlay2(options) {
|
|
|
1303
1212
|
el.style.background = isDark ? "#27272a" : "#f4f4f5";
|
|
1304
1213
|
el.style.borderColor = border;
|
|
1305
1214
|
});
|
|
1215
|
+
pop.querySelectorAll(".gf-help").forEach((el) => {
|
|
1216
|
+
el.style.background = helpBg;
|
|
1217
|
+
el.style.color = helpColor;
|
|
1218
|
+
});
|
|
1219
|
+
pop.querySelectorAll(".gf-picker-value").forEach((el) => el.style.color = pickerColor);
|
|
1220
|
+
pop.querySelectorAll(".gf-theme-btn").forEach((el) => el.style.color = textDim);
|
|
1306
1221
|
}
|
|
1307
1222
|
bar.style.background = bg;
|
|
1308
|
-
bar.style.boxShadow = isDark ? "0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0
|
|
1223
|
+
bar.style.boxShadow = isDark ? "0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px 16px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.06)";
|
|
1309
1224
|
bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
|
|
1310
1225
|
btn.style.color = textMuted;
|
|
1226
|
+
const isActive = btn.classList.contains("active");
|
|
1227
|
+
btn.style.background = isActive ? btnActiveBg : "transparent";
|
|
1311
1228
|
btn.onmouseenter = () => {
|
|
1312
1229
|
btn.style.background = btnHoverBg;
|
|
1313
1230
|
btn.style.color = text;
|
|
1314
1231
|
};
|
|
1315
1232
|
btn.onmouseleave = () => {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1233
|
+
const stillActive = btn.classList.contains("active");
|
|
1234
|
+
btn.style.background = stillActive ? btnActiveBg : "transparent";
|
|
1235
|
+
btn.style.color = stillActive ? text : textMuted;
|
|
1320
1236
|
};
|
|
1321
1237
|
});
|
|
1322
1238
|
bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
|
|
1323
1239
|
fab.style.background = bg;
|
|
1324
1240
|
fab.style.color = textMuted;
|
|
1325
|
-
fab.style.boxShadow = isDark ? "0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px
|
|
1241
|
+
fab.style.boxShadow = isDark ? "0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px 12px rgba(0,0,0,0.08), 0 0 0 1px rgba(0,0,0,0.06)";
|
|
1326
1242
|
}
|
|
1327
|
-
if (currentTheme === "light") applyTheme("light");
|
|
1328
|
-
sThemeBtn.addEventListener("click", () => {
|
|
1329
|
-
applyTheme(currentTheme === "dark" ? "light" : "dark");
|
|
1330
|
-
});
|
|
1331
1243
|
const promptPop = document.createElement("div");
|
|
1332
1244
|
promptPop.className = "gf-popover";
|
|
1333
1245
|
promptPop.style.width = "300px";
|
|
@@ -1344,6 +1256,7 @@ function createOverlay2(options) {
|
|
|
1344
1256
|
<label class="gf-label" style="margin:0">Preset</label>
|
|
1345
1257
|
<div class="gf-picker" id="gf-p-preset-picker">
|
|
1346
1258
|
<span class="gf-picker-value" id="gf-p-preset-label">None</span>
|
|
1259
|
+
<div class="gf-cycle-dots" id="gf-p-preset-dots"></div>
|
|
1347
1260
|
</div>
|
|
1348
1261
|
</div>
|
|
1349
1262
|
<div class="gf-field" id="gf-p-prompt-wrap">
|
|
@@ -1362,6 +1275,7 @@ function createOverlay2(options) {
|
|
|
1362
1275
|
const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
|
|
1363
1276
|
const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
|
|
1364
1277
|
const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
|
|
1278
|
+
const pPresetDots = promptPop.querySelector("#gf-p-preset-dots");
|
|
1365
1279
|
const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
|
|
1366
1280
|
const pPromptEl = promptPop.querySelector("#gf-p-prompt");
|
|
1367
1281
|
const pFillBtn = promptPop.querySelector("#gf-p-fill");
|
|
@@ -1378,10 +1292,18 @@ function createOverlay2(options) {
|
|
|
1378
1292
|
pPresetRow.style.display = "flex";
|
|
1379
1293
|
const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
|
|
1380
1294
|
pPresetLabel.textContent = active ? active.name : "None";
|
|
1295
|
+
const totalOptions = presets.length + 1;
|
|
1296
|
+
const activeIdx = activePresetId ? presets.findIndex((p) => p.id === activePresetId) + 1 : 0;
|
|
1297
|
+
pPresetDots.innerHTML = "";
|
|
1298
|
+
for (let i = 0; i < totalOptions; i++) {
|
|
1299
|
+
const dot = document.createElement("span");
|
|
1300
|
+
dot.className = `gf-cycle-dot${i === activeIdx ? " active" : ""}`;
|
|
1301
|
+
pPresetDots.appendChild(dot);
|
|
1302
|
+
}
|
|
1381
1303
|
pPromptWrap.style.display = active ? "none" : "flex";
|
|
1382
1304
|
}
|
|
1383
1305
|
function persistActivePreset() {
|
|
1384
|
-
const s = loadSettings();
|
|
1306
|
+
const s = loadSettings(aiConfig?.provider || "openai");
|
|
1385
1307
|
s.activePresetId = activePresetId;
|
|
1386
1308
|
saveSettings(s);
|
|
1387
1309
|
}
|
|
@@ -1401,6 +1323,10 @@ function createOverlay2(options) {
|
|
|
1401
1323
|
updateFillPresetUI();
|
|
1402
1324
|
});
|
|
1403
1325
|
updateFillPresetUI();
|
|
1326
|
+
if (currentTheme === "light") applyTheme("light");
|
|
1327
|
+
sThemeBtn.addEventListener("click", () => {
|
|
1328
|
+
applyTheme(currentTheme === "dark" ? "light" : "dark");
|
|
1329
|
+
});
|
|
1404
1330
|
function setStatus(text, type) {
|
|
1405
1331
|
pStatusEl.textContent = text;
|
|
1406
1332
|
pStatusEl.className = `gf-status ${type}`;
|
|
@@ -1436,7 +1362,7 @@ function createOverlay2(options) {
|
|
|
1436
1362
|
if (name === "settings") {
|
|
1437
1363
|
settingsPop.classList.add("open");
|
|
1438
1364
|
btnSettings.classList.add("active");
|
|
1439
|
-
|
|
1365
|
+
(aiConfig && sUseAIToggle.checked ? sPickerEl : sSaveBtn).focus();
|
|
1440
1366
|
} else if (name === "prompt") {
|
|
1441
1367
|
promptPop.classList.add("open");
|
|
1442
1368
|
btnFill.classList.add("active");
|
|
@@ -1574,6 +1500,7 @@ function createOverlay2(options) {
|
|
|
1574
1500
|
badge.textContent = String(fields.length);
|
|
1575
1501
|
badge.style.display = "flex";
|
|
1576
1502
|
btnFill.disabled = false;
|
|
1503
|
+
openPopover("prompt");
|
|
1577
1504
|
},
|
|
1578
1505
|
() => {
|
|
1579
1506
|
state.selecting = false;
|
|
@@ -1633,7 +1560,16 @@ function createOverlay2(options) {
|
|
|
1633
1560
|
document.removeEventListener("mouseup", onUp);
|
|
1634
1561
|
fabDragState.dragging = false;
|
|
1635
1562
|
if (fabDragState.moved) {
|
|
1636
|
-
|
|
1563
|
+
try {
|
|
1564
|
+
localStorage.setItem(
|
|
1565
|
+
FAB_POS_KEY,
|
|
1566
|
+
JSON.stringify({
|
|
1567
|
+
x: fab.getBoundingClientRect().left,
|
|
1568
|
+
y: fab.getBoundingClientRect().top
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1571
|
+
} catch {
|
|
1572
|
+
}
|
|
1637
1573
|
}
|
|
1638
1574
|
};
|
|
1639
1575
|
document.addEventListener("mousemove", onMove);
|
|
@@ -1656,44 +1592,63 @@ function createOverlay2(options) {
|
|
|
1656
1592
|
});
|
|
1657
1593
|
const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
|
|
1658
1594
|
const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
|
|
1595
|
+
const sPresetFormTitle = settingsPop.querySelector("#gf-s-preset-form-title");
|
|
1659
1596
|
const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
|
|
1660
1597
|
const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
|
|
1661
1598
|
const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
|
|
1662
1599
|
const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
|
|
1663
1600
|
const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
|
|
1664
1601
|
let editingPresetId = null;
|
|
1602
|
+
const PILL_COLORS = [
|
|
1603
|
+
{ bg: "rgba(99,102,241,0.15)", border: "rgba(99,102,241,0.3)", text: "#a5b4fc" },
|
|
1604
|
+
{ bg: "rgba(52,211,153,0.12)", border: "rgba(52,211,153,0.25)", text: "#6ee7b7" },
|
|
1605
|
+
{ bg: "rgba(251,146,60,0.12)", border: "rgba(251,146,60,0.25)", text: "#fdba74" },
|
|
1606
|
+
{ bg: "rgba(244,114,182,0.12)", border: "rgba(244,114,182,0.25)", text: "#f9a8d4" },
|
|
1607
|
+
{ bg: "rgba(56,189,248,0.12)", border: "rgba(56,189,248,0.25)", text: "#7dd3fc" },
|
|
1608
|
+
{ bg: "rgba(163,130,255,0.12)", border: "rgba(163,130,255,0.25)", text: "#c4b5fd" },
|
|
1609
|
+
{ bg: "rgba(250,204,21,0.12)", border: "rgba(250,204,21,0.25)", text: "#fde68a" }
|
|
1610
|
+
];
|
|
1665
1611
|
function renderPresetList() {
|
|
1666
1612
|
sPresetList.innerHTML = "";
|
|
1667
|
-
presets.forEach((p) => {
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1613
|
+
presets.forEach((p, i) => {
|
|
1614
|
+
const c = PILL_COLORS[i % PILL_COLORS.length];
|
|
1615
|
+
const pill = document.createElement("span");
|
|
1616
|
+
pill.className = "gf-preset-pill";
|
|
1617
|
+
pill.style.background = c.bg;
|
|
1618
|
+
pill.style.borderColor = c.border;
|
|
1619
|
+
pill.style.color = c.text;
|
|
1670
1620
|
const name = document.createElement("span");
|
|
1671
|
-
name.className = "gf-
|
|
1621
|
+
name.className = "gf-pp-name";
|
|
1672
1622
|
name.textContent = p.name;
|
|
1673
|
-
name.
|
|
1623
|
+
name.title = "Click to edit";
|
|
1674
1624
|
name.addEventListener("click", () => {
|
|
1675
1625
|
editingPresetId = p.id;
|
|
1626
|
+
sPresetFormTitle.textContent = "Edit Preset";
|
|
1676
1627
|
sPresetForm.style.display = "flex";
|
|
1677
1628
|
sPresetName.value = p.name;
|
|
1678
1629
|
sPresetPrompt.value = p.prompt;
|
|
1679
1630
|
sPresetName.focus();
|
|
1680
1631
|
});
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1632
|
+
const x = document.createElement("button");
|
|
1633
|
+
x.className = "gf-pp-x";
|
|
1634
|
+
x.innerHTML = "×";
|
|
1635
|
+
x.style.color = c.text;
|
|
1636
|
+
x.title = "Delete";
|
|
1637
|
+
x.addEventListener("click", (e) => {
|
|
1638
|
+
e.stopPropagation();
|
|
1639
|
+
presets = presets.filter((v) => v.id !== p.id);
|
|
1686
1640
|
if (activePresetId === p.id) activePresetId = null;
|
|
1687
1641
|
renderPresetList();
|
|
1688
1642
|
updateFillPresetUI();
|
|
1689
1643
|
});
|
|
1690
|
-
|
|
1691
|
-
sPresetList.appendChild(
|
|
1644
|
+
pill.append(name, x);
|
|
1645
|
+
sPresetList.appendChild(pill);
|
|
1692
1646
|
});
|
|
1693
1647
|
}
|
|
1694
1648
|
renderPresetList();
|
|
1695
1649
|
sPresetAddBtn.addEventListener("click", () => {
|
|
1696
1650
|
editingPresetId = null;
|
|
1651
|
+
sPresetFormTitle.textContent = "New Preset";
|
|
1697
1652
|
sPresetForm.style.display = "flex";
|
|
1698
1653
|
sPresetName.value = "";
|
|
1699
1654
|
sPresetPrompt.value = "";
|
|
@@ -1729,11 +1684,11 @@ function createOverlay2(options) {
|
|
|
1729
1684
|
activePresetId
|
|
1730
1685
|
};
|
|
1731
1686
|
saveSettings(s);
|
|
1732
|
-
dotWarn.style.display =
|
|
1687
|
+
dotWarn.style.display = "none";
|
|
1733
1688
|
openPopover(null);
|
|
1734
1689
|
});
|
|
1735
1690
|
async function doFill() {
|
|
1736
|
-
const settings = loadSettings();
|
|
1691
|
+
const settings = loadSettings(aiConfig?.provider || "openai");
|
|
1737
1692
|
const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
|
|
1738
1693
|
const userText = pPromptEl.value.trim();
|
|
1739
1694
|
const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
|
|
@@ -1750,14 +1705,47 @@ function createOverlay2(options) {
|
|
|
1750
1705
|
pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
|
|
1751
1706
|
return;
|
|
1752
1707
|
}
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1708
|
+
const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
|
|
1709
|
+
let blockContext = `Page: ${document.title}`;
|
|
1710
|
+
if (state.selectedBlock) {
|
|
1711
|
+
state.selectedBlock.querySelectorAll("h1,h2,h3,h4,label,legend").forEach((el) => {
|
|
1712
|
+
const t = el.textContent?.trim();
|
|
1713
|
+
if (t && t.length < 80) blockContext += `
|
|
1714
|
+
${t}`;
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
const fieldDesc = describeFields(state.fields);
|
|
1718
|
+
let userContent = `Form fields:
|
|
1719
|
+
${fieldDesc}`;
|
|
1720
|
+
if (blockContext) userContent += `
|
|
1721
|
+
|
|
1722
|
+
Page context:
|
|
1723
|
+
${blockContext}`;
|
|
1724
|
+
if (promptText) userContent += `
|
|
1725
|
+
|
|
1726
|
+
User instructions: ${promptText}`;
|
|
1727
|
+
else userContent += `
|
|
1728
|
+
|
|
1729
|
+
No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
|
|
1730
|
+
const resp = await fetch(`${provider.baseURL}/chat/completions`, {
|
|
1731
|
+
method: "POST",
|
|
1732
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.apiKey}` },
|
|
1733
|
+
body: JSON.stringify({
|
|
1734
|
+
model: provider.model,
|
|
1735
|
+
messages: [
|
|
1736
|
+
{ role: "system", content: `You are a form-filling assistant. Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys. Fill EVERY field. For select fields pick from listed options EXACTLY. For checkboxes add "checked" boolean. Generate coherent data. No markdown code blocks.` },
|
|
1737
|
+
{ role: "user", content: userContent }
|
|
1738
|
+
],
|
|
1739
|
+
temperature: 0.7,
|
|
1740
|
+
...settings.provider === "openai" ? { response_format: { type: "json_object" } } : {}
|
|
1741
|
+
})
|
|
1742
|
+
});
|
|
1743
|
+
if (!resp.ok) throw new Error(await resp.text());
|
|
1744
|
+
const data = await resp.json();
|
|
1745
|
+
const content = data.choices?.[0]?.message?.content || "";
|
|
1746
|
+
const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
|
|
1747
|
+
const parsed = JSON.parse(jsonMatch[1].trim());
|
|
1748
|
+
fillData = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
|
|
1761
1749
|
} else {
|
|
1762
1750
|
fillData = generateFakeData(state.fields);
|
|
1763
1751
|
}
|
|
@@ -1767,8 +1755,38 @@ function createOverlay2(options) {
|
|
|
1767
1755
|
} else {
|
|
1768
1756
|
setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
|
|
1769
1757
|
}
|
|
1758
|
+
if (state.selectedBlock) {
|
|
1759
|
+
const el = state.selectedBlock;
|
|
1760
|
+
el.style.transition = "box-shadow 0.8s ease";
|
|
1761
|
+
el.style.animation = "none";
|
|
1762
|
+
const rect = el.getBoundingClientRect();
|
|
1763
|
+
const ripple = document.createElement("div");
|
|
1764
|
+
Object.assign(ripple.style, {
|
|
1765
|
+
position: "fixed",
|
|
1766
|
+
top: `${rect.top}px`,
|
|
1767
|
+
left: `${rect.left}px`,
|
|
1768
|
+
width: `${rect.width}px`,
|
|
1769
|
+
height: `${rect.height}px`,
|
|
1770
|
+
borderRadius: "6px",
|
|
1771
|
+
pointerEvents: "none",
|
|
1772
|
+
zIndex: "2147483644",
|
|
1773
|
+
border: "2px solid rgba(52,211,153,0.6)",
|
|
1774
|
+
boxShadow: "0 0 0 0 rgba(52,211,153,0.4), inset 0 0 20px rgba(52,211,153,0.08)",
|
|
1775
|
+
animation: "none"
|
|
1776
|
+
});
|
|
1777
|
+
document.body.appendChild(ripple);
|
|
1778
|
+
requestAnimationFrame(() => {
|
|
1779
|
+
ripple.style.transition = "box-shadow 0.8s ease, border-color 0.8s ease, opacity 0.8s ease";
|
|
1780
|
+
ripple.style.boxShadow = "0 0 0 8px rgba(52,211,153,0), inset 0 0 0 rgba(52,211,153,0)";
|
|
1781
|
+
ripple.style.borderColor = "rgba(52,211,153,0)";
|
|
1782
|
+
ripple.style.opacity = "0";
|
|
1783
|
+
});
|
|
1784
|
+
setTimeout(() => {
|
|
1785
|
+
ripple.remove();
|
|
1786
|
+
}, 1e3);
|
|
1787
|
+
}
|
|
1770
1788
|
removeBlockHighlight();
|
|
1771
|
-
setTimeout(() => openPopover(null),
|
|
1789
|
+
setTimeout(() => openPopover(null), 800);
|
|
1772
1790
|
} catch (err) {
|
|
1773
1791
|
setStatus(cleanError(err), "error");
|
|
1774
1792
|
} finally {
|
|
@@ -1806,6 +1824,12 @@ function createOverlay2(options) {
|
|
|
1806
1824
|
}
|
|
1807
1825
|
}
|
|
1808
1826
|
document.addEventListener("keydown", handleShortcut);
|
|
1827
|
+
document.addEventListener("keydown", (e) => {
|
|
1828
|
+
if (e.key === "Escape" && currentPopover) {
|
|
1829
|
+
e.preventDefault();
|
|
1830
|
+
openPopover(null);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1809
1833
|
function destroy() {
|
|
1810
1834
|
cleanupSelector?.();
|
|
1811
1835
|
removeBlockHighlight();
|
|
@@ -1835,10 +1859,17 @@ async function fill(params) {
|
|
|
1835
1859
|
if (fields.length === 0) {
|
|
1836
1860
|
return { filled: 0, errors: ["No fillable fields found in container"] };
|
|
1837
1861
|
}
|
|
1838
|
-
const fillData =
|
|
1862
|
+
const fillData = params.ai ? await generateFillData(
|
|
1863
|
+
fields,
|
|
1864
|
+
params.prompt || "",
|
|
1865
|
+
params.provider || params.ai.provider || "openai",
|
|
1866
|
+
params.ai,
|
|
1867
|
+
params.systemPrompt
|
|
1868
|
+
) : generateFakeData(fields);
|
|
1839
1869
|
return fillFields(fields, fillData);
|
|
1840
1870
|
}
|
|
1841
1871
|
export {
|
|
1872
|
+
PROVIDERS,
|
|
1842
1873
|
fill,
|
|
1843
1874
|
init
|
|
1844
1875
|
};
|