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/dist/index.mjs CHANGED
@@ -1,261 +1,9 @@
1
- // src/types.ts
2
- var PROVIDERS = {
3
- openai: { label: "OpenAI", model: "gpt-4o-mini", baseURL: "https://api.openai.com/v1", helpText: "Uses gpt-4o-mini \u2014 fast & cheap" },
4
- xai: { label: "xAI", model: "grok-4-fast", baseURL: "https://api.x.ai/v1", helpText: "Uses Grok 4 Fast" },
5
- moonshot: { label: "Moonshot", model: "kimi-k2", baseURL: "https://api.moonshot.ai/v1", helpText: "Uses Kimi K2 \u2014 fast & cheap" }
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: Math.random() > 0.5 };
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 || findLabel2(r) === 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 findLabel2(el) {
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 loadSettings() {
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) return JSON.parse(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 { apiKey: "", provider: "openai", highlightColor: "#6366f1", theme: "dark", useAI: false, presets: [], activePresetId: null };
421
+ return defaultSettings(provider);
640
422
  }
641
423
  function saveSettings(s) {
642
- localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
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
- localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
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(/^OpenAI API error \(\d+\):\s*/, "");
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: 14px;
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: 44px; height: 44px; border-radius: 50%; border: none;
760
- background: #18181b; color: #a1a1aa; cursor: grab;
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(255,255,255,0.06);
764
- transition: transform 0.15s, color 0.15s;
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
- @keyframes gf-float {
770
- 0%, 100% { transform: translateY(0) rotate(0deg); }
771
- 25% { transform: translateY(-2px) rotate(-5deg); }
772
- 75% { transform: translateY(1px) rotate(5deg); }
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
- min-width: 280px;
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: rgba(255,255,255,0.06); color: rgba(255,255,255,0.25); cursor: not-allowed; }
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 list in settings */
982
- .gf-preset-list { display: flex; flex-direction: column; gap: 4px; }
983
- .gf-preset-item {
984
- display: flex; align-items: center; justify-content: space-between;
985
- padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,0.04);
986
- }
987
- .gf-preset-item-name { font-size: 12px; color: rgba(255,255,255,0.7); }
988
- .gf-preset-actions { display: flex; align-items: center; gap: 2px; }
989
- .gf-preset-edit {
990
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
991
- font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
992
- display: flex; align-items: center;
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-edit:hover { color: #6366f1; }
995
- .gf-preset-del {
996
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
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-del:hover { color: #f87171; }
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: 3px 10px; border: none; border-radius: 6px; font-size: 11px;
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 saved = loadSettings();
1025
- if (options.apiKey && !saved.apiKey) {
1026
- saved.apiKey = options.apiKey;
1027
- saveSettings(saved);
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
- if (options.model && !saved.model) saved.model = options.model;
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
- if (!saved.useAI || saved.apiKey) dotWarn.style.display = "none";
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.0</span>
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" title="">?</span>
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
- <label class="gf-label" style="margin:0">Presets</label>
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...&#10;&#10;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
- sKeyInput.value = saved.apiKey;
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
- sHelpEl.title = p.helpText;
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 ? "#18181b" : "#ffffff";
1289
- const bgInput = isDark ? "#09090b" : "#f4f4f5";
1290
- const border = isDark ? "#27272a" : "#e4e4e7";
1291
- const text = isDark ? "#fafafa" : "#18181b";
1292
- const textMuted = isDark ? "#a1a1aa" : "#52525b";
1293
- const textDim = isDark ? "#52525b" : "#a1a1aa";
1294
- const btnHoverBg = isDark ? "#27272a" : "#f4f4f5";
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 12px 40px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)" : "0 12px 40px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.08)";
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 8px 32px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.08)";
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
- if (!btn.classList.contains("active")) {
1324
- btn.style.background = "transparent";
1325
- btn.style.color = textMuted;
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 16px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.08)";
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
- sKeyInput.focus();
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
- localStorage.setItem(FAB_POS_KEY, JSON.stringify({ x: fab.getBoundingClientRect().left, y: fab.getBoundingClientRect().top }));
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 item = document.createElement("div");
1676
- item.className = "gf-preset-item";
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-preset-item-name";
1621
+ name.className = "gf-pp-name";
1679
1622
  name.textContent = p.name;
1680
- name.style.cursor = "pointer";
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 actions = document.createElement("div");
1689
- actions.className = "gf-preset-actions";
1690
- const edit = document.createElement("button");
1691
- edit.className = "gf-preset-edit";
1692
- edit.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.85 0 114 4L7.5 20.5 2 22l1.5-5.5z"/></svg>';
1693
- edit.title = "Edit preset";
1694
- edit.addEventListener("click", () => {
1695
- editingPresetId = p.id;
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 = "&times;";
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 = "&times;";
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
- actions.append(edit, del);
1711
- item.append(name, actions);
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 = s.useAI && !s.apiKey ? "block" : "none";
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 blockContext = state.selectedBlock ? extractBlockContext(state.selectedBlock) : "";
1775
- fillData = await generateFillData(
1776
- state.fields,
1777
- promptText,
1778
- settings,
1779
- options.systemPrompt,
1780
- blockContext
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), 600);
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 = generateFakeData(fields);
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
  };