ghostfill 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ghostfill might be problematic. Click here for more details.

package/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,26 +785,42 @@ var CSS2 = `
978
785
  }
979
786
  .gf-preset-chip.add:hover { color: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.3); }
980
787
 
981
- /* Preset 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);
788
+ /* Preset pills in settings */
789
+ .gf-preset-list { display: flex; flex-wrap: wrap; gap: 6px; }
790
+ .gf-preset-pill {
791
+ display: inline-flex; align-items: center; gap: 4px;
792
+ padding: 4px 10px; border-radius: 20px;
793
+ font-size: 12px; font-weight: 500; cursor: pointer;
794
+ border: 1px solid; transition: all 0.15s;
795
+ }
796
+ .gf-preset-pill .gf-pp-name {
797
+ cursor: pointer; transition: opacity 0.15s;
798
+ }
799
+ .gf-preset-pill .gf-pp-name:hover { opacity: 0.7; }
800
+ .gf-preset-pill .gf-pp-x {
801
+ background: none; border: none; cursor: pointer;
802
+ font-size: 13px; line-height: 1; opacity: 0.4; transition: opacity 0.15s, color 0.15s;
803
+ padding: 0; margin-left: 2px; font-family: inherit;
804
+ }
805
+ .gf-preset-pill .gf-pp-x:hover { opacity: 1; color: #f87171; }
806
+
807
+ /* Preset edit overlay \u2014 takes over the entire settings panel */
808
+ .gf-preset-overlay {
809
+ position: absolute; inset: 0;
810
+ background: #1a1a1a; border-radius: 16px;
811
+ display: none; flex-direction: column;
812
+ z-index: 5;
813
+ }
814
+ .gf-preset-overlay[style*="display: flex"], .gf-preset-overlay[style*="display:flex"] {
815
+ display: flex;
986
816
  }
987
- .gf-preset-item-name { font-size: 12px; color: rgba(255,255,255,0.7); }
988
- .gf-preset-del {
989
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
990
- font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
817
+ .gf-preset-overlay-body {
818
+ flex: 1; display: flex; flex-direction: column;
819
+ padding: 0 16px 16px; gap: 10px; overflow-y: auto;
991
820
  }
992
- .gf-preset-del:hover { color: #f87171; }
993
-
994
- /* Preset add form */
995
- .gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
996
- .gf-preset-form-row { display: flex; gap: 4px; }
997
- .gf-preset-form-row .gf-input { flex: 1; }
998
- .gf-preset-form-actions { display: flex; gap: 4px; justify-content: flex-end; }
821
+ .gf-preset-form-actions { display: flex; gap: 6px; justify-content: flex-end; }
999
822
  .gf-preset-form-btn {
1000
- padding: 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;
1001
824
  cursor: pointer; font-family: inherit; transition: background 0.15s;
1002
825
  }
1003
826
  .gf-preset-form-btn.save { background: #6366f1; color: white; }
@@ -1005,22 +828,50 @@ var CSS2 = `
1005
828
  .gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
1006
829
  .gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
1007
830
 
