ghostfill 0.1.3 → 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 -246
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -385
- 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,33 +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
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
.gf-preset-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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;
|
|
993
816
|
}
|
|
994
|
-
.gf-preset-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
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;
|
|
998
820
|
}
|
|
999
|
-
.gf-preset-
|
|
1000
|
-
|
|
1001
|
-
/* Preset add form */
|
|
1002
|
-
.gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
|
|
1003
|
-
.gf-preset-form-row { display: flex; gap: 4px; }
|
|
1004
|
-
.gf-preset-form-row .gf-input { flex: 1; }
|
|
1005
|
-
.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; }
|
|
1006
822
|
.gf-preset-form-btn {
|
|
1007
|
-
padding:
|
|
823
|
+
padding: 6px 14px; border: none; border-radius: 8px; font-size: 12px; font-weight: 500;
|
|
1008
824
|
cursor: pointer; font-family: inherit; transition: background 0.15s;
|
|
1009
825
|
}
|
|
1010
826
|
.gf-preset-form-btn.save { background: #6366f1; color: white; }
|
|
@@ -1012,22 +828,50 @@ var CSS2 = `
|
|
|
1012
828
|
.gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
|
|
1013
829
|
.gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
|
|
1014
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
|
+
|
|
1015
842
|
/* Help badge */
|
|
1016
843
|
.gf-help {
|
|
844
|
+
position: relative;
|
|
1017
845
|
display: inline-flex; align-items: center; justify-content: center;
|
|
1018
846
|
width: 14px; height: 14px; border-radius: 50%;
|
|
1019
847
|
background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
|
|
1020
848
|
cursor: help; flex-shrink: 0;
|
|
1021
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
|
+
}
|
|
1022
865
|
`;
|
|
1023
866
|
function createOverlay2(options) {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
+
);
|
|
1028
873
|
}
|
|
1029
|
-
|
|
1030
|
-
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.";
|
|
1031
875
|
const host = document.createElement("div");
|
|
1032
876
|
host.id = "ghostfill-root";
|
|
1033
877
|
host.style.cssText = "display:contents;";
|
|
@@ -1077,7 +921,7 @@ function createOverlay2(options) {
|
|
|
1077
921
|
dotWarn.className = "gf-dot-warn";
|
|
1078
922
|
btnSettings.style.position = "relative";
|
|
1079
923
|
btnSettings.appendChild(dotWarn);
|
|
1080
|
-
|
|
924
|
+
dotWarn.style.display = "none";
|
|
1081
925
|
const divider1 = document.createElement("span");
|
|
1082
926
|
divider1.className = "gf-divider";
|
|
1083
927
|
const divider2 = document.createElement("span");
|
|
@@ -1184,11 +1028,12 @@ function createOverlay2(options) {
|
|
|
1184
1028
|
];
|
|
1185
1029
|
const settingsPop = document.createElement("div");
|
|
1186
1030
|
settingsPop.className = "gf-popover";
|
|
1031
|
+
settingsPop.style.position = "fixed";
|
|
1187
1032
|
settingsPop.innerHTML = `
|
|
1188
1033
|
<div class="gf-pop-header">
|
|
1189
1034
|
<h3><span class="gf-slash">/</span>ghostfill</h3>
|
|
1190
1035
|
<div class="gf-header-right">
|
|
1191
|
-
<span class="gf-version">v0.1.
|
|
1036
|
+
<span class="gf-version">v0.1.3</span>
|
|
1192
1037
|
<button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
|
|
1193
1038
|
${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
|
|
1194
1039
|
</button>
|
|
@@ -1215,10 +1060,15 @@ function createOverlay2(options) {
|
|
|
1215
1060
|
<div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
|
|
1216
1061
|
<div style="display:flex;align-items:center;gap:4px">
|
|
1217
1062
|
<label class="gf-label" style="margin:0">Provider</label>
|
|
1218
|
-
<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>
|
|
1219
1064
|
</div>
|
|
1220
|
-
<div class="gf-picker" id="gf-s-provider-picker">
|
|
1065
|
+
<div class="gf-picker" id="gf-s-provider-picker" tabindex="0">
|
|
1221
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>
|
|
1222
1072
|
</div>
|
|
1223
1073
|
</div>
|
|
1224
1074
|
<div class="gf-field">
|
|
@@ -1229,39 +1079,64 @@ function createOverlay2(options) {
|
|
|
1229
1079
|
<div class="gf-sep"></div>
|
|
1230
1080
|
<div class="gf-field">
|
|
1231
1081
|
<div style="display:flex;align-items:center;justify-content:space-between">
|
|
1232
|
-
<
|
|
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>
|
|
1233
1086
|
<button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
|
|
1234
1087
|
</div>
|
|
1235
1088
|
<div class="gf-preset-list" id="gf-s-preset-list"></div>
|
|
1236
|
-
<div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
|
|
1237
|
-
<input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
|
|
1238
|
-
<textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
|
|
1239
|
-
<div class="gf-preset-form-actions">
|
|
1240
|
-
<button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
|
|
1241
|
-
<button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
|
|
1242
|
-
</div>
|
|
1243
|
-
</div>
|
|
1244
1089
|
</div>
|
|
1245
1090
|
<button class="gf-save-btn" id="gf-s-save">Save</button>
|
|
1246
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>
|
|
1247
1112
|
`;
|
|
1248
1113
|
shadow.appendChild(settingsPop);
|
|
1249
1114
|
const sKeyInput = settingsPop.querySelector("#gf-s-key");
|
|
1250
1115
|
const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
|
|
1251
1116
|
const sAISection = settingsPop.querySelector("#gf-s-ai-section");
|
|
1252
1117
|
const sHelpEl = settingsPop.querySelector("#gf-s-help");
|
|
1118
|
+
sKeyInput.value = saved.apiKey || "";
|
|
1253
1119
|
const sSaveBtn = settingsPop.querySelector("#gf-s-save");
|
|
1254
1120
|
const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
|
|
1255
1121
|
const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
|
|
1256
1122
|
const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
|
|
1257
1123
|
const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
|
|
1258
|
-
|
|
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");
|
|
1259
1127
|
const providerOrder = ["openai", "xai", "moonshot"];
|
|
1260
|
-
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
|
+
}
|
|
1261
1135
|
function updateProviderDisplay() {
|
|
1262
1136
|
const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
|
|
1263
1137
|
sPickerLabel.textContent = `${p.label} (${p.model})`;
|
|
1264
|
-
|
|
1138
|
+
sHelpTip.textContent = p.helpText;
|
|
1139
|
+
updateProviderDots();
|
|
1265
1140
|
}
|
|
1266
1141
|
updateProviderDisplay();
|
|
1267
1142
|
sPickerEl.addEventListener("click", () => {
|
|
@@ -1269,6 +1144,24 @@ function createOverlay2(options) {
|
|
|
1269
1144
|
selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
|
|
1270
1145
|
updateProviderDisplay();
|
|
1271
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
|
+
});
|
|
1272
1165
|
sUseAIToggle.addEventListener("change", () => {
|
|
1273
1166
|
sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
|
|
1274
1167
|
});
|
|
@@ -1285,18 +1178,27 @@ function createOverlay2(options) {
|
|
|
1285
1178
|
currentTheme = theme;
|
|
1286
1179
|
const isDark = theme === "dark";
|
|
1287
1180
|
sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
|
|
1288
|
-
const bg = isDark ? "#
|
|
1289
|
-
const bgInput = isDark ? "
|
|
1290
|
-
const border = isDark ? "
|
|
1291
|
-
const text = isDark ? "#
|
|
1292
|
-
const textMuted = isDark ? "
|
|
1293
|
-
const textDim = isDark ? "
|
|
1294
|
-
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)";
|
|
1295
1195
|
for (const pop of [settingsPop, promptPop]) {
|
|
1296
1196
|
pop.style.background = bg;
|
|
1297
|
-
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)";
|
|
1298
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);
|
|
1299
1200
|
pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
|
|
1201
|
+
pop.querySelectorAll(".gf-note").forEach((el) => el.style.color = textMuted);
|
|
1300
1202
|
pop.querySelectorAll(".gf-input").forEach((el) => {
|
|
1301
1203
|
el.style.background = bgInput;
|
|
1302
1204
|
el.style.borderColor = border;
|
|
@@ -1310,31 +1212,34 @@ function createOverlay2(options) {
|
|
|
1310
1212
|
el.style.background = isDark ? "#27272a" : "#f4f4f5";
|
|
1311
1213
|
el.style.borderColor = border;
|
|
1312
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);
|
|
1313
1221
|
}
|
|
1314
1222
|
bar.style.background = bg;
|
|
1315
|
-
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)";
|
|
1316
1224
|
bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
|
|
1317
1225
|
btn.style.color = textMuted;
|
|
1226
|
+
const isActive = btn.classList.contains("active");
|
|
1227
|
+
btn.style.background = isActive ? btnActiveBg : "transparent";
|
|
1318
1228
|
btn.onmouseenter = () => {
|
|
1319
1229
|
btn.style.background = btnHoverBg;
|
|
1320
1230
|
btn.style.color = text;
|
|
1321
1231
|
};
|
|
1322
1232
|
btn.onmouseleave = () => {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
}
|
|
1233
|
+
const stillActive = btn.classList.contains("active");
|
|
1234
|
+
btn.style.background = stillActive ? btnActiveBg : "transparent";
|
|
1235
|
+
btn.style.color = stillActive ? text : textMuted;
|
|
1327
1236
|
};
|
|
1328
1237
|
});
|
|
1329
1238
|
bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
|
|
1330
1239
|
fab.style.background = bg;
|
|
1331
1240
|
fab.style.color = textMuted;
|
|
1332
|
-
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)";
|
|
1333
1242
|
}
|
|
1334
|
-
if (currentTheme === "light") applyTheme("light");
|
|
1335
|
-
sThemeBtn.addEventListener("click", () => {
|
|
1336
|
-
applyTheme(currentTheme === "dark" ? "light" : "dark");
|
|
1337
|
-
});
|
|
1338
1243
|
const promptPop = document.createElement("div");
|
|
1339
1244
|
promptPop.className = "gf-popover";
|
|
1340
1245
|
promptPop.style.width = "300px";
|
|
@@ -1351,6 +1256,7 @@ function createOverlay2(options) {
|
|
|
1351
1256
|
<label class="gf-label" style="margin:0">Preset</label>
|
|
1352
1257
|
<div class="gf-picker" id="gf-p-preset-picker">
|
|
1353
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>
|
|
1354
1260
|
</div>
|
|
1355
1261
|
</div>
|
|
1356
1262
|
<div class="gf-field" id="gf-p-prompt-wrap">
|
|
@@ -1369,6 +1275,7 @@ function createOverlay2(options) {
|
|
|
1369
1275
|
const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
|
|
1370
1276
|
const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
|
|
1371
1277
|
const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
|
|
1278
|
+
const pPresetDots = promptPop.querySelector("#gf-p-preset-dots");
|
|
1372
1279
|
const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
|
|
1373
1280
|
const pPromptEl = promptPop.querySelector("#gf-p-prompt");
|
|
1374
1281
|
const pFillBtn = promptPop.querySelector("#gf-p-fill");
|
|
@@ -1385,10 +1292,18 @@ function createOverlay2(options) {
|
|
|
1385
1292
|
pPresetRow.style.display = "flex";
|
|
1386
1293
|
const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
|
|
1387
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
|
+
}
|
|
1388
1303
|
pPromptWrap.style.display = active ? "none" : "flex";
|
|
1389
1304
|
}
|
|
1390
1305
|
function persistActivePreset() {
|
|
1391
|
-
const s = loadSettings();
|
|
1306
|
+
const s = loadSettings(aiConfig?.provider || "openai");
|
|
1392
1307
|
s.activePresetId = activePresetId;
|
|
1393
1308
|
saveSettings(s);
|
|
1394
1309
|
}
|
|
@@ -1408,6 +1323,10 @@ function createOverlay2(options) {
|
|
|
1408
1323
|
updateFillPresetUI();
|
|
1409
1324
|
});
|
|
1410
1325
|
updateFillPresetUI();
|
|
1326
|
+
if (currentTheme === "light") applyTheme("light");
|
|
1327
|
+
sThemeBtn.addEventListener("click", () => {
|
|
1328
|
+
applyTheme(currentTheme === "dark" ? "light" : "dark");
|
|
1329
|
+
});
|
|
1411
1330
|
function setStatus(text, type) {
|
|
1412
1331
|
pStatusEl.textContent = text;
|
|
1413
1332
|
pStatusEl.className = `gf-status ${type}`;
|
|
@@ -1443,7 +1362,7 @@ function createOverlay2(options) {
|
|
|
1443
1362
|
if (name === "settings") {
|
|
1444
1363
|
settingsPop.classList.add("open");
|
|
1445
1364
|
btnSettings.classList.add("active");
|
|
1446
|
-
|
|
1365
|
+
(aiConfig && sUseAIToggle.checked ? sPickerEl : sSaveBtn).focus();
|
|
1447
1366
|
} else if (name === "prompt") {
|
|
1448
1367
|
promptPop.classList.add("open");
|
|
1449
1368
|
btnFill.classList.add("active");
|
|
@@ -1581,6 +1500,7 @@ function createOverlay2(options) {
|
|
|
1581
1500
|
badge.textContent = String(fields.length);
|
|
1582
1501
|
badge.style.display = "flex";
|
|
1583
1502
|
btnFill.disabled = false;
|
|
1503
|
+
openPopover("prompt");
|
|
1584
1504
|
},
|
|
1585
1505
|
() => {
|
|
1586
1506
|
state.selecting = false;
|
|
@@ -1640,7 +1560,16 @@ function createOverlay2(options) {
|
|
|
1640
1560
|
document.removeEventListener("mouseup", onUp);
|
|
1641
1561
|
fabDragState.dragging = false;
|
|
1642
1562
|
if (fabDragState.moved) {
|
|
1643
|
-
|
|
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
|
+
}
|
|
1644
1573
|
}
|
|
1645
1574
|
};
|
|
1646
1575
|
document.addEventListener("mousemove", onMove);
|
|
@@ -1663,58 +1592,63 @@ function createOverlay2(options) {
|
|
|
1663
1592
|
});
|
|
1664
1593
|
const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
|
|
1665
1594
|
const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
|
|
1595
|
+
const sPresetFormTitle = settingsPop.querySelector("#gf-s-preset-form-title");
|
|
1666
1596
|
const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
|
|
1667
1597
|
const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
|
|
1668
1598
|
const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
|
|
1669
1599
|
const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
|
|
1670
1600
|
const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
|
|
1671
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
|
+
];
|
|
1672
1611
|
function renderPresetList() {
|
|
1673
1612
|
sPresetList.innerHTML = "";
|
|
1674
|
-
presets.forEach((p) => {
|
|
1675
|
-
const
|
|
1676
|
-
|
|
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;
|
|
1677
1620
|
const name = document.createElement("span");
|
|
1678
|
-
name.className = "gf-
|
|
1621
|
+
name.className = "gf-pp-name";
|
|
1679
1622
|
name.textContent = p.name;
|
|
1680
|
-
name.
|
|
1623
|
+
name.title = "Click to edit";
|
|
1681
1624
|
name.addEventListener("click", () => {
|
|
1682
1625
|
editingPresetId = p.id;
|
|
1626
|
+
sPresetFormTitle.textContent = "Edit Preset";
|
|
1683
1627
|
sPresetForm.style.display = "flex";
|
|
1684
1628
|
sPresetName.value = p.name;
|
|
1685
1629
|
sPresetPrompt.value = p.prompt;
|
|
1686
1630
|
sPresetName.focus();
|
|
1687
1631
|
});
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
sPresetForm.style.display = "flex";
|
|
1697
|
-
sPresetName.value = p.name;
|
|
1698
|
-
sPresetPrompt.value = p.prompt;
|
|
1699
|
-
sPresetName.focus();
|
|
1700
|
-
});
|
|
1701
|
-
const del = document.createElement("button");
|
|
1702
|
-
del.className = "gf-preset-del";
|
|
1703
|
-
del.innerHTML = "×";
|
|
1704
|
-
del.addEventListener("click", () => {
|
|
1705
|
-
presets = presets.filter((x) => x.id !== p.id);
|
|
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);
|
|
1706
1640
|
if (activePresetId === p.id) activePresetId = null;
|
|
1707
1641
|
renderPresetList();
|
|
1708
1642
|
updateFillPresetUI();
|
|
1709
1643
|
});
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
sPresetList.appendChild(item);
|
|
1644
|
+
pill.append(name, x);
|
|
1645
|
+
sPresetList.appendChild(pill);
|
|
1713
1646
|
});
|
|
1714
1647
|
}
|
|
1715
1648
|
renderPresetList();
|
|
1716
1649
|
sPresetAddBtn.addEventListener("click", () => {
|
|
1717
1650
|
editingPresetId = null;
|
|
1651
|
+
sPresetFormTitle.textContent = "New Preset";
|
|
1718
1652
|
sPresetForm.style.display = "flex";
|
|
1719
1653
|
sPresetName.value = "";
|
|
1720
1654
|
sPresetPrompt.value = "";
|
|
@@ -1750,11 +1684,11 @@ function createOverlay2(options) {
|
|
|
1750
1684
|
activePresetId
|
|
1751
1685
|
};
|
|
1752
1686
|
saveSettings(s);
|
|
1753
|
-
dotWarn.style.display =
|
|
1687
|
+
dotWarn.style.display = "none";
|
|
1754
1688
|
openPopover(null);
|
|
1755
1689
|
});
|
|
1756
1690
|
async function doFill() {
|
|
1757
|
-
const settings = loadSettings();
|
|
1691
|
+
const settings = loadSettings(aiConfig?.provider || "openai");
|
|
1758
1692
|
const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
|
|
1759
1693
|
const userText = pPromptEl.value.trim();
|
|
1760
1694
|
const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
|
|
@@ -1771,14 +1705,47 @@ function createOverlay2(options) {
|
|
|
1771
1705
|
pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
|
|
1772
1706
|
return;
|
|
1773
1707
|
}
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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 || [];
|
|
1782
1749
|
} else {
|
|
1783
1750
|
fillData = generateFakeData(state.fields);
|
|
1784
1751
|
}
|
|
@@ -1788,8 +1755,38 @@ function createOverlay2(options) {
|
|
|
1788
1755
|
} else {
|
|
1789
1756
|
setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
|
|
1790
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
|
+
}
|
|
1791
1788
|
removeBlockHighlight();
|
|
1792
|
-
setTimeout(() => openPopover(null),
|
|
1789
|
+
setTimeout(() => openPopover(null), 800);
|
|
1793
1790
|
} catch (err) {
|
|
1794
1791
|
setStatus(cleanError(err), "error");
|
|
1795
1792
|
} finally {
|
|
@@ -1827,6 +1824,12 @@ function createOverlay2(options) {
|
|
|
1827
1824
|
}
|
|
1828
1825
|
}
|
|
1829
1826
|
document.addEventListener("keydown", handleShortcut);
|
|
1827
|
+
document.addEventListener("keydown", (e) => {
|
|
1828
|
+
if (e.key === "Escape" && currentPopover) {
|
|
1829
|
+
e.preventDefault();
|
|
1830
|
+
openPopover(null);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1830
1833
|
function destroy() {
|
|
1831
1834
|
cleanupSelector?.();
|
|
1832
1835
|
removeBlockHighlight();
|
|
@@ -1856,10 +1859,17 @@ async function fill(params) {
|
|
|
1856
1859
|
if (fields.length === 0) {
|
|
1857
1860
|
return { filled: 0, errors: ["No fillable fields found in container"] };
|
|
1858
1861
|
}
|
|
1859
|
-
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);
|
|
1860
1869
|
return fillFields(fields, fillData);
|
|
1861
1870
|
}
|
|
1862
1871
|
export {
|
|
1872
|
+
PROVIDERS,
|
|
1863
1873
|
fill,
|
|
1864
1874
|
init
|
|
1865
1875
|
};
|