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/README.md +100 -10
- 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 +476 -225
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +395 -364
- 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 +6 -1
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;
|
|
@@ -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,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
|
|
1009
|
-
.gf-preset-list { display: flex; flex-
|
|
1010
|
-
.gf-preset-
|
|
1011
|
-
display: flex; align-items: center;
|
|
1012
|
-
padding: 4px
|
|
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-
|
|
1015
|
-
|
|
1016
|
-
|
|
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-
|
|
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:
|
|
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
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
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
|
-
<
|
|
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... 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
|
-
|
|
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
|
-
|
|
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 ? "#
|
|
1309
|
-
const bgInput = isDark ? "
|
|
1310
|
-
const border = isDark ? "
|
|
1311
|
-
const text = isDark ? "#
|
|
1312
|
-
const textMuted = isDark ? "
|
|
1313
|
-
const textDim = isDark ? "
|
|
1314
|
-
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)";
|
|
1315
1354
|
for (const pop of [settingsPop, promptPop]) {
|
|
1316
1355
|
pop.style.background = bg;
|
|
1317
|
-
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)";
|
|
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
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1696
|
-
|
|
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-
|
|
1780
|
+
name.className = "gf-pp-name";
|
|
1699
1781
|
name.textContent = p.name;
|
|
1700
|
-
name.
|
|
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
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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);
|
|
1713
1799
|
if (activePresetId === p.id) activePresetId = null;
|
|
1714
1800
|
renderPresetList();
|
|
1715
1801
|
updateFillPresetUI();
|
|
1716
1802
|
});
|
|
1717
|
-
|
|
1718
|
-
sPresetList.appendChild(
|
|
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 =
|
|
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
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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),
|
|
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 =
|
|
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
|
});
|