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.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;
@@ -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,26 +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);
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;
1013
975
  }
1014
- .gf-preset-item-name { font-size: 12px; color: rgba(255,255,255,0.7); }
1015
- .gf-preset-del {
1016
- background: none; border: none; color: rgba(255,255,255,0.25); cursor: pointer;
1017
- 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;
1018
979
  }
1019
- .gf-preset-del:hover { color: #f87171; }
1020
-
1021
- /* Preset add form */
1022
- .gf-preset-form { display: flex; flex-direction: column; gap: 6px; }
1023
- .gf-preset-form-row { display: flex; gap: 4px; }
1024
- .gf-preset-form-row .gf-input { flex: 1; }
1025
- .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; }
1026
981
  .gf-preset-form-btn {
1027
- 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;
1028
983
  cursor: pointer; font-family: inherit; transition: background 0.15s;
1029
984
  }
1030
985
  .gf-preset-form-btn.save { background: #6366f1; color: white; }
@@ -1032,22 +987,50 @@ var CSS2 = `
1032
987
  .gf-preset-form-btn.cancel { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.5); }
1033
988
  .gf-preset-form-btn.cancel:hover { background: rgba(255,255,255,0.1); }
1034
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
+
1035
1001
  /* Help badge */
1036
1002
  .gf-help {
1003
+ position: relative;
1037
1004
  display: inline-flex; align-items: center; justify-content: center;
1038
1005
  width: 14px; height: 14px; border-radius: 50%;
1039
1006
  background: #3f3f46; color: #a1a1aa; font-size: 9px; font-weight: 700;
1040
1007
  cursor: help; flex-shrink: 0;
1041
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
+ }
1042
1024
  `;
1043
1025
  function createOverlay2(options) {
1044
- const saved = loadSettings();
1045
- if (options.apiKey && !saved.apiKey) {
1046
- saved.apiKey = options.apiKey;
1047
- 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
+ );
1048
1032
  }
1049
- if (options.model && !saved.model) saved.model = options.model;
1050
- 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.";
1051
1034
  const host = document.createElement("div");
1052
1035
  host.id = "ghostfill-root";
1053
1036
  host.style.cssText = "display:contents;";
@@ -1097,7 +1080,7 @@ function createOverlay2(options) {
1097
1080
  dotWarn.className = "gf-dot-warn";
1098
1081
  btnSettings.style.position = "relative";
1099
1082
  btnSettings.appendChild(dotWarn);
1100
- if (!saved.useAI || saved.apiKey) dotWarn.style.display = "none";
1083
+ dotWarn.style.display = "none";
1101
1084
  const divider1 = document.createElement("span");
1102
1085
  divider1.className = "gf-divider";
1103
1086
  const divider2 = document.createElement("span");
@@ -1204,11 +1187,12 @@ function createOverlay2(options) {
1204
1187
  ];
1205
1188
  const settingsPop = document.createElement("div");
1206
1189
  settingsPop.className = "gf-popover";
1190
+ settingsPop.style.position = "fixed";
1207
1191
  settingsPop.innerHTML = `
1208
1192
  <div class="gf-pop-header">
1209
1193
  <h3><span class="gf-slash">/</span>ghostfill</h3>
1210
1194
  <div class="gf-header-right">
1211
- <span class="gf-version">v0.1.0</span>
1195
+ <span class="gf-version">v0.1.3</span>
1212
1196
  <button class="gf-theme-btn" id="gf-s-theme" title="Toggle theme">
1213
1197
  ${saved.theme === "dark" ? ICONS.sun : ICONS.moon}
1214
1198
  </button>
@@ -1235,10 +1219,15 @@ function createOverlay2(options) {
1235
1219
  <div class="gf-field" style="flex-direction:row;align-items:center;justify-content:space-between">
1236
1220
  <div style="display:flex;align-items:center;gap:4px">
1237
1221
  <label class="gf-label" style="margin:0">Provider</label>
1238
- <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>
1239
1223
  </div>
1240
- <div class="gf-picker" id="gf-s-provider-picker">
1224
+ <div class="gf-picker" id="gf-s-provider-picker" tabindex="0">
1241
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>
1242
1231
  </div>
1243
1232
  </div>
1244
1233
  <div class="gf-field">
@@ -1249,39 +1238,64 @@ function createOverlay2(options) {
1249
1238
  <div class="gf-sep"></div>
1250
1239
  <div class="gf-field">
1251
1240
  <div style="display:flex;align-items:center;justify-content:space-between">
1252
- <label class="gf-label" style="margin:0">Presets</label>
1241
+ <div style="display:flex;align-items:center;gap:4px">
1242
+ <label class="gf-label" style="margin:0">Presets</label>
1243
+ <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>
1244
+ </div>
1253
1245
  <button class="gf-preset-chip add" id="gf-s-preset-add" style="font-size:10px;padding:2px 6px">+ Add</button>
1254
1246
  </div>
1255
1247
  <div class="gf-preset-list" id="gf-s-preset-list"></div>
1256
- <div class="gf-preset-form" id="gf-s-preset-form" style="display:none">
1257
- <input class="gf-input" id="gf-s-preset-name" placeholder="Name (e.g. D365)" />
1258
- <textarea class="gf-input" id="gf-s-preset-prompt" placeholder="Prompt context..." rows="2" style="min-height:40px"></textarea>
1259
- <div class="gf-preset-form-actions">
1260
- <button class="gf-preset-form-btn cancel" id="gf-s-preset-cancel">Cancel</button>
1261
- <button class="gf-preset-form-btn save" id="gf-s-preset-save">Save</button>
1262
- </div>
1263
- </div>
1264
1248
  </div>
1265
1249
  <button class="gf-save-btn" id="gf-s-save">Save</button>
1266
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>
1267
1271
  `;
1268
1272
  shadow.appendChild(settingsPop);
1269
1273
  const sKeyInput = settingsPop.querySelector("#gf-s-key");
1270
1274
  const sUseAIToggle = settingsPop.querySelector("#gf-s-useai");
1271
1275
  const sAISection = settingsPop.querySelector("#gf-s-ai-section");
1272
1276
  const sHelpEl = settingsPop.querySelector("#gf-s-help");
1277
+ sKeyInput.value = saved.apiKey || "";
1273
1278
  const sSaveBtn = settingsPop.querySelector("#gf-s-save");
1274
1279
  const sThemeBtn = settingsPop.querySelector("#gf-s-theme");
1275
1280
  const sColorsDiv = settingsPop.querySelector("#gf-s-colors");
1276
1281
  const sPickerEl = settingsPop.querySelector("#gf-s-provider-picker");
1277
1282
  const sPickerLabel = settingsPop.querySelector("#gf-s-provider-label");
1278
- 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");
1279
1286
  const providerOrder = ["openai", "xai", "moonshot"];
1280
- 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
+ }
1281
1294
  function updateProviderDisplay() {
1282
1295
  const p = PROVIDERS[selectedProvider] || PROVIDERS.openai;
1283
1296
  sPickerLabel.textContent = `${p.label} (${p.model})`;
1284
- sHelpEl.title = p.helpText;
1297
+ sHelpTip.textContent = p.helpText;
1298
+ updateProviderDots();
1285
1299
  }
1286
1300
  updateProviderDisplay();
1287
1301
  sPickerEl.addEventListener("click", () => {
@@ -1289,6 +1303,24 @@ function createOverlay2(options) {
1289
1303
  selectedProvider = providerOrder[(idx + 1) % providerOrder.length];
1290
1304
  updateProviderDisplay();
1291
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
+ });
1292
1324
  sUseAIToggle.addEventListener("change", () => {
1293
1325
  sAISection.style.display = sUseAIToggle.checked ? "flex" : "none";
1294
1326
  });
@@ -1305,18 +1337,27 @@ function createOverlay2(options) {
1305
1337
  currentTheme = theme;
1306
1338
  const isDark = theme === "dark";
1307
1339
  sThemeBtn.innerHTML = isDark ? ICONS.sun : ICONS.moon;
1308
- const bg = isDark ? "#18181b" : "#ffffff";
1309
- const bgInput = isDark ? "#09090b" : "#f4f4f5";
1310
- const border = isDark ? "#27272a" : "#e4e4e7";
1311
- const text = isDark ? "#fafafa" : "#18181b";
1312
- const textMuted = isDark ? "#a1a1aa" : "#52525b";
1313
- const textDim = isDark ? "#52525b" : "#a1a1aa";
1314
- 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)";
1315
1354
  for (const pop of [settingsPop, promptPop]) {
1316
1355
  pop.style.background = bg;
1317
- 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)";
1318
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);
1319
1359
  pop.querySelectorAll(".gf-label").forEach((el) => el.style.color = textMuted);
1360
+ pop.querySelectorAll(".gf-note").forEach((el) => el.style.color = textMuted);
1320
1361
  pop.querySelectorAll(".gf-input").forEach((el) => {
1321
1362
  el.style.background = bgInput;
1322
1363
  el.style.borderColor = border;
@@ -1330,31 +1371,34 @@ function createOverlay2(options) {
1330
1371
  el.style.background = isDark ? "#27272a" : "#f4f4f5";
1331
1372
  el.style.borderColor = border;
1332
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);
1333
1380
  }
1334
1381
  bar.style.background = bg;
1335
- 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)";
1336
1383
  bar.querySelectorAll(".gf-bar-btn").forEach((btn) => {
1337
1384
  btn.style.color = textMuted;
1385
+ const isActive = btn.classList.contains("active");
1386
+ btn.style.background = isActive ? btnActiveBg : "transparent";
1338
1387
  btn.onmouseenter = () => {
1339
1388
  btn.style.background = btnHoverBg;
1340
1389
  btn.style.color = text;
1341
1390
  };
1342
1391
  btn.onmouseleave = () => {
1343
- if (!btn.classList.contains("active")) {
1344
- btn.style.background = "transparent";
1345
- btn.style.color = textMuted;
1346
- }
1392
+ const stillActive = btn.classList.contains("active");
1393
+ btn.style.background = stillActive ? btnActiveBg : "transparent";
1394
+ btn.style.color = stillActive ? text : textMuted;
1347
1395
  };
1348
1396
  });
1349
1397
  bar.querySelectorAll(".gf-divider").forEach((el) => el.style.background = border);
1350
1398
  fab.style.background = bg;
1351
1399
  fab.style.color = textMuted;
1352
- 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)";
1353
1401
  }
