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/README.md +162 -12
- package/dist/chunk-VMCU3BNJ.mjs +275 -0
- package/dist/chunk-VMCU3BNJ.mjs.map +1 -0
- package/dist/index.d.mts +6 -41
- package/dist/index.d.ts +6 -41
- package/dist/index.js +492 -252
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +411 -391
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +12 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +165 -0
- package/dist/server.js.map +1 -0
- package/dist/server.mjs +15 -0
- package/dist/server.mjs.map +1 -0
- package/dist/types-B3DGbZyx.d.mts +83 -0
- package/dist/types-B3DGbZyx.d.ts +83 -0
- package/package.json +7 -2
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:
|
|
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
|
|
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)
|
|
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
|
|
580
|
+
return defaultSettings(provider);
|
|
667
581
|
}
|
|
668
582
|
function saveSettings(s) {
|
|
669
|
-
|
|
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
|
-
|
|
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(/^
|
|
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:
|
|
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:
|
|
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:
|
|
787
|
-
background: #18181b; color: #
|
|
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(
|
|
791
|
-
transition:
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
1009
|
-
.gf-preset-list { display: flex; flex-
|
|
1010
|
-
.gf-preset-
|
|
1011
|
-
display: flex; align-items: center;
|
|
1012
|
-
padding: 4px
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
.gf-preset-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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-
|
|
1022
|
-
|
|
1023
|
-
|
|
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-
|
|
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:
|
|
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
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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... 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
|
-
|
|
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
|
-
|
|
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 ? "#
|
|
1316
|
-
const bgInput = isDark ? "
|
|
1317
|
-
const border = isDark ? "
|
|
1318
|
-
const text = isDark ? "#
|
|
1319
|
-
const textMuted = isDark ? "
|
|
1320
|
-
const textDim = isDark ? "
|
|
1321
|
-
const btnHoverBg = isDark ? "#27272a" : "#
|
|
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
|
|
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
|
|
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
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1703
|
-
|
|
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-
|
|
1780
|
+
name.className = "gf-pp-name";
|
|
1706
1781
|
name.textContent = p.name;
|
|
1707
|
-
name.
|
|
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
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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 = "×";
|
|
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 = "×";
|
|
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
|
-
|
|
1738
|
-
|
|
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 =
|
|
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
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
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),
|
|
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 =
|
|
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
|
});
|