ghostfill 0.1.3 → 0.2.1

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.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ PROVIDERS: () => PROVIDERS,
23
24
  fill: () => fill,
24
25
  init: () => init
25
26
  });
@@ -158,131 +159,10 @@ function describeFields(fields) {
158
159
  if (f.min) desc += `, min: ${f.min}`;
159
160
  if (f.max) desc += `, max: ${f.max}`;
160
161
  if (f.pattern) desc += `, pattern: ${f.pattern}`;
161
- if (f.currentValue) desc += `, current: "${f.currentValue}"`;
162
162
  desc += ")";
163
163
  return desc;
164
164
  }).join("\n");
165
165
  }
166
- function extractBlockContext(container) {
167
- const parts = [];
168
- const pageTitle = document.title;
169
- if (pageTitle) parts.push(`Page: ${pageTitle}`);
170
- const headings = container.querySelectorAll("h1, h2, h3, h4, h5, h6");
171
- headings.forEach((h) => {
172
- const text = h.textContent?.trim();
173
- if (text) parts.push(`Heading: ${text}`);
174
- });
175
- if (headings.length === 0) {
176
- let prev = container;
177
- while (prev) {
178
- prev = prev.previousElementSibling;
179
- if (prev && /^H[1-6]$/.test(prev.tagName)) {
180
- const text = prev.textContent?.trim();
181
- if (text) parts.push(`Section: ${text}`);
182
- break;
183
- }
184
- }
185
- const parent = container.closest("section, article, form, div[class]");
186
- if (parent) {
187
- const parentH = parent.querySelector("h1, h2, h3, h4, h5, h6");
188
- if (parentH?.textContent?.trim()) {
189
- parts.push(`Section: ${parentH.textContent.trim()}`);
190
- }
191
- }
192
- }
193
- const labels = container.querySelectorAll("label, legend, .label, p, span");
194
- const seen = /* @__PURE__ */ new Set();
195
- labels.forEach((el) => {
196
- const text = el.textContent?.trim();
197
- if (text && text.length > 2 && text.length < 100 && !seen.has(text)) {
198
- seen.add(text);
199
- parts.push(text);
200
- }
201
- });
202
- return parts.slice(0, 20).join("\n");
203
- }
204
-
205
- // src/ai.ts
206
- 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.
207
-
208
- Rules:
209
- - Return ONLY a JSON object with a "fields" array of objects, each with "index" and "value" keys
210
- - You MUST fill EVERY field \u2014 do not skip any
211
- - 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.)
212
- - For select/dropdown fields: you MUST pick one of the listed options EXACTLY as written
213
- - For checkboxes: add a "checked" boolean (true or false)
214
- - For radio buttons: only fill one per group, use the option value
215
- - Respect min/max constraints and patterns
216
- - Generate contextually coherent data (same person's name, matching city/state/zip, etc.)
217
- - Use the page context to infer what kind of data makes sense (e.g. a "Create Project" form \u2192 project-related data)
218
- - If no user prompt is given, infer appropriate data from the field labels and page context
219
- - Do NOT wrap the JSON in markdown code blocks \u2014 return raw JSON only`;
220
- async function generateFillData(fields, userPrompt, settings, systemPrompt, blockContext) {
221
- const fieldDescription = describeFields(fields);
222
- const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
223
- let userContent = `Form fields:
224
- ${fieldDescription}`;
225
- if (blockContext) {
226
- userContent += `
227
-
228
- Page context:
229
- ${blockContext}`;
230
- }
231
- if (userPrompt) {
232
- userContent += `
233
-
234
- User instructions: ${userPrompt}`;
235
- } else {
236
- userContent += `
237
-
238
- No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
239
- }
240
- const messages = [
241
- {
242
- role: "system",
243
- content: systemPrompt ? `${systemPrompt}
244
-
245
- ${SYSTEM_PROMPT}` : SYSTEM_PROMPT
246
- },
247
- {
248
- role: "user",
249
- content: userContent
250
- }
251
- ];
252
- const body = {
253
- model: provider.model,
254
- messages,
255
- temperature: 0.7
256
- };
257
- if (settings.provider === "openai") {
258
- body.response_format = { type: "json_object" };
259
- }
260
- const response = await fetch(`${provider.baseURL}/chat/completions`, {
261
- method: "POST",
262
- headers: {
263
- "Content-Type": "application/json",
264
- Authorization: `Bearer ${settings.apiKey}`
265
- },
266
- body: JSON.stringify(body)
267
- });
268
- if (!response.ok) {
269
- const error = await response.text();
270
- throw new Error(`API error (${response.status}): ${error}`);
271
- }
272
- const data = await response.json();
273
- const content = data.choices?.[0]?.message?.content;
274
- if (!content) {
275
- throw new Error("No content in API response");
276
- }
277
- const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
278
- const jsonStr = jsonMatch[1].trim();
279
- const parsed = JSON.parse(jsonStr);
280
- const arr = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
281
- if (!Array.isArray(arr)) {
282
- throw new Error("AI response is not an array of field fills");
283
- }
284
- return arr;
285
- }
286
166
 
287
167
  // src/faker.ts
288
168
  var FIRST_NAMES = ["James", "Sarah", "Michael", "Emma", "Robert", "Olivia", "David", "Sophia", "Daniel", "Isabella", "Ahmed", "Fatima", "Carlos", "Yuki", "Priya"];