831
+ /* Cycle dots (vertical indicator like Agentation) */
832
+ .gf-cycle-dots {
833
+ display: flex; flex-direction: column; gap: 2px; margin-left: 4px;
834
+ }
835
+ .gf-cycle-dot {
836
+ width: 3px; height: 3px; border-radius: 50%;
837
+ background: rgba(255,255,255,0.2); transition: background 0.2s, transform 0.2s;
838
+ transform: scale(0.67);
839
+ }
840
+ .gf-cycle-dot.active { background: #fff; transform: scale(1); }
841
+
1008
842
  /* Help badge */
1009
843
  .gf-help {
844
+ position: relative;
1010
845
  display: inline-flex; align-items: center; justify-content: center;
1011
846
  width: 14px; height: 14px; border-radius: 50%;
1012
847
  background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
1013
848
  cursor: help; flex-shrink: 0;
1014
849
  }
850
+ .gf-help-tip {
851
+ display: none; position: absolute; bottom: calc(100% + 6px); left: 50%;
852
+ transform: translateX(-50%); padding: 6px 10px;
853
+ background: #383838; color: rgba(255,255,255,0.7);
854
+ font-size: 11px; font-weight: 400; line-height: 1.4;
855
+ border-radius: 8px; white-space: normal; width: 180px; text-align: left;
856
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 100;
857
+ }
858
+ .gf-help-tip.show { display: block; }
859
+
860
+ .gf-note {
861
+ font-size: 11px;
862
+ color: rgba(255,255,255,0.5);
863
+ line-height: 1.5;
864
+ }
1015
865
  `;
1016
866
  function createOverlay2(options) {
1017
- const saved = loadSettings();
1018
- if (options.apiKey && !saved.apiKey) {
1019
- saved.apiKey = options.apiKey;
1020
- 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
+ );
1021
873
  }
1022
- if (options.model && !saved.model) saved.model = options.model;
1023
- if (options.baseURL && !saved.baseURL) saved.baseURL = options.baseURL;
874
+ const backendLabel = aiConfig ? aiConfig.requestFillData ? "Custom secure handler" : aiConfig.endpoint || "/api/ghostfill" : "Configure init({ ai: ... }) to enable AI.";
1024
875
  const host = document.createElement("div");
1025
876
  host.id = "ghostfill-root";
1026
877
  host.style.cssText = "display:contents;";
@@ -1070,7 +921,7 @@ function createOverlay2(options) {
1070
921
  dotWarn.className = "gf-dot-warn";
1071
922
  btnSettings.style.position = "relative";
1072
923
  btnSettings.appendChild(dotWarn);
1073
- if (!saved.useAI || saved.apiKey) dotWarn.style.display = "none";
924
+ dotWarn.style.display = "none";
1074
925
  const divider1 = document.createElement("span");
1075
926
  divider1.className = "gf-divider";
1076
927
  const divider2 = document.createElement("span");
@@ -1177,11 +1028,12 @@ function createOverlay2(options) {
1177
1028
  ];
1178
1029
  const settingsPop = document.createElement("div");
1179
1030
  settingsPop.className = "gf-popover";
1031
+ settingsPop.style.position = "fixed";
1180
1032
  settingsPop.innerHTML = `
1181
1033
  <div class="gf-pop-header">
1182
1034
  <h3><span class="gf-slash">/</span>ghostfill</h3>
1183
1035
  <div class="gf-header-right">
1184
- <span class="gf-version">v0.1.0</span>
1036
+ <span class="gf-version">v0.1.3</span>
1185
1037
  <button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
1186
1038
  ${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
1187
1039
  </button>
@@ -1208,10 +1060,15 @@ function createOverlay2(options) {
1208
1060
  <div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
1209
1061
  <div style="display:flex;align-items:center;gap:4px">
1210
1062
  <label class="gf-label" style="margin:0">Provider</label>
1211
- <span class="gf-help" id="gf-s-help" title="">?</span>
1063
+ <span class="gf-help" id="gf-s-help">?<span class="gf-help-tip" id="gf-s-help-tip"></span></span>
1212
1064
  </div>
1213
- <div class="gf-picker" id="gf-s-provider-picker">
1065
+ <div class="gf-picker" id="gf-s-provider-picker" tabindex="0">
1214
1066
  <span class="gf-picker-value" id="gf-s-provider-label">${PROVIDERS[saved.provider]?.label || "OpenAI"}</span>
1067
+ <div class="gf-cycle-dots" id="gf-s-provider-dots">
1068
+ <span class="gf-cycle-dot"></span>
1069
+ <span class="gf-cycle-dot"></span>
1070
+ <span class="gf-cycle-dot"></span>
1071
+ </div>
1215
1072
  </div>
1216
1073
  </div>
1217
1074
  <div class="gf-field">
@@ -1222,39 +1079,64 @@ function createOverlay2(options) {
1222
1079
  <div class="gf-sep"></div>
1223
1080
  <div class="gf-field">
1224
1081
  <div style="display:flex;align-items:center;justify-content:space-between">
1225
- <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>
1226
1086
  <button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
1227
1087
  </div>
1228
1088
  <div class="gf-preset-list" id="gf-s-preset-list"></div>
1229
- <div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
1230
- <input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
1231
- <textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
1232
- <div class="gf-preset-form-actions">
1233
- <button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
1234
- <button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
1235
- </div>
1236
- </div>
1237
1089
  </div>
1238
1090
  <button class="gf-save-btn" id="gf-s-save">Save</button>
1239
1091
  </div>
1092
+ <!-- Preset edit overlay \u2014 takes over entire panel -->
1093
+ <div class="gf-preset-overlay" id="gf-s-preset-form" style="display:none">
1094
+ <div class="gf-pop-header">
1095
+ <h3 id="gf-s-preset-form-title">New Preset</h3>
1096
+ </div>
1097
+ <div class="gf-preset-overlay-body">
1098
+ <div class="gf-field">
1099
+ <label class="gf-label">Name</label>
1100
+ <input class="gf-input" id="gf-s-preset-name" placeholder="e.g. D365, Healthcare, E-commerce" />
1101
+ </div>
1102
+ <div class="gf-field" style="flex:1;display:flex;flex-direction:column">
1103
+ <label class="gf-label">Prompt</label>
1104
+ <textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Describe the context for this preset...&#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>
1240
1112
  `;
1241
1113
  shadow.appendChild(settingsPop);
1242
1114
  const sKeyInput = settingsPop.querySelector("#gf-s-key");
1243
1115
  const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
1244
1116
  const sAISection = settingsPop.querySelector("#gf-s-ai-section");
1245
1117
  const sHelpEl = settingsPop.querySelector("#gf-s-help");
1118
+ sKeyInput.value = saved.apiKey || "";
1246
1119
  const sSaveBtn = settingsPop.querySelector("#gf-s-save");
1247
1120
  const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
1248
1121
  const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
1249
1122
  const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
1250
1123
  const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
1251
- 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");
1252
1127
  const providerOrder = ["openai", "xai", "moonshot"];
1253
- let selectedProvider = saved.provider || "openai";
1128
+ let selectedProvider = saved.provider || aiConfig?.provider || "openai";
1129
+ function updateProviderDots() {
1130
+ const idx = providerOrder.indexOf(selectedProvider);
1131
+ sProviderDots.querySelectorAll(".gf-cycle-dot").forEach((dot, i) => {
1132
+ dot.classList.toggle("active", i === idx);
1133
+ });
1134
+ }
1254
1135
  function updateProviderDisplay() {
1255
1136
  const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
1256
1137
  sPickerLabel.textContent = `${p.label} (${p.model})`;
1257
- sHelpEl.title = p.helpText;
1138
+ sHelpTip.textContent = p.helpText;
1139
+ updateProviderDots();
1258
1140
  }
1259
1141
  updateProviderDisplay();
1260
1142
  sPickerEl.addEventListener("click", () => {
@@ -1262,6 +1144,24 @@ function createOverlay2(options) {
1262
1144
  selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
1263
1145
  updateProviderDisplay();
1264
1146
  });
1147
+ sHelpEl.addEventListener("click", (e) => {
1148
+ e.stopPropagation();
1149
+ sHelpTip.classList.toggle("show");
1150
+ });
1151
+ sPresetsHelp.addEventListener("click", (e) => {
1152
+ e.stopPropagation();
1153
+ sPresetsHelp.querySelector(".gf-help-tip").classList.toggle("show");
1154
+ });
1155
+ shadow.addEventListener("click", () => {
1156
+ sHelpTip.classList.remove("show");
1157
+ sPresetsHelp.querySelector(".gf-help-tip")?.classList.remove("show");
1158
+ });
1159
+ sPickerEl.addEventListener("keydown", (e) => {
1160
+ if (e.key === "Enter" || e.key === " ") {
1161
+ e.preventDefault();
1162
+ sPickerEl.click();
1163
+ }
1164
+ });
1265
1165
  sUseAIToggle.addEventListener("change", () => {
1266
1166
  sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
1267
1167
  });
@@ -1278,18 +1178,27 @@ function createOverlay2(options) {
1278
1178
  currentTheme = theme;
1279
1179
  const isDark = theme === "dark";
1280
1180
  sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
1281
- const bg = isDark ? "#18181b" : "#ffffff";
1282
- const bgInput = isDark ? "#09090b" : "#f4f4f5";
1283
- const border = isDark ? "#27272a" : "#e4e4e7";
1284
- const text = isDark ? "#fafafa" : "#18181b";
1285
- const textMuted = isDark ? "#a1a1aa" : "#52525b";
1286
- const textDim = isDark ? "#52525b" : "#a1a1aa";
1287
- 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)";
1288
1195
  for (const pop of [settingsPop, promptPop]) {
1289
1196
  pop.style.background = bg;
1290
- 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)";
1291
1198
  pop.querySelectorAll(".gf-pop-header h3").forEach((el) => el.style.color = text);
1199
+ pop.querySelectorAll(".gf-pop-header").forEach((el) => el.style.borderBottomColor = border);
1292
1200
  pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
1201
+ pop.querySelectorAll(".gf-note").forEach((el) => el.style.color = textMuted);
1293
1202
  pop.querySelectorAll(".gf-input").forEach((el) => {
1294
1203
  el.style.background = bgInput;
1295
1204
  el.style.borderColor = border;
@@ -1303,31 +1212,34 @@ function createOverlay2(options) {
1303
1212
  el.style.background = isDark ? "#27272a" : "#f4f4f5";
1304
1213
  el.style.borderColor = border;
1305
1214
  });
1215
+ pop.querySelectorAll(".gf-help").forEach((el) => {
1216
+ el.style.background = helpBg;
1217
+ el.style.color = helpColor;
1218
+ });
1219
+ pop.querySelectorAll(".gf-picker-value").forEach((el) => el.style.color = pickerColor);
1220
+ pop.querySelectorAll(".gf-theme-btn").forEach((el) => el.style.color = textDim);
1306
1221
  }
1307
1222
  bar.style.background = bg;
1308
- bar.style.boxShadow = isDark ? "0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 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)";
1309
1224
  bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
1310
1225
  btn.style.color = textMuted;
1226
+ const isActive = btn.classList.contains("active");
1227
+ btn.style.background = isActive ? btnActiveBg : "transparent";
1311
1228
  btn.onmouseenter = () => {
1312
1229
  btn.style.background = btnHoverBg;
1313
1230
  btn.style.color = text;
1314
1231
  };
1315
1232
  btn.onmouseleave = () => {
1316
- if (!btn.classList.contains("active")) {
1317
- btn.style.background = "transparent";
1318
- btn.style.color = textMuted;
1319
- }
1233
+ const stillActive = btn.classList.contains("active");
1234
+ btn.style.background = stillActive ? btnActiveBg : "transparent";
1235
+ btn.style.color = stillActive ? text : textMuted;
1320
1236
  };
1321
1237
  });
1322
1238
  bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
1323
1239
  fab.style.background = bg;
1324
1240
  fab.style.color = textMuted;
1325
- fab.style.boxShadow = isDark ? "0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06)" : "0 4px 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)";
1326
1242
  }
1327
- if (currentTheme === "light") applyTheme("light");
1328
- sThemeBtn.addEventListener("click", () => {
1329
- applyTheme(currentTheme === "dark" ? "light" : "dark");
1330
- });
1331
1243
  const promptPop = document.createElement("div");
1332
1244
  promptPop.className = "gf-popover";
1333
1245
  promptPop.style.width = "300px";
@@ -1344,6 +1256,7 @@ function createOverlay2(options) {
1344
1256
  <label class="gf-label" style="margin:0">Preset</label>
1345
1257
  <div class="gf-picker" id="gf-p-preset-picker">
1346
1258
  <span class="gf-picker-value" id="gf-p-preset-label">None</span>
1259
+ <div class="gf-cycle-dots" id="gf-p-preset-dots"></div>
1347
1260
  </div>
1348
1261
  </div>
1349
1262
  <div class="gf-field" id="gf-p-prompt-wrap">
@@ -1362,6 +1275,7 @@ function createOverlay2(options) {
1362
1275
  const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
1363
1276
  const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
1364
1277
  const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
1278
+ const pPresetDots = promptPop.querySelector("#gf-p-preset-dots");
1365
1279
  const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
1366
1280
  const pPromptEl = promptPop.querySelector("#gf-p-prompt");
1367
1281
  const pFillBtn = promptPop.querySelector("#gf-p-fill");
@@ -1378,10 +1292,18 @@ function createOverlay2(options) {
1378
1292
  pPresetRow.style.display = "flex";
1379
1293
  const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
1380
1294
  pPresetLabel.textContent = active ? active.name : "None";
1295
+ const totalOptions = presets.length + 1;
1296
+ const activeIdx = activePresetId ? presets.findIndex((p) => p.id === activePresetId) + 1 : 0;
1297
+ pPresetDots.innerHTML = "";
1298
+ for (let i = 0; i < totalOptions; i++) {
1299
+ const dot = document.createElement("span");
1300
+ dot.className = `gf-cycle-dot${i === activeIdx ? " active" : ""}`;
1301
+ pPresetDots.appendChild(dot);
1302
+ }
1381
1303
  pPromptWrap.style.display = active ? "none" : "flex";
1382
1304
  }
1383
1305
  function persistActivePreset() {
1384
- const s = loadSettings();
1306
+ const s = loadSettings(aiConfig?.provider || "openai");
1385
1307
  s.activePresetId = activePresetId;
1386
1308
  saveSettings(s);
1387
1309
  }
@@ -1401,6 +1323,10 @@ function createOverlay2(options) {
1401
1323
  updateFillPresetUI();
1402
1324
  });
1403
1325
  updateFillPresetUI();
1326
+ if (currentTheme === "light") applyTheme("light");
1327
+ sThemeBtn.addEventListener("click", () => {
1328
+ applyTheme(currentTheme === "dark" ? "light" : "dark");
1329
+ });
1404
1330
  function setStatus(text, type) {
1405
1331
  pStatusEl.textContent = text;
1406
1332
  pStatusEl.className = `gf-status ${type}`;
@@ -1436,7 +1362,7 @@ function createOverlay2(options) {
1436
1362
  if (name === "settings") {
1437
1363
  settingsPop.classList.add("open");
1438
1364
  btnSettings.classList.add("active");
1439
- sKeyInput.focus();
1365
+ (aiConfig && sUseAIToggle.checked ? sPickerEl : sSaveBtn).focus();
1440
1366
  } else if (name === "prompt") {
1441
1367
  promptPop.classList.add("open");
1442
1368
  btnFill.classList.add("active");
@@ -1574,6 +1500,7 @@ function createOverlay2(options) {
1574
1500
  badge.textContent = String(fields.length);
1575
1501
  badge.style.display = "flex";
1576
1502
  btnFill.disabled = false;
1503
+ openPopover("prompt");
1577
1504
  },
1578
1505
  () => {
1579
1506
  state.selecting = false;
@@ -1633,7 +1560,16 @@ function createOverlay2(options) {
1633
1560
  document.removeEventListener("mouseup", onUp);
1634
1561
  fabDragState.dragging = false;
1635
1562
  if (fabDragState.moved) {
1636
- 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
+ }
1637
1573
  }
1638
1574
  };
1639
1575
  document.addEventListener("mousemove", onMove);
@@ -1656,44 +1592,63 @@ function createOverlay2(options) {
1656
1592
  });
1657
1593
  const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
1658
1594
  const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
1595
+ const sPresetFormTitle = settingsPop.querySelector("#gf-s-preset-form-title");
1659
1596
  const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
1660
1597
  const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
1661
1598
  const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
1662
1599
  const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
1663
1600
  const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
1664
1601
  let editingPresetId = null;
1602
+ const PILL_COLORS = [
1603
+ { bg: "rgba(99,102,241,0.15)", border: "rgba(99,102,241,0.3)", text: "#a5b4fc" },
1604
+ { bg: "rgba(52,211,153,0.12)", border: "rgba(52,211,153,0.25)", text: "#6ee7b7" },
1605
+ { bg: "rgba(251,146,60,0.12)", border: "rgba(251,146,60,0.25)", text: "#fdba74" },
1606
+ { bg: "rgba(244,114,182,0.12)", border: "rgba(244,114,182,0.25)", text: "#f9a8d4" },
1607
+ { bg: "rgba(56,189,248,0.12)", border: "rgba(56,189,248,0.25)", text: "#7dd3fc" },
1608
+ { bg: "rgba(163,130,255,0.12)", border: "rgba(163,130,255,0.25)", text: "#c4b5fd" },
1609
+ { bg: "rgba(250,204,21,0.12)", border: "rgba(250,204,21,0.25)", text: "#fde68a" }
1610
+ ];
1665
1611
  function renderPresetList() {
1666
1612
  sPresetList.innerHTML = "";
1667
- presets.forEach((p) => {
1668
- const item = document.createElement("div");
1669
- 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;
1670
1620
  const name = document.createElement("span");
1671
- name.className = "gf-preset-item-name";
1621
+ name.className = "gf-pp-name";
1672
1622
  name.textContent = p.name;
1673
- name.style.cursor = "pointer";
1623
+ name.title = "Click to edit";
1674
1624
  name.addEventListener("click", () => {
1675
1625
  editingPresetId = p.id;
1626
+ sPresetFormTitle.textContent = "Edit Preset";
1676
1627
  sPresetForm.style.display = "flex";
1677
1628
  sPresetName.value = p.name;
1678
1629
  sPresetPrompt.value = p.prompt;
1679
1630
  sPresetName.focus();
1680
1631
  });
1681
- const del = document.createElement("button");
1682
- del.className = "gf-preset-del";
1683
- del.innerHTML = "&times;";
1684
- del.addEventListener("click", () => {
1685
- 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);
1686
1640
  if (activePresetId === p.id) activePresetId = null;
1687
1641
  renderPresetList();
1688
1642
  updateFillPresetUI();
1689
1643
  });
1690
- item.append(name, del);
1691
- sPresetList.appendChild(item);
1644
+ pill.append(name, x);
1645
+ sPresetList.appendChild(pill);
1692
1646
  });
1693
1647
  }
1694
1648
  renderPresetList();
1695
1649
  sPresetAddBtn.addEventListener("click", () => {
1696
1650
  editingPresetId = null;
1651
+ sPresetFormTitle.textContent = "New Preset";
1697
1652
  sPresetForm.style.display = "flex";
1698
1653
  sPresetName.value = "";
1699
1654
  sPresetPrompt.value = "";
@@ -1729,11 +1684,11 @@ function createOverlay2(options) {
1729
1684
  activePresetId
1730
1685
  };
1731
1686
  saveSettings(s);
1732
- dotWarn.style.display = s.useAI && !s.apiKey ? "block" : "none";
1687
+ dotWarn.style.display = "none";
1733
1688
  openPopover(null);
1734
1689
  });
1735
1690
  async function doFill() {
1736
- const settings = loadSettings();
1691
+ const settings = loadSettings(aiConfig?.provider || "openai");
1737
1692
  const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
1738
1693
  const userText = pPromptEl.value.trim();
1739
1694
  const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
@@ -1750,14 +1705,47 @@ function createOverlay2(options) {
1750
1705
  pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
1751
1706
  return;
1752
1707
  }
1753
- const blockContext = state.selectedBlock ? extractBlockContext(state.selectedBlock) : "";
1754
- fillData = await generateFillData(
1755
- state.fields,
1756
- promptText,
1757
- settings,
1758
- options.systemPrompt,
1759
- blockContext
1760
- );
1708
+ const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
1709
+ let blockContext = `Page: ${document.title}`;
1710
+ if (state.selectedBlock) {
1711
+ state.selectedBlock.querySelectorAll("h1,h2,h3,h4,label,legend").forEach((el) => {
1712
+ const t = el.textContent?.trim();
1713
+ if (t && t.length < 80) blockContext += `
1714
+ ${t}`;
1715
+ });
1716
+ }
1717
+ const fieldDesc = describeFields(state.fields);
1718
+ let userContent = `Form fields:
1719
+ ${fieldDesc}`;
1720
+ if (blockContext) userContent += `
1721
+
1722
+ Page context:
1723
+ ${blockContext}`;
1724
+ if (promptText) userContent += `
1725
+
1726
+ User instructions: ${promptText}`;
1727
+ else userContent += `
1728
+
1729
+ No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
1730
+ const resp = await fetch(`${provider.baseURL}/chat/completions`, {
1731
+ method: "POST",
1732
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.apiKey}` },
1733
+ body: JSON.stringify({
1734
+ model: provider.model,
1735
+ messages: [
1736
+ { role: "system", content: `You are a form-filling assistant. Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys. Fill EVERY field. For select fields pick from listed options EXACTLY. For checkboxes add "checked" boolean. Generate coherent data. No markdown code blocks.` },
1737
+ { role: "user", content: userContent }
1738
+ ],
1739
+ temperature: 0.7,
1740
+ ...settings.provider === "openai" ? { response_format: { type: "json_object" } } : {}
1741
+ })
1742
+ });
1743
+ if (!resp.ok) throw new Error(await resp.text());
1744
+ const data = await resp.json();
1745
+ const content = data.choices?.[0]?.message?.content || "";
1746
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
1747
+ const parsed = JSON.parse(jsonMatch[1].trim());
1748
+ fillData = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
1761
1749
  } else {
1762
1750
  fillData = generateFakeData(state.fields);
1763
1751
  }
@@ -1767,8 +1755,38 @@ function createOverlay2(options) {
1767
1755
  } else {
1768
1756
  setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
1769
1757
  }
1758
+ if (state.selectedBlock) {
1759
+ const el = state.selectedBlock;
1760
+ el.style.transition = "box-shadow 0.8s ease";
1761
+ el.style.animation = "none";
1762
+ const rect = el.getBoundingClientRect();
1763
+ const ripple = document.createElement("div");
1764
+ Object.assign(ripple.style, {
1765
+ position: "fixed",
1766
+ top: `${rect.top}px`,
1767
+ left: `${rect.left}px`,
1768
+ width: `${rect.width}px`,
1769
+ height: `${rect.height}px`,
1770
+ borderRadius: "6px",
1771
+ pointerEvents: "none",
1772
+ zIndex: "2147483644",
1773
+ border: "2px solid rgba(52,211,153,0.6)",
1774
+ boxShadow: "0 0 0 0 rgba(52,211,153,0.4), inset 0 0 20px rgba(52,211,153,0.08)",
1775
+ animation: "none"
1776
+ });
1777
+ document.body.appendChild(ripple);
1778
+ requestAnimationFrame(() => {
1779
+ ripple.style.transition = "box-shadow 0.8s ease, border-color 0.8s ease, opacity 0.8s ease";
1780
+ ripple.style.boxShadow = "0 0 0 8px rgba(52,211,153,0), inset 0 0 0 rgba(52,211,153,0)";
1781
+ ripple.style.borderColor = "rgba(52,211,153,0)";
1782
+ ripple.style.opacity = "0";
1783
+ });
1784
+ setTimeout(() => {
1785
+ ripple.remove();
1786
+ }, 1e3);
1787
+ }
1770
1788
  removeBlockHighlight();
1771
- setTimeout(() => openPopover(null), 600);
1789
+ setTimeout(() => openPopover(null), 800);
1772
1790
  } catch (err) {
1773
1791
  setStatus(cleanError(err), "error");
1774
1792
  } finally {
@@ -1806,6 +1824,12 @@ function createOverlay2(options) {
1806
1824
  }
1807
1825
  }
1808
1826
  document.addEventListener("keydown", handleShortcut);
1827
+ document.addEventListener("keydown", (e) => {
1828
+ if (e.key === "Escape" && currentPopover) {
1829
+ e.preventDefault();
1830
+ openPopover(null);
1831
+ }
1832
+ });
1809
1833
  function destroy() {
1810
1834
  cleanupSelector?.();
1811
1835
  removeBlockHighlight();
@@ -1835,10 +1859,17 @@ async function fill(params) {
1835
1859
  if (fields.length === 0) {
1836
1860
  return { filled: 0, errors: ["No fillable fields found in container"] };
1837
1861
  }
1838
- const fillData = 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);
1839
1869
  return fillFields(fields, fillData);
1840
1870
  }
1841
1871
  export {
1872
+ PROVIDERS,
1842
1873
  fill,
1843
1874
  init
1844
1875
  };