1354
- if (currentTheme === "light") applyTheme("light");
1355
- sThemeBtn.addEventListener("click", () => {
1356
- applyTheme(currentTheme === "dark" ? "light" : "dark");
1357
- });
1358
1402
  const promptPop = document.createElement("div");
1359
1403
  promptPop.className = "gf-popover";
1360
1404
  promptPop.style.width = "300px";
@@ -1371,6 +1415,7 @@ function createOverlay2(options) {
1371
1415
  <label class="gf-label" style="margin:0">Preset</label>
1372
1416
  <div class="gf-picker" id="gf-p-preset-picker">
1373
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>
1374
1419
  </div>
1375
1420
  </div>
1376
1421
  <div class="gf-field" id="gf-p-prompt-wrap">
@@ -1389,6 +1434,7 @@ function createOverlay2(options) {
1389
1434
  const pPresetRow = promptPop.querySelector("#gf-p-preset-row");
1390
1435
  const pPresetPicker = promptPop.querySelector("#gf-p-preset-picker");
1391
1436
  const pPresetLabel = promptPop.querySelector("#gf-p-preset-label");
1437
+ const pPresetDots = promptPop.querySelector("#gf-p-preset-dots");
1392
1438
  const pPromptWrap = promptPop.querySelector("#gf-p-prompt-wrap");
1393
1439
  const pPromptEl = promptPop.querySelector("#gf-p-prompt");
1394
1440
  const pFillBtn = promptPop.querySelector("#gf-p-fill");
@@ -1405,10 +1451,18 @@ function createOverlay2(options) {
1405
1451
  pPresetRow.style.display = "flex";
1406
1452
  const active = activePresetId ? presets.find((p) => p.id === activePresetId) : null;
1407
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
+ }
1408
1462
  pPromptWrap.style.display = active ? "none" : "flex";
1409
1463
  }
1410
1464
  function persistActivePreset() {
1411
- const s = loadSettings();
1465
+ const s = loadSettings(aiConfig?.provider || "openai");
1412
1466
  s.activePresetId = activePresetId;
1413
1467
  saveSettings(s);
1414
1468
  }
@@ -1428,6 +1482,10 @@ function createOverlay2(options) {
1428
1482
  updateFillPresetUI();
1429
1483
  });
1430
1484
  updateFillPresetUI();
1485
+ if (currentTheme === "light") applyTheme("light");
1486
+ sThemeBtn.addEventListener("click", () => {
1487
+ applyTheme(currentTheme === "dark" ? "light" : "dark");
1488
+ });
1431
1489
  function setStatus(text, type) {
1432
1490
  pStatusEl.textContent = text;
1433
1491
  pStatusEl.className = `gf-status ${type}`;
@@ -1463,7 +1521,7 @@ function createOverlay2(options) {
1463
1521
  if (name === "settings") {
1464
1522
  settingsPop.classList.add("open");
1465
1523
  btnSettings.classList.add("active");
1466
- sKeyInput.focus();
1524
+ (aiConfig && sUseAIToggle.checked ? sPickerEl : sSaveBtn).focus();
1467
1525
  } else if (name === "prompt") {
1468
1526
  promptPop.classList.add("open");
1469
1527
  btnFill.classList.add("active");
@@ -1601,6 +1659,7 @@ function createOverlay2(options) {
1601
1659
  badge.textContent = String(fields.length);
1602
1660
  badge.style.display = "flex";
1603
1661
  btnFill.disabled = false;
1662
+ openPopover("prompt");
1604
1663
  },
1605
1664
  () => {
1606
1665
  state.selecting = false;
@@ -1660,7 +1719,16 @@ function createOverlay2(options) {
1660
1719
  document.removeEventListener("mouseup", onUp);
1661
1720
  fabDragState.dragging = false;
1662
1721
  if (fabDragState.moved) {
1663
- 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
+ }
1664
1732
  }
1665
1733
  };
1666
1734
  document.addEventListener("mousemove", onMove);
@@ -1683,44 +1751,63 @@ function createOverlay2(options) {
1683
1751
  });
1684
1752
  const sPresetList = settingsPop.querySelector("#gf-s-preset-list");
1685
1753
  const sPresetForm = settingsPop.querySelector("#gf-s-preset-form");
1754
+ const sPresetFormTitle = settingsPop.querySelector("#gf-s-preset-form-title");
1686
1755
  const sPresetAddBtn = settingsPop.querySelector("#gf-s-preset-add");
1687
1756
  const sPresetName = settingsPop.querySelector("#gf-s-preset-name");
1688
1757
  const sPresetPrompt = settingsPop.querySelector("#gf-s-preset-prompt");
1689
1758
  const sPresetSaveBtn = settingsPop.querySelector("#gf-s-preset-save");
1690
1759
  const sPresetCancelBtn = settingsPop.querySelector("#gf-s-preset-cancel");
1691
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
+ ];
1692
1770
  function renderPresetList() {
1693
1771
  sPresetList.innerHTML = "";
1694
- presets.forEach((p) => {
1695
- const item = document.createElement("div");
1696
- 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;
1697
1779
  const name = document.createElement("span");
1698
- name.className = "gf-preset-item-name";
1780
+ name.className = "gf-pp-name";
1699
1781
  name.textContent = p.name;
1700
- name.style.cursor = "pointer";
1782
+ name.title = "Click to edit";
1701
1783
  name.addEventListener("click", () => {
1702
1784
  editingPresetId = p.id;
1785
+ sPresetFormTitle.textContent = "Edit Preset";
1703
1786
  sPresetForm.style.display = "flex";
1704
1787
  sPresetName.value = p.name;
1705
1788
  sPresetPrompt.value = p.prompt;
1706
1789
  sPresetName.focus();
1707
1790
  });
1708
- const del = document.createElement("button");
1709
- del.className = "gf-preset-del";
1710
- del.innerHTML = "&times;";
1711
- del.addEventListener("click", () => {
1712
- 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);
1713
1799
  if (activePresetId === p.id) activePresetId = null;
1714
1800
  renderPresetList();
1715
1801
  updateFillPresetUI();
1716
1802
  });
1717
- item.append(name, del);
1718
- sPresetList.appendChild(item);
1803
+ pill.append(name, x);
1804
+ sPresetList.appendChild(pill);
1719
1805
  });
1720
1806
  }
1721
1807
  renderPresetList();
1722
1808
  sPresetAddBtn.addEventListener("click", () => {
1723
1809
  editingPresetId = null;
1810
+ sPresetFormTitle.textContent = "New Preset";
1724
1811
  sPresetForm.style.display = "flex";
1725
1812
  sPresetName.value = "";
1726
1813
  sPresetPrompt.value = "";
@@ -1756,11 +1843,11 @@ function createOverlay2(options) {
1756
1843
  activePresetId
1757
1844
  };
1758
1845
  saveSettings(s);
1759
- dotWarn.style.display = s.useAI && !s.apiKey ? "block" : "none";
1846
+ dotWarn.style.display = "none";
1760
1847
  openPopover(null);
1761
1848
  });
1762
1849
  async function doFill() {
1763
- const settings = loadSettings();
1850
+ const settings = loadSettings(aiConfig?.provider || "openai");
1764
1851
  const activePreset = activePresetId ? (settings.presets || []).find((p) => p.id === activePresetId) : null;
1765
1852
  const userText = pPromptEl.value.trim();
1766
1853
  const promptText = [activePreset?.prompt, userText].filter(Boolean).join("\n\n");
@@ -1777,14 +1864,47 @@ function createOverlay2(options) {
1777
1864
  pFillBtn.innerHTML = `${ICONS.sparkles} Fill`;
1778
1865
  return;
1779
1866
  }
1780
- const blockContext = state.selectedBlock ? extractBlockContext(state.selectedBlock) : "";
1781
- fillData = await generateFillData(
1782
- state.fields,
1783
- promptText,
1784
- settings,
1785
- options.systemPrompt,
1786
- blockContext
1787
- );
1867
+ const provider = PROVIDERS[settings.provider] || PROVIDERS.openai;
1868
+ let blockContext = `Page: ${document.title}`;
1869
+ if (state.selectedBlock) {
1870
+ state.selectedBlock.querySelectorAll("h1,h2,h3,h4,label,legend").forEach((el) => {
1871
+ const t = el.textContent?.trim();
1872
+ if (t && t.length < 80) blockContext += `
1873
+ ${t}`;
1874
+ });
1875
+ }
1876
+ const fieldDesc = describeFields(state.fields);
1877
+ let userContent = `Form fields:
1878
+ ${fieldDesc}`;
1879
+ if (blockContext) userContent += `
1880
+
1881
+ Page context:
1882
+ ${blockContext}`;
1883
+ if (promptText) userContent += `
1884
+
1885
+ User instructions: ${promptText}`;
1886
+ else userContent += `
1887
+
1888
+ No specific instructions \u2014 generate realistic, contextually appropriate data for all fields.`;
1889
+ const resp = await fetch(`${provider.baseURL}/chat/completions`, {
1890
+ method: "POST",
1891
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${settings.apiKey}` },
1892
+ body: JSON.stringify({
1893
+ model: provider.model,
1894
+ messages: [
1895
+ { 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.` },
1896
+ { role: "user", content: userContent }
1897
+ ],
1898
+ temperature: 0.7,
1899
+ ...settings.provider === "openai" ? { response_format: { type: "json_object" } } : {}
1900
+ })
1901
+ });
1902
+ if (!resp.ok) throw new Error(await resp.text());
1903
+ const data = await resp.json();
1904
+ const content = data.choices?.[0]?.message?.content || "";
1905
+ const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, content];
1906
+ const parsed = JSON.parse(jsonMatch[1].trim());
1907
+ fillData = Array.isArray(parsed) ? parsed : parsed.fields || parsed.data || parsed.items || [];
1788
1908
  } else {
1789
1909
  fillData = generateFakeData(state.fields);
1790
1910
  }
@@ -1794,8 +1914,38 @@ function createOverlay2(options) {
1794
1914
  } else {
1795
1915
  setStatus(`Filled ${filled} field${filled === 1 ? "" : "s"}`, "success");
1796
1916
  }
1917
+ if (state.selectedBlock) {
1918
+ const el = state.selectedBlock;
1919
+ el.style.transition = "box-shadow 0.8s ease";
1920
+ el.style.animation = "none";
1921
+ const rect = el.getBoundingClientRect();
1922
+ const ripple = document.createElement("div");
1923
+ Object.assign(ripple.style, {
1924
+ position: "fixed",
1925
+ top: `${rect.top}px`,
1926
+ left: `${rect.left}px`,
1927
+ width: `${rect.width}px`,
1928
+ height: `${rect.height}px`,
1929
+ borderRadius: "6px",
1930
+ pointerEvents: "none",
1931
+ zIndex: "2147483644",
1932
+ border: "2px solid rgba(52,211,153,0.6)",
1933
+ boxShadow: "0 0 0 0 rgba(52,211,153,0.4), inset 0 0 20px rgba(52,211,153,0.08)",
1934
+ animation: "none"
1935
+ });
1936
+ document.body.appendChild(ripple);
1937
+ requestAnimationFrame(() => {
1938
+ ripple.style.transition = "box-shadow 0.8s ease, border-color 0.8s ease, opacity 0.8s ease";
1939
+ ripple.style.boxShadow = "0 0 0 8px rgba(52,211,153,0), inset 0 0 0 rgba(52,211,153,0)";
1940
+ ripple.style.borderColor = "rgba(52,211,153,0)";
1941
+ ripple.style.opacity = "0";
1942
+ });
1943
+ setTimeout(() => {
1944
+ ripple.remove();
1945
+ }, 1e3);
1946
+ }
1797
1947
  removeBlockHighlight();