@@ -370,7 +250,7 @@ function generateFakeData(fields) {
370
250
  const context = { firstName, lastName, email, company };
371
251
  return fields.map((field, index) => {
372
252
  if (field.type === "checkbox") {
373
- return { index, value: "true", checked: Math.random() > 0.5 };
253
+ return { index, value: "true", checked: true };
374
254
  }
375
255
  const value = generateForField(field, context);
376
256
  return { index, value };
@@ -657,16 +537,53 @@ function startSelection(onSelect, onCancel, ghostfillRoot, highlightColor = "#63
657
537
  var STORAGE_KEY = "ghostfill_settings";
658
538
  var POS_KEY = "ghostfill_pos";
659
539
  var FAB_POS_KEY = "ghostfill_fab_pos";
660
- function loadSettings() {
540
+ function isProvider(value) {
541
+ return value === "openai" || value === "xai" || value === "moonshot";
542
+ }
543
+ function defaultSettings(provider) {
544
+ return {
545
+ apiKey: "",
546
+ provider,
547
+ highlightColor: "#6366f1",
548
+ theme: "dark",
549
+ useAI: false,
550
+ presets: [],
551
+ activePresetId: null
552
+ };
553
+ }
554
+ function sanitizePresets(value) {
555
+ if (!Array.isArray(value)) return [];
556
+ return value.flatMap((item) => {
557
+ if (typeof item === "object" && item !== null && typeof item.id === "string" && typeof item.name === "string" && typeof item.prompt === "string") {
558
+ return [item];
559
+ }
560
+ return [];
561
+ });
562
+ }
563
+ function loadSettings(provider) {
661
564
  try {
662
565
  const raw = localStorage.getItem(STORAGE_KEY);
663
- if (raw) return JSON.parse(raw);
566
+ if (raw) {
567
+ const parsed = JSON.parse(raw);
568
+ return {
569
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : "",
570
+ provider: isProvider(parsed.provider) ? parsed.provider : provider,
571
+ highlightColor: typeof parsed.highlightColor === "string" ? parsed.highlightColor : "#6366f1",
572
+ theme: parsed.theme === "light" ? "light" : "dark",
573
+ useAI: parsed.useAI === true,
574
+ presets: sanitizePresets(parsed.presets),
575
+ activePresetId: typeof parsed.activePresetId === "string" ? parsed.activePresetId : null
576
+ };
577
+ }
664
578
  } catch {
665
579
  }
666
- return { apiKey: "", provider: "openai", highlightColor: "#6366f1", theme: "dark", useAI: false, presets: [], activePresetId: null };
580
+ return defaultSettings(provider);
667
581
  }
668
582
  function saveSettings(s) {
669
- localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
583
+ try {
584
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
585
+ } catch {
586
+ }
670
587
  }
671
588
  function loadPosition() {
672
589
  try {
@@ -677,13 +594,16 @@ function loadPosition() {
677
594
  return null;
678
595
  }
679
596
  function savePosition(x, y) {
680
- localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
597
+ try {
598
+ localStorage.setItem(POS_KEY, JSON.stringify({ x, y }));
599
+ } catch {
600
+ }
681
601
  }
682
602
  function cleanError(err) {
683
603
  const raw = err instanceof Error ? err.message : String(err);
684
604
  const match = raw.match(/"message"\s*:\s*"([^"]+)"/);
685
605
  if (match) return match[1];
686
- const stripped = raw.replace(/^OpenAI API error \(\d+\):\s*/, "");
606
+ const stripped = raw.replace(/^AI API error \(\d+\):\s*/, "");
687
607
  try {
688
608
  const parsed = JSON.parse(stripped);
689
609
  if (parsed?.error?.message) return parsed.error.message;
@@ -748,7 +668,7 @@ var CSS2 = `
748
668
  align-items: center;
749
669
  gap: 2px;
750
670
  background: #18181b;
751
- border-radius: 14px;
671
+ border-radius: 22px;
752
672
  padding: 5px 6px;
753
673
  box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06);
754
674
  user-select: none;
@@ -762,7 +682,7 @@ var CSS2 = `
762
682
  width: 36px;
763
683
  height: 36px;
764
684
  border: none;
765
- border-radius: 10px;
685
+ border-radius: 50%;
766
686
  background: transparent;
767
687
  color: #a1a1aa;
768
688
  cursor: pointer;
@@ -783,20 +703,39 @@ var CSS2 = `
783
703
 
784
704
  .gf-fab {
785
705
  position: fixed; z-index: 2147483646;
786
- width: 44px; height: 44px; border-radius: 50%; border: none;
787
- background: #18181b; color: #a1a1aa; cursor: grab;
706
+ width: 48px; height: 48px; border-radius: 50%; border: none;
707
+ background: #18181b; color: #e4e4e7; cursor: grab;
788
708
  display: none; align-items: center; justify-content: center;
789
709
  pointer-events: auto;
790
- box-shadow: 0 4px 16px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.06);
791
- transition: transform 0.15s, color 0.15s;
710
+ 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);
711
+ transition: color 0.2s, box-shadow 0.3s;
712
+ }
713
+ .gf-fab > svg {
714
+ filter: drop-shadow(0 0 4px rgba(99,102,241,0.5));
715
+ transition: transform 0.2s;
716
+ }
717
+ .gf-fab:hover {
718
+ color: #fff;
719
+ 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);
720
+ }
721
+ .gf-fab:hover > svg {
722
+ animation: gf-ghost-wobble 1.5s ease-in-out infinite;
792
723
  }
793
- .gf-fab:hover { color: #a78bfa; }
794
- .gf-fab:hover > svg { animation: gf-float 1.2s ease-in-out infinite; }
795
724
  .gf-fab.visible { display: flex; }
796
- @keyframes gf-float {
797
- 0%, 100% { transform: translateY(0) rotate(0deg); }
798
- 25% { transform: translateY(-2px) rotate(-5deg); }
799
- 75% { transform: translateY(1px) rotate(5deg); }
725
+
726
+ @keyframes gf-ghost-wobble {
727
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
728
+ 25% { transform: translate(1px, -2px) rotate(4deg); }
729
+ 50% { transform: translate(0, -3px) rotate(-1deg); }
730
+ 75% { transform: translate(-1px, -1px) rotate(-4deg); }
731
+ }
732
+
733
+ /* Success flash on filled block */
734
+ @keyframes gf-fill-success {
735
+ 0% { box-shadow: 0 0 0 0 rgba(99,102,241,0.4); }
736
+ 30% { box-shadow: 0 0 0 6px rgba(99,102,241,0.2); }
737
+ 60% { box-shadow: 0 0 0 12px rgba(52,211,153,0.15); }
738
+ 100% { box-shadow: 0 0 0 0 rgba(52,211,153,0); }
800
739
  }
801
740
 
802
741
  .gf-popover {
@@ -804,7 +743,7 @@ var CSS2 = `
804
743
  background: #1a1a1a; border-radius: 16px; pointer-events: auto;
805
744
  box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
806
745
  display: none; flex-direction: column; overflow: hidden;
807
- min-width: 280px;
746
+ width: 280px;
808
747
  }
809
748
  .gf-popover.open { display: flex; }
810
749
 
@@ -879,7 +818,7 @@ var CSS2 = `
879
818
  display: flex; align-items: center; justify-content: center; gap: 5px;
880
819
  }
881
820
  .gf-save-btn:hover, .gf-fill-btn:hover { background: #4f46e5; }
882
- .gf-fill-btn:disabled { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.25); cursor: not-allowed; }
821
+ .gf-fill-btn:disabled { background: #6366f1; opacity: 0.5; cursor: not-allowed; }
883
822
 
884
823
  .gf-pop-body::-webkit-scrollbar { width: 6px; }
885
824
  .gf-pop-body::-webkit-scrollbar-track { background: transparent; }
@@ -1005,33 +944,42 @@ var CSS2 = `
1005
944
  }
1006
945
  .gf-preset-chip.add:hover { color: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.3); }
1007
946
 
1008
- /* Preset list in settings */
1009
- .gf-preset-list { display: flex; flex-direction: column; gap: 4px; }
1010
- .gf-preset-item {
1011
- display: flex; align-items: center; justify-content: space-between;
1012
- padding: 4px 8px; border-radius: 6px; background: rgba(255,255,255,0.04);
1013
- }
1014
- .gf-preset-item-name { font-size: 12px; color: rgba(255,255,255,0.7); }
1015
- .gf-preset-actions { display: flex; align-items: center; gap: 2px; }
1016
- .gf-preset-edit {
1017
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
1018
- font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
1019
- display: flex; align-items: center;
947
+ /* Preset pills in settings */
948
+ .gf-preset-list { display: flex; flex-wrap: wrap; gap: 6px; }
949
+ .gf-preset-pill {
950
+ display: inline-flex; align-items: center; gap: 4px;
951
+ padding: 4px 10px; border-radius: 20px;
952
+ font-size: 12px; font-weight: 500; cursor: pointer;
953
+ border: 1px solid; transition: all 0.15s;
954
+ }
955
+ .gf-preset-pill .gf-pp-name {
956
+ cursor: pointer; transition: opacity 0.15s;
957
+ }
958
+ .gf-preset-pill .gf-pp-name:hover { opacity: 0.7; }
959
+ .gf-preset-pill .gf-pp-x {
960
+ background: none; border: none; cursor: pointer;
961
+ font-size: 13px; line-height: 1; opacity: 0.4; transition: opacity 0.15s, color 0.15s;
962
+ padding: 0; margin-left: 2px; font-family: inherit;
963
+ }
964
+ .gf-preset-pill .gf-pp-x:hover { opacity: 1; color: #f87171; }
965
+
966
+ /* Preset edit overlay \u2014 takes over the entire settings panel */
967
+ .gf-preset-overlay {
968
+ position: absolute; inset: 0;
969
+ background: #1a1a1a; border-radius: 16px;
970
+ display: none; flex-direction: column;
971
+ z-index: 5;
972
+ }
973
+ .gf-preset-overlay[style*="display: flex"], .gf-preset-overlay[style*="display:flex"] {
974
+ display: flex;
1020
975
  }
1021
- .gf-preset-edit:hover { color: #6366f1; }
1022
- .gf-preset-del {
1023
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
1024
- font-size: 14px; padding: 0 2px; line-height: 1; transition: color 0.15s;
976
+ .gf-preset-overlay-body {
977
+ flex: 1; display: flex; flex-direction: column;
978
+ padding: 0 16px 16px; gap: 10px; overflow-y: auto;
1025
979
  }
1026
- .gf-preset-del:hover { color: #f87171; }
1027
-
1028
- /* Preset add form */
1029
- .gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
1030
- .gf-preset-form-row { display: flex; gap: 4px; }
1031
- .gf-preset-form-row .gf-input { flex: 1; }
1032
- .gf-preset-form-actions { display: flex; gap: 4px; justify-content: flex-end; }
980
+ .gf-preset-form-actions { display: flex; gap: 6px; justify-content: flex-end; }
1033
981
  .gf-preset-form-btn {
1034
- padding: 3px 10px; border: none; border-radius: 6px; font-size: 11px;
982
+ padding: 6px 14px; border: none; border-radius: 8px; font-size: 12px; font-weight: 500;
1035
983
  cursor: pointer; font-family: inherit; transition: background 0.15s;
1036
984
  }
1037
985
  .gf-preset-form-btn.save { background: #6366f1; color: white; }
@@ -1039,22 +987,50 @@ var CSS2 = `
1039
987
  .gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
1040
988
  .gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
1041
989
 
990
+ /* Cycle dots (vertical indicator like Agentation) */
991
+ .gf-cycle-dots {
992
+ display: flex; flex-direction: column; gap: 2px; margin-left: 4px;
993
+ }
994
+ .gf-cycle-dot {
995
+ width: 3px; height: 3px; border-radius: 50%;
996
+ background: rgba(255,255,255,0.2); transition: background 0.2s, transform 0.2s;
997
+ transform: scale(0.67);
998
+ }
999
+ .gf-cycle-dot.active { background: #fff; transform: scale(1); }
1000
+
1042
1001
  /* Help badge */
1043
1002
  .gf-help {
1003
+ position: relative;
1044
1004
  display: inline-flex; align-items: center; justify-content: center;
1045
1005
  width: 14px; height: 14px; border-radius: 50%;
1046
1006
  background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
1047
1007
  cursor: help; flex-shrink: 0;
1048
1008
  }
1009
+ .gf-help-tip {
1010
+ display: none; position: absolute; bottom: calc(100% + 6px); left: 50%;
1011
+ transform: translateX(-50%); padding: 6px 10px;
1012
+ background: #383838; color: rgba(255,255,255,0.7);
1013
+ font-size: 11px; font-weight: 400; line-height: 1.4;
1014
+ border-radius: 8px; white-space: normal; width: 180px; text-align: left;
1015
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3); z-index: 100;
1016
+ }
1017
+ .gf-help-tip.show { display: block; }
1018
+
1019
+ .gf-note {
1020
+ font-size: 11px;
1021
+ color: rgba(255,255,255,0.5);
1022
+ line-height: 1.5;
1023
+ }
1049
1024
  `;
1050
1025
  function createOverlay2(options) {
1051
- const saved = loadSettings();
1052
- if (options.apiKey && !saved.apiKey) {
1053
- saved.apiKey = options.apiKey;
1054
- saveSettings(saved);
1026
+ const aiConfig = options.ai || null;
1027
+ const saved = loadSettings(aiConfig?.provider || "openai");
1028
+ if (options.apiKey) {
1029
+ console.warn(
1030
+ "[ghostfill] Browser API keys are ignored. Configure init({ ai: ... }) and keep provider keys on your backend."
1031
+ );
1055
1032
  }
1056
- if (options.model && !saved.model) saved.model = options.model;
1057
- if (options.baseURL && !saved.baseURL) saved.baseURL = options.baseURL;
1033
+ const backendLabel = aiConfig ? aiConfig.requestFillData ? "Custom secure handler" : aiConfig.endpoint || "/api/ghostfill" : "Configure init({ ai: ... }) to enable AI.";
1058
1034
  const host = document.createElement("div");
1059
1035
  host.id = "ghostfill-root";
1060
1036
  host.style.cssText = "display:contents;";
@@ -1104,7 +1080,7 @@ function createOverlay2(options) {
1104
1080
  dotWarn.className = "gf-dot-warn";
1105
1081
  btnSettings.style.position = "relative";
1106
1082
  btnSettings.appendChild(dotWarn);
1107
- if (!saved.useAI || saved.apiKey) dotWarn.style.display = "none";
1083
+ dotWarn.style.display = "none";
1108
1084
  const divider1 = document.createElement("span");
1109
1085
  divider1.className = "gf-divider";
1110
1086
  const divider2 = document.createElement("span");
@@ -1211,11 +1187,12 @@ function createOverlay2(options) {
1211
1187
  ];
1212
1188
  const settingsPop = document.createElement("div");
1213
1189
  settingsPop.className = "gf-popover";
1190
+ settingsPop.style.position = "fixed";
1214
1191
  settingsPop.innerHTML = `
1215
1192
  <div class="gf-pop-header">
1216
1193
  <h3><span class="gf-slash">/</span>ghostfill</h3>
1217
1194
  <div class="gf-header-right">
1218
- <span class="gf-version">v0.1.0</span>
1195
+ <span class="gf-version">v0.2.1</span>
1219
1196
  <button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
1220
1197
  ${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
1221
1198
  </button>
@@ -1242,53 +1219,83 @@ function createOverlay2(options) {
1242
1219
  <div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
1243
1220
  <div style="display:flex;align-items:center;gap:4px">
1244
1221
  <label class="gf-label" style="margin:0">Provider</label>
1245
- <span class="gf-help" id="gf-s-help" title="">?</span>
1222
+ <span class="gf-help" id="gf-s-help">?<span class="gf-help-tip" id="gf-s-help-tip"></span></span>
1246
1223
  </div>
1247
- <div class="gf-picker" id="gf-s-provider-picker">
1224
+ <div class="gf-picker" id="gf-s-provider-picker" tabindex="0">
1248
1225
  <span class="gf-picker-value" id="gf-s-provider-label">${PROVIDERS[saved.provider]?.label || "OpenAI"}</span>
1226
+ <div class="gf-cycle-dots" id="gf-s-provider-dots">
1227
+ <span class="gf-cycle-dot"></span>
1228
+ <span class="gf-cycle-dot"></span>
1229
+ <span class="gf-cycle-dot"></span>
1230
+ </div>
1249
1231
  </div>
1250
1232
  </div>
1251
1233
  <div class="gf-field">
1252
1234
  <label class="gf-label">API Key</label>
1253
1235
  <input type="password" class="gf-input gf-input-mono" id="gf-s-key" placeholder="sk-..." autocomplete="off" spellcheck="false" />
1254
1236
  </div>
1255
- </div>
1256
- <div class="gf-sep"></div>
1257
- <div class="gf-field">
1258
- <div style="display:flex;align-items:center;justify-content:space-between">
1259
- <label class="gf-label" style="margin:0">Presets</label>
1260
- <button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
1261
- </div>
1262
- <div class="gf-preset-list" id="gf-s-preset-list"></div>
1263
- <div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
1264
- <input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
1265
- <textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
1266
- <div class="gf-preset-form-actions">
1267
- <button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
1268
- <button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
1237
+ <div class="gf-sep"></div>
1238
+ <div class="gf-field">
1239
+ <div style="display:flex;align-items:center;justify-content:space-between">
1240
+ <div style="display:flex;align-items:center;gap:4px">
1241
+ <label class="gf-label" style="margin:0">Presets</label>
1242
+ <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>
1243
+ </div>
1244
+ <button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
1269
1245
  </div>
1246
+ <div class="gf-preset-list" id="gf-s-preset-list"></div>
1270
1247
  </div>
1271
1248
  </div>
1272
1249
  <button class="gf-save-btn" id="gf-s-save">Save</button>
1273
1250
  </div>
1251
+ <!-- Preset edit overlay \u2014 takes over entire panel -->
1252
+ <div class="gf-preset-overlay" id="gf-s-preset-form" style="display:none">
1253
+ <div class="gf-pop-header">
1254
+ <h3 id="gf-s-preset-form-title">New Preset</h3>
1255
+ </div>
1256
+ <div class="gf-preset-overlay-body">
1257
+ <div class="gf-field">
1258
+ <label class="gf-label">Name</label>
1259
+ <input class="gf-input" id="gf-s-preset-name" placeholder="e.g. D365, Healthcare, E-commerce" />
1260
+ </div>
1261
+ <div class="gf-field" style="flex:1;display:flex;flex-direction:column">
1262
+ <label class="gf-label">Prompt</label>
1263
+ <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>
1264
+ </div>
1265
+ <div class="gf-preset-form-actions">
1266
+ <button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
1267
+ <button class="gf-preset-form-btn save" id="gf-s-preset-save">Save Preset</button>
1268
+ </div>
1269
+ </div>
1270
+ </div>
1274
1271
  `;
1275
1272
  shadow.appendChild(settingsPop);
1276
1273
  const sKeyInput = settingsPop.querySelector("#gf-s-key");
1277
1274
  const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
1278
1275
  const sAISection = settingsPop.querySelector("#gf-s-ai-section");
1279
1276
  const sHelpEl = settingsPop.querySelector("#gf-s-help");
1277
+ sKeyInput.value = saved.apiKey || "";
1280
1278
  const sSaveBtn = settingsPop.querySelector("#gf-s-save");
1281
1279
  const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
1282
1280
  const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
1283
1281
  const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
1284
1282
  const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
1285
- sKeyInput.value = saved.apiKey;
1283
+ const sProviderDots = settingsPop.querySelector("#gf-s-provider-dots");
1284
+ const sHelpTip = settingsPop.querySelector("#gf-s-help-tip");
1285
+ const sPresetsHelp = settingsPop.querySelector("#gf-s-presets-help");
1286
1286
  const providerOrder = ["openai", "xai", "moonshot"];
1287
- let selectedProvider = saved.provider || "openai";
1287
+ let selectedProvider = saved.provider || aiConfig?.provider || "openai";
1288
+ function updateProviderDots() {
1289
+ const idx = providerOrder.indexOf(selectedProvider);
1290
+ sProviderDots.querySelectorAll(".gf-cycle-dot").forEach((dot, i) => {
1291
+ dot.classList.toggle("active", i === idx);
1292
+ });
1293
+ }
1288
1294
  function updateProviderDisplay() {
1289
1295
  const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
1290
1296
  sPickerLabel.textContent = `${p.label} (${p.model})`;
1291
- sHelpEl.title = p.helpText;
1297
+ sHelpTip.textContent = p.helpText;
1298
+ updateProviderDots();
1292
1299
  }
1293
1300
  updateProviderDisplay();
1294
1301
  sPickerEl.addEventListener("click", () => {
@@ -1296,6 +1303,24 @@ function createOverlay2(options) {
1296
1303
  selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
1297
1304
  updateProviderDisplay();
1298
1305
  });
1306
+ sHelpEl.addEventListener("click", (e) => {
1307
+ e.stopPropagation();
1308
+ sHelpTip.classList.toggle("show");
1309
+ });
1310
+ sPresetsHelp.addEventListener("click", (e) => {
1311
+ e.stopPropagation();
1312
+ sPresetsHelp.querySelector(".gf-help-tip").classList.toggle("show");
1313
+ });
1314
+ shadow.addEventListener("click", () => {
1315
+ sHelpTip.classList.remove("show");
1316
+ sPresetsHelp.querySelector(".gf-help-tip")?.classList.remove("show");
1317
+ });
1318
+ sPickerEl.addEventListener("keydown", (e) => {
1319
+ if (e.key === "Enter" || e.key === " ") {
1320
+ e.preventDefault();
1321
+ sPickerEl.click();
1322
+ }
1323
+ });
1299
1324
  sUseAIToggle.addEventListener("change", () => {
1300
1325
  sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
1301
1326
  });
@@ -1312,18 +1337,27 @@ function createOverlay2(options) {
1312
1337
  currentTheme = theme;
1313
1338
  const isDark = theme === "dark";
1314
1339
  sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
1315
- const bg = isDark ? "#18181b" : "#ffffff";
1316
- const bgInput = isDark ? "#09090b" : "#f4f4f5";
1317
- const border = isDark ? "#27272a" : "#e4e4e7";
1318
- const text = isDark ? "#fafafa" : "#18181b";
1319
- const textMuted = isDark ? "#a1a1aa" : "#52525b";
1320
- const textDim = isDark ? "#52525b" : "#a1a1aa";
1321
- const btnHoverBg = isDark ? "#27272a" : "#f4f4f5";
1340
+ const bg = isDark ? "#1a1a1a" : "#ffffff";
1341
+ const bgInput = isDark ? "rgba(255,255,255,0.06)" : "#f4f4f5";
1342
+ const border = isDark ? "rgba(255,255,255,0.07)" : "#d4d4d8";
1343
+ const text = isDark ? "#fff" : "#18181b";
1344
+ const textMuted = isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.5)";
1345
+ const textDim = isDark ? "rgba(255,255,255,0.4)" : "rgba(0,0,0,0.35)";
1346
+ const btnHoverBg = isDark ? "#27272a" : "#e4e4e7";
1347
+ const btnActiveBg = isDark ? "#3f3f46" : "#d4d4d8";
1348
+ const presetItemBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)";
1349
+ const presetItemText = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
1350
+ const presetBtnColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.25)";
1351
+ const helpBg = isDark ? "#3f3f46" : "#d4d4d8";
1352
+ const helpColor = isDark ? "#a1a1aa" : "#52525b";
1353
+ const pickerColor = isDark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.75)";
1322
1354
  for (const pop of [settingsPop, promptPop]) {
1323
1355
  pop.style.background = bg;
1324
- 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)";
1356
+ 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)";
1325
1357
  pop.querySelectorAll(".gf-pop-header h3").forEach((el) => el.style.color = text);
1358
+ pop.querySelectorAll(".gf-pop-header").forEach((el) => el.style.borderBottomColor = border);
1326
1359
  pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
1360
+ pop.querySelectorAll(".gf-note").forEach((el) => el.style.color = textMuted);
1327
1361
  pop.querySelectorAll(".gf-input").forEach((el) => {
1328
1362
  el.style.background = bgInput;
1329
1363
  el.style.borderColor = border;
@@ -1337,31 +1371,34 @@ function createOverlay2(options) {
1337
1371
  el.style.background = isDark ? "#27272a" : "#f4f4f5";
1338
1372
  el.style.borderColor = border;
1339
1373
  });
1374
+ pop.querySelectorAll(".gf-help").forEach((el) => {
1375
+ el.style.background = helpBg;
1376
+ el.style.color = helpColor;
1377
+ });
1378
+ pop.querySelectorAll(".gf-picker-value").forEach((el) => el.style.color = pickerColor);
1379
+ pop.querySelectorAll(".gf-theme-btn").forEach((el) => el.style.color = textDim);
1340
1380
  }
1341
1381
  bar.style.background = bg;
1342
- 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)";
1382
+ 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)";
1343
1383
  bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
1344
1384
  btn.style.color = textMuted;
1385
+ const isActive = btn.classList.contains("active");
1386
+ btn.style.background = isActive ? btnActiveBg : "transparent";
1345
1387
  btn.onmouseenter = () => {
1346
1388
  btn.style.background = btnHoverBg;
1347
1389
  btn.style.color = text;
1348
1390
  };
1349
1391
  btn.onmouseleave = () => {
1350
- if (!btn.classList.contains("active")) {
1351
- btn.style.background = "transparent";
1352
- btn.style.color = textMuted;
1353
- }
1392
+ const stillActive = btn.classList.contains("active");
1393
+ btn.style.background = stillActive ? btnActiveBg : "transparent";
1394
+ btn.style.color = stillActive ? text : textMuted;
1354
1395
  };
1355
1396
  });
1356
1397
  bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
1357
1398
  fab.style.background = bg;
1358
1399
  fab.style.color = textMuted;
1359
- 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)";
1400
+ 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)";
1360
1401
  }
1361
- if (currentTheme === "light") applyTheme("light");
1362
- sThemeBtn.addEventListener("click", () => {
1363
- applyTheme(currentTheme === "dark" ? "light" : "dark");
1364
- });
1365
1402
  const promptPop = document.createElement("div");
1366
1403
  promptPop.className = "gf-popover";
1367
1404
  promptPop.style.width = "300px";
@@ -1378,6 +1415,7 @@ function createOverlay2(options) {
1378
1415
  <label class="gf-label" style="margin:0">Preset</label>
1379
1416
  <div class="gf-picker" id="gf-p-preset-picker">
1380
1417
  <span class="gf-picker-value" id="gf-p-preset-label">None</span>
1418
+ <div class="gf-cycle-dots" id="gf-p-preset-dots"></div>
1381
1419
  </div>
1382
1420
  </div>
1383
1421
  <div class="gf-field" id="gf-p-prompt-wrap">
@@ -1396,6 +1434,7 @@ function createOverlay2(options) {
1396
1434
  const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
1397
1435
  const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
1398
1436
  const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
1437
+ const pPresetDots = promptPop.querySelector("#gf-p-preset-dots");
1399
1438
  const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
1400
1439
  const pPromptEl = promptPop.querySelector("#gf-p-prompt");
1401
1440
  const pFillBtn = promptPop.querySelector("#gf-p-fill");
@@ -1412,10 +1451,18 @@ function createOverlay2(options) {
1412
1451
  pPresetRow.style.display = "flex";
1413
1452
  const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
1414
1453
  pPresetLabel.textContent = active ? active.name : "None";
1454
+ const totalOptions = presets.length + 1;
1455
+ const activeIdx = activePresetId ? presets.findIndex((p) => p.id === activePresetId) + 1 : 0;
1456
+ pPresetDots.innerHTML = "";
1457
+ for (let i = 0; i < totalOptions; i++) {
1458
+ const dot = document.createElement("span");
1459
+ dot.className = `gf-cycle-dot${i === activeIdx ? " active" : ""}`;
1460
+ pPresetDots.appendChild(dot);
1461
+ }
1415
1462
  pPromptWrap.style.display = active ? "none" : "flex";
1416
1463
  }
1417
1464
  function persistActivePreset() {
1418
- const s = loadSettings();
1465
+ const s = loadSettings(aiConfig?.provider || "openai");
1419
1466
  s.activePresetId = activePresetId;
1420
1467
  saveSettings(s);
1421
1468
  }
@@ -1435,6 +1482,10 @@ function createOverlay2(options) {
1435
1482
  updateFillPresetUI();
1436
1483
  });
1437
1484
  updateFillPresetUI();
1485
+ if (currentTheme === "light") applyTheme("light");
1486
+ sThemeBtn.addEventListener("click", () => {
1487
+ applyTheme(currentTheme === "dark" ? "light" : "dark");
1488
+ });
1438
1489
  function setStatus(text, type) {
1439
1490
  pStatusEl.textContent = text;
1440
1491
  pStatusEl.className = `gf-status ${type}`;
@@ -1470,7 +1521,7 @@ function createOverlay2(options) {
1470
1521
  if (name === "settings") {
1471
1522
  settingsPop.classList.add("open");
1472
1523
  btnSettings.classList.add("active");
1473
- sKeyInput.focus();
1524
+ (aiConfig && sUseAIToggle.checked ? sPickerEl : sSaveBtn).focus();
1474
1525
  } else if (name === "prompt") {
1475
1526
  promptPop.classList.add("open");
1476
1527
  btnFill.classList.add("active");
@@ -1608,6 +1659,7 @@ function createOverlay2(options) {
1608
1659
  badge.textContent = String(fields.length);
1609
1660
  badge.style.display = "flex";
1610
1661
  btnFill.disabled = false;
1662
+ openPopover("prompt");
1611
1663
  },
1612
1664
  () => {
1613
1665
  state.selecting = false;
@@ -1667,7 +1719,16 @@ function createOverlay2(options) {
1667
1719
  document.removeEventListener("mouseup", onUp);
1668
1720
  fabDragState.dragging = false;
1669
1721
  if (fabDragState.moved) {
1670
- localStorage.setItem(FAB_POS_KEY, JSON.stringify({ x: fab.getBoundingClientRect().left, y: fab.getBoundingClientRect().top }));
1722
+ try {
1723
+ localStorage.setItem(
1724
+ FAB_POS_KEY,
1725
+ JSON.stringify({
1726
+ x: fab.getBoundingClientRect().left,
1727
+ y: fab.getBoundingClientRect().top
1728
+ })
1729
+ );
1730
+ } catch {
1731
+ }
1671
1732
  }
1672
1733
  };
1673
1734
  document.addEventListener("mousemove", onMove);
@@ -1690,58 +1751,67 @@ function createOverlay2(options) {
1690
1751
  });
1691
1752
  const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
1692
1753
  const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
1754
+ const sPresetFormTitle = settingsPop.querySelector("#gf-s-preset-form-title");
1693
1755
  const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
1694
1756
  const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
1695
1757
  const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
1696
1758
  const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
1697
1759
  const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
1698
1760
  let editingPresetId = null;
1761
+ const PILL_COLORS = [
1762
+ { bg: "rgba(99,102,241,0.15)", border: "rgba(99,102,241,0.3)", text: "#a5b4fc" },
1763
+ { bg: "rgba(52,211,153,0.12)", border: "rgba(52,211,153,0.25)", text: "#6ee7b7" },
1764
+ { bg: "rgba(251,146,60,0.12)", border: "rgba(251,146,60,0.25)", text: "#fdba74" },
1765
+ { bg: "rgba(244,114,182,0.12)", border: "rgba(244,114,182,0.25)", text: "#f9a8d4" },
1766
+ { bg: "rgba(56,189,248,0.12)", border: "rgba(56,189,248,0.25)", text: "#7dd3fc" },
1767
+ { bg: "rgba(163,130,255,0.12)", border: "rgba(163,130,255,0.25)", text: "#c4b5fd" },
1768
+ { bg: "rgba(250,204,21,0.12)", border: "rgba(250,204,21,0.25)", text: "#fde68a" }
1769
+ ];
1699
1770
  function renderPresetList() {
1700
1771
  sPresetList.innerHTML = "";
1701
- presets.forEach((p) => {
1702
- const item = document.createElement("div");
1703
- item.className = "gf-preset-item";
1772
+ presets.forEach((p, i) => {
1773
+ const c = PILL_COLORS[i % PILL_COLORS.length];
1774
+ const pill = document.createElement("span");
1775
+ pill.className = "gf-preset-pill";
1776
+ pill.style.background = c.bg;
1777
+ pill.style.borderColor = c.border;
1778
+ pill.style.color = c.text;
1704
1779
  const name = document.createElement("span");
1705
- name.className = "gf-preset-item-name";
1780
+ name.className = "gf-pp-name";
1706
1781
  name.textContent = p.name;
1707
- name.style.cursor = "pointer";
1782
+ name.title = "Click to edit";
1708
1783
  name.addEventListener("click", () => {
1709
1784
  editingPresetId = p.id;
1785
+ sPresetFormTitle.textContent = "Edit Preset";
1710
1786
  sPresetForm.style.display = "flex";
1711
1787
  sPresetName.value = p.name;
1712
1788
  sPresetPrompt.value = p.prompt;
1713
1789
  sPresetName.focus();
1714
1790
  });
1715
- const actions = document.createElement("div");
1716
- actions.className = "gf-preset-actions";
1717
- const edit = document.createElement("button");
1718
- edit.className = "gf-preset-edit";
1719
- 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>';
1720
- edit.title = "Edit preset";
1721
- edit.addEventListener("click", () => {
1722
- editingPresetId = p.id;
1723
- sPresetForm.style.display = "flex";
1724
- sPresetName.value = p.name;
1725
- sPresetPrompt.value = p.prompt;
1726
- sPresetName.focus();
1727
- });
1728
- const del = document.createElement("button");
1729
- del.className = "gf-preset-del";
1730
- del.innerHTML = "&times;";
1731
- del.addEventListener("click", () => {
1732
- presets = presets.filter((x) => x.id !== p.id);
1791
+ const x = document.createElement("button");
1792
+ x.className = "gf-pp-x";
1793
+ x.innerHTML = "&times;";
1794
+ x.style.color = c.text;
1795
+ x.title = "Delete";
1796
+ x.addEventListener("click", (e) => {
1797
+ e.stopPropagation();
1798
+ presets = presets.filter((v) => v.id !== p.id);
1733
1799
  if (activePresetId === p.id) activePresetId = null;
1734
1800
  renderPresetList();
1735
1801
  updateFillPresetUI();
1802
+ const s = loadSettings(aiConfig?.provider || "openai");
1803
+ s.presets = presets;
1804
+ s.activePresetId = activePresetId;
1805
+ saveSettings(s);
1736
1806
  });
1737
- actions.append(edit, del);
1738
- item.append(name, actions);
1739
- sPresetList.appendChild(item);
1807
+ pill.append(name, x);
1808
+ sPresetList.appendChild(pill);
1740
1809
  });
1741
1810
  }
1742
1811
  renderPresetList();
1743
1812
  sPresetAddBtn.addEventListener("click", () => {
1744
1813
  editingPresetId = null;
1814
+ sPresetFormTitle.textContent = "New Preset";
1745
1815
  sPresetForm.style.display = "flex";
1746
1816
  sPresetName.value = "";
1747
1817
  sPresetPrompt.value = "";
@@ -1777,11 +1847,11 @@ function createOverlay2(options) {
1777
1847
  activePresetId
1778
1848
  };
1779
1849
  saveSettings(s);
1780
- dotWarn.style.display = s.useAI && !s.apiKey ? "block" : "none";
1850
+ dotWarn.style.display = "none";
1781
1851
  openPopover(null);
1782
1852
  });
1783
1853
  async function doFill() {
1784
- const settings = loadSettings();
1854
+ const settings = loadSettings(aiConfig?.provider || "openai");
1785
1855
  const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
1786
1856
  const userText = pPromptEl.value.trim();
1787
1857
  const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
@@ -1798,14 +1868,47 @@ function createOverlay2(options) {
1798
1868
  pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
1799
1869
  return;
1800
1870
  }
1801
- const blockContext = state.selectedBlock ? extractBlockContext(state.selectedBlock) : "";
1802
- fillData = await generateFillData(
1803
- state.fields,
1804
- promptText,
1805
- settings,
1806
- options.systemPrompt,
1807
- blockContext
1808
- );
1871
+ const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
1872
+ let blockContext = `Page: ${document.title}`;
1873
+ if (state.selectedBlock) {
1874
+ state.selectedBlock.querySelectorAll("h1,h2,h3,h4,label,legend").forEach((el) => {
1875
+ const t = el.textContent?.trim();
1876
+ if (t && t.length < 80) blockContext += `
1877
+ ${t}`;
1878
+ });
1879
+ }
1880
+ const fieldDesc = describeFields(state.fields);
1881
+ let userContent = `Form fields:
1882
+ ${fieldDesc}`;
1883
+ if (blockContext) userContent += `
1884
+
1885
+ Page context:
1886
+ ${blockContext}`;
1887
+ if (promptText) userContent += `
1888
+
1889
+ User instructions: ${promptText}`;
1890
+ else userContent += `
1891
+
1892
+ No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
1893
+ const resp = await fetch(`${provider.baseURL}/chat/completions`, {
1894
+ method: "POST",
1895
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.apiKey}` },
1896
+ body: JSON.stringify({
1897
+ model: provider.model,
1898
+ messages: [
1899
+ { 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.` },
1900
+ { role: "user", content: userContent }
1901
+ ],
1902
+ temperature: 0.7,
1903
+ ...settings.provider === "openai" ? { response_format: { type: "json_object" } } : {}
1904
+ })
1905
+ });
1906
+ if (!resp.ok) throw new Error(await resp.text());
1907
+ const data = await resp.json();
1908
+ const content = data.choices?.[0]?.message?.content || "";
1909
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
1910
+ const parsed = JSON.parse(jsonMatch[1].trim());
1911
+ fillData = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
1809
1912
  } else {
1810
1913
  fillData = generateFakeData(state.fields);
1811
1914
  }
@@ -1815,8 +1918,38 @@ function createOverlay2(options) {
1815
1918
  } else {
1816
1919
  setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
1817
1920
  }
1921
+ if (state.selectedBlock) {
1922
+ const el = state.selectedBlock;
1923
+ el.style.transition = "box-shadow 0.8s ease";
1924
+ el.style.animation = "none";
1925
+ const rect = el.getBoundingClientRect();
1926
+ const ripple = document.createElement("div");
1927
+ Object.assign(ripple.style, {
1928
+ position: "fixed",
1929
+ top: `${rect.top}px`,
1930
+ left: `${rect.left}px`,
1931
+ width: `${rect.width}px`,
1932
+ height: `${rect.height}px`,
1933
+ borderRadius: "6px",
1934
+ pointerEvents: "none",
1935
+ zIndex: "2147483644",
1936
+ border: "2px solid rgba(52,211,153,0.6)",
1937
+ boxShadow: "0 0 0 0 rgba(52,211,153,0.4), inset 0 0 20px rgba(52,211,153,0.08)",
1938
+ animation: "none"
1939
+ });
1940
+ document.body.appendChild(ripple);
1941
+ requestAnimationFrame(() => {
1942
+ ripple.style.transition = "box-shadow 0.8s ease, border-color 0.8s ease, opacity 0.8s ease";
1943
+ ripple.style.boxShadow = "0 0 0 8px rgba(52,211,153,0), inset 0 0 0 rgba(52,211,153,0)";
1944
+ ripple.style.borderColor = "rgba(52,211,153,0)";
1945
+ ripple.style.opacity = "0";
1946
+ });
1947
+ setTimeout(() => {
1948
+ ripple.remove();
1949
+ }, 1e3);
1950
+ }
1818
1951
  removeBlockHighlight();
1819
- setTimeout(() => openPopover(null), 600);
1952
+ setTimeout(() => openPopover(null), 800);
1820
1953
  } catch (err) {
1821
1954
  setStatus(cleanError(err), "error");
1822
1955
  } finally {
@@ -1854,6 +1987,18 @@ function createOverlay2(options) {
1854
1987
  }
1855
1988
  }
1856
1989
  document.addEventListener("keydown", handleShortcut);
1990
+ document.addEventListener("keydown", (e) => {
1991
+ if (e.key !== "Escape") return;
1992
+ if (currentPopover) {
1993
+ e.preventDefault();
1994
+ openPopover(null);
1995
+ return;
1996
+ }
1997
+ if (state.active) {
1998
+ e.preventDefault();
1999
+ btnMinimize.click();
2000
+ }
2001
+ });
1857
2002
  function destroy() {
1858
2003
  cleanupSelector?.();
1859
2004
  removeBlockHighlight();
@@ -1863,6 +2008,94 @@ function createOverlay2(options) {
1863
2008
  return { state, destroy };
1864
2009
  }
1865
2010
 
2011
+ // src/ai.ts
2012
+ function isRecord(value) {
2013
+ return typeof value === "object" && value !== null;
2014
+ }
2015
+ function toFieldFillData(value) {
2016
+ if (!isRecord(value)) {
2017
+ throw new Error("AI response item is not an object");
2018
+ }
2019
+ const index = value.index;
2020
+ const rawValue = value.value;
2021
+ const checked = value.checked;
2022
+ if (typeof index !== "number" || !Number.isInteger(index)) {
2023
+ throw new Error("AI response item is missing a numeric index");
2024
+ }
2025
+ if (typeof rawValue !== "string") {
2026
+ throw new Error("AI response item is missing a string value");
2027
+ }
2028
+ return {
2029
+ index,
2030
+ value: rawValue,
2031
+ checked: typeof checked === "boolean" ? checked : void 0
2032
+ };
2033
+ }
2034
+ function toPromptFields(fields) {
2035
+ return fields.map((field, index) => ({
2036
+ index,
2037
+ type: field.type,
2038
+ name: field.name,
2039
+ label: field.label,
2040
+ options: field.options,
2041
+ required: field.required,
2042
+ min: field.min,
2043
+ max: field.max,
2044
+ pattern: field.pattern
2045
+ }));
2046
+ }
2047
+ function parseFillDataPayload(payload) {
2048
+ if (typeof payload === "string") {
2049
+ const jsonMatch = payload.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, payload];
2050
+ return parseFillDataPayload(JSON.parse(jsonMatch[1].trim()));
2051
+ }
2052
+ if (Array.isArray(payload)) {
2053
+ return payload.map(toFieldFillData);
2054
+ }
2055
+ if (isRecord(payload)) {
2056
+ if (Array.isArray(payload.choices)) {
2057
+ const content = payload.choices[0]?.message;
2058
+ if (typeof content?.content === "string") {
2059
+ return parseFillDataPayload(content.content);
2060
+ }
2061
+ }
2062
+ const candidate = payload.fields ?? payload.data ?? payload.items ?? payload.result;
2063
+ if (Array.isArray(candidate)) {
2064
+ return candidate.map(toFieldFillData);
2065
+ }
2066
+ }
2067
+ throw new Error("AI response is not an array of field fills");
2068
+ }
2069
+ function createRequest(fields, userPrompt, provider, systemPrompt) {
2070
+ return {
2071
+ provider,
2072
+ prompt: userPrompt,
2073
+ systemPrompt,
2074
+ fields: toPromptFields(fields)
2075
+ };
2076
+ }
2077
+ async function generateFillData(fields, userPrompt, provider, transport, systemPrompt) {
2078
+ const request = createRequest(fields, userPrompt, provider, systemPrompt);
2079
+ if (transport.requestFillData) {
2080
+ return transport.requestFillData(request);
2081
+ }
2082
+ const endpoint = transport.endpoint ?? "/api/ghostfill";
2083
+ const response = await fetch(endpoint, {
2084
+ method: "POST",
2085
+ headers: {
2086
+ "Content-Type": "application/json"
2087
+ },
2088
+ body: JSON.stringify(request)
2089
+ });
2090
+ if (!response.ok) {
2091
+ const error = await response.text();
2092
+ throw new Error(`AI API error (${response.status}): ${error}`);
2093
+ }
2094
+ const contentType = response.headers.get("content-type") || "";
2095
+ const payload = contentType.includes("application/json") ? await response.json() : await response.text();
2096
+ return parseFillDataPayload(payload);
2097
+ }
2098
+
1866
2099
  // src/index.ts
1867
2100
  var instance = null;
1868
2101
  function init(options = {}) {
@@ -1883,11 +2116,18 @@ async function fill(params) {
1883
2116
  if (fields.length === 0) {
1884
2117
  return { filled: 0, errors: ["No fillable fields found in container"] };
1885
2118
  }
1886
- const fillData = generateFakeData(fields);
2119
+ const fillData = params.ai ? await generateFillData(
2120
+ fields,
2121
+ params.prompt || "",
2122
+ params.provider || params.ai.provider || "openai",
2123
+ params.ai,
2124
+ params.systemPrompt
2125
+ ) : generateFakeData(fields);
1887
2126
  return fillFields(fields, fillData);
1888
2127
  }
1889
2128
  // Annotate the CommonJS export names for ESM import in node:
1890
2129
  0 && (module.exports = {
2130
+ PROVIDERS,
1891
2131
  fill,
1892
2132
  init
1893
2133
  });