1798
- setTimeout(() => openPopover(null), 600);
1948
+ setTimeout(() => openPopover(null), 800);
1799
1949
  } catch (err) {
1800
1950
  setStatus(cleanError(err), "error");
1801
1951
  } finally {
@@ -1833,6 +1983,12 @@ function createOverlay2(options) {
1833
1983
  }
1834
1984
  }
1835
1985
  document.addEventListener("keydown", handleShortcut);
1986
+ document.addEventListener("keydown", (e) => {
1987
+ if (e.key === "Escape" && currentPopover) {
1988
+ e.preventDefault();
1989
+ openPopover(null);
1990
+ }
1991
+ });
1836
1992
  function destroy() {
1837
1993
  cleanupSelector?.();
1838
1994
  removeBlockHighlight();
@@ -1842,6 +1998,94 @@ function createOverlay2(options) {
1842
1998
  return { state, destroy };
1843
1999
  }
1844
2000
 
2001
+ // src/ai.ts
2002
+ function isRecord(value) {
2003
+ return typeof value === "object" && value !== null;
2004
+ }
2005
+ function toFieldFillData(value) {
2006
+ if (!isRecord(value)) {
2007
+ throw new Error("AI response item is not an object");
2008
+ }
2009
+ const index = value.index;
2010
+ const rawValue = value.value;
2011
+ const checked = value.checked;
2012
+ if (typeof index !== "number" || !Number.isInteger(index)) {
2013
+ throw new Error("AI response item is missing a numeric index");
2014
+ }
2015
+ if (typeof rawValue !== "string") {
2016
+ throw new Error("AI response item is missing a string value");
2017
+ }
2018
+ return {
2019
+ index,
2020
+ value: rawValue,
2021
+ checked: typeof checked === "boolean" ? checked : void 0
2022
+ };
2023
+ }
2024
+ function toPromptFields(fields) {
2025
+ return fields.map((field, index) => ({
2026
+ index,
2027
+ type: field.type,
2028
+ name: field.name,
2029
+ label: field.label,
2030
+ options: field.options,
2031
+ required: field.required,
2032
+ min: field.min,
2033
+ max: field.max,
2034
+ pattern: field.pattern
2035
+ }));
2036
+ }
2037
+ function parseFillDataPayload(payload) {
2038
+ if (typeof payload === "string") {
2039
+ const jsonMatch = payload.match(/```(?:json)?\s*([\s\S]*?)```/) || [null, payload];
2040
+ return parseFillDataPayload(JSON.parse(jsonMatch[1].trim()));
2041
+ }
2042
+ if (Array.isArray(payload)) {
2043
+ return payload.map(toFieldFillData);
2044
+ }
2045
+ if (isRecord(payload)) {
2046
+ if (Array.isArray(payload.choices)) {
2047
+ const content = payload.choices[0]?.message;
2048
+ if (typeof content?.content === "string") {
2049
+ return parseFillDataPayload(content.content);
2050
+ }
2051
+ }
2052
+ const candidate = payload.fields ?? payload.data ?? payload.items ?? payload.result;
2053
+ if (Array.isArray(candidate)) {
2054
+ return candidate.map(toFieldFillData);
2055
+ }
2056
+ }
2057
+ throw new Error("AI response is not an array of field fills");
2058
+ }
2059
+ function createRequest(fields, userPrompt, provider, systemPrompt) {
2060
+ return {
2061
+ provider,
2062
+ prompt: userPrompt,
2063
+ systemPrompt,
2064
+ fields: toPromptFields(fields)
2065
+ };
2066
+ }
2067
+ async function generateFillData(fields, userPrompt, provider, transport, systemPrompt) {
2068
+ const request = createRequest(fields, userPrompt, provider, systemPrompt);
2069
+ if (transport.requestFillData) {
2070
+ return transport.requestFillData(request);
2071
+ }
2072
+ const endpoint = transport.endpoint ?? "/api/ghostfill";
2073
+ const response = await fetch(endpoint, {
2074
+ method: "POST",
2075
+ headers: {
2076
+ "Content-Type": "application/json"
2077
+ },
2078
+ body: JSON.stringify(request)
2079
+ });
2080
+ if (!response.ok) {
2081
+ const error = await response.text();
2082
+ throw new Error(`AI API error (${response.status}): ${error}`);
2083
+ }
2084
+ const contentType = response.headers.get("content-type") || "";
2085
+ const payload = contentType.includes("application/json") ? await response.json() : await response.text();
2086
+ return parseFillDataPayload(payload);
2087
+ }
2088
+
1845
2089
  // src/index.ts
1846
2090
  var instance = null;
1847
2091
  function init(options = {}) {
@@ -1862,11 +2106,18 @@ async function fill(params) {
1862
2106
  if (fields.length === 0) {
1863
2107
  return { filled: 0, errors: ["No fillable fields found in container"] };
1864
2108
  }
1865
- const fillData = generateFakeData(fields);
2109
+ const fillData = params.ai ? await generateFillData(
2110
+ fields,
2111
+ params.prompt || "",
2112
+ params.provider || params.ai.provider || "openai",
2113
+ params.ai,
2114
+ params.systemPrompt
2115
+ ) : generateFakeData(fields);
1866
2116
  return fillFields(fields, fillData);
1867
2117
  }
1868
2118
  // Annotate the CommonJS export names for ESM import in node:
1869
2119
  0 && (module.exports = {
2120
+ PROVIDERS,
1870
2121
  fill,
1871
2122
  init
1872
2123
  });