@maintainer-pro/ai-bridge 0.1.32 → 0.1.33
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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/daemon.mjs +522 -80
- package/src/intake-server.mjs +516 -0
- package/src/share-rewrite.mjs +12 -2
- package/src/website-feedback.mjs +130 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-process intake form on the reserved site port.
|
|
5
|
+
* After generation, the caller stops this server and starts Next.js on the same port.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function escapeHtml(value) {
|
|
9
|
+
return String(value ?? "")
|
|
10
|
+
.replaceAll("&", "&")
|
|
11
|
+
.replaceAll("<", "<")
|
|
12
|
+
.replaceAll(">", ">")
|
|
13
|
+
.replaceAll('"', """);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function intakeClientUrlHelper() {
|
|
17
|
+
return `
|
|
18
|
+
function intakeUrl(path) {
|
|
19
|
+
var base = new URL(location.href);
|
|
20
|
+
base.hash = "";
|
|
21
|
+
if (!base.pathname.endsWith("/")) base.pathname += "/";
|
|
22
|
+
return new URL(path, base);
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const INTAKE_ETA_SECONDS = 5 * 60;
|
|
28
|
+
|
|
29
|
+
function statusPage(opts) {
|
|
30
|
+
const title = opts.title || "Building your website";
|
|
31
|
+
const message = opts.message || "This usually takes about 5 minutes.";
|
|
32
|
+
const kind = opts.kind === "error" ? "error" : "building";
|
|
33
|
+
const startedAt = Number(opts.startedAt) || 0;
|
|
34
|
+
const etaSeconds = Number(opts.etaSeconds) || INTAKE_ETA_SECONDS;
|
|
35
|
+
const boot = JSON.stringify({ kind, startedAt, etaSeconds, message });
|
|
36
|
+
return `<!doctype html>
|
|
37
|
+
<html lang="en" data-mp-intake>
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8" />
|
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
41
|
+
<title>${escapeHtml(title)}</title>
|
|
42
|
+
<style>
|
|
43
|
+
:root { --bg:#f3eee4; --ink:#1c1917; --muted:#6b645b; --accent:#0f4c5c; --danger:#9b2c2c; }
|
|
44
|
+
body { margin:0; min-height:100vh; display:grid; place-items:center; padding:24px;
|
|
45
|
+
font:16px/1.5 system-ui,sans-serif; color:var(--ink); background:var(--bg); }
|
|
46
|
+
main { width:min(440px,100%); background:#fffdf8; border:1px solid #ddd4c6; border-radius:16px; padding:32px; text-align:center; }
|
|
47
|
+
h1 { margin:0 0 8px; font-size:1.4rem; }
|
|
48
|
+
#message { margin:0; color:var(--muted); min-height:3em; }
|
|
49
|
+
.mark { width:64px; height:64px; margin:0 auto 18px; border-radius:50%;
|
|
50
|
+
border:4px solid #ddd4c6; border-top-color:${kind === "error" ? "var(--danger)" : "var(--accent)"};
|
|
51
|
+
animation:${kind === "error" ? "none" : "spin .9s linear infinite"}; }
|
|
52
|
+
.meta { display:${kind === "error" ? "none" : "block"}; margin-top:18px; }
|
|
53
|
+
.eta { font-weight:700; color:var(--ink); }
|
|
54
|
+
.clock { margin-top:4px; font-variant-numeric:tabular-nums; color:var(--muted); font-size:.92rem; }
|
|
55
|
+
.bar { margin:16px auto 0; height:6px; background:#ddd4c6; border-radius:99px; overflow:hidden; }
|
|
56
|
+
.bar > i { display:block; height:100%; width:8%; background:var(--accent); border-radius:inherit; transition:width .7s ease; }
|
|
57
|
+
@keyframes spin { to { transform: rotate(1turn); } }
|
|
58
|
+
@media (prefers-reduced-motion: reduce) { .mark { animation:none; } .bar > i { transition:none; } }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<main>
|
|
63
|
+
<div class="mark" aria-hidden="true"></div>
|
|
64
|
+
<h1 id="title">${escapeHtml(title)}</h1>
|
|
65
|
+
<p id="message">${escapeHtml(message)}</p>
|
|
66
|
+
<div class="meta" id="meta">
|
|
67
|
+
<div class="eta" id="eta">Usually about 5 minutes</div>
|
|
68
|
+
<div class="clock" id="clock">0:00 elapsed</div>
|
|
69
|
+
<div class="bar" aria-hidden="true"><i id="bar"></i></div>
|
|
70
|
+
</div>
|
|
71
|
+
</main>
|
|
72
|
+
<script type="application/json" id="mp-intake-boot">${boot}</script>
|
|
73
|
+
<script>
|
|
74
|
+
${intakeClientUrlHelper()}
|
|
75
|
+
var boot = {};
|
|
76
|
+
try { boot = JSON.parse(document.getElementById("mp-intake-boot").textContent || "{}"); } catch (e) {}
|
|
77
|
+
var startedAt = Number(boot.startedAt) || Date.now();
|
|
78
|
+
var etaSeconds = Number(boot.etaSeconds) || ${INTAKE_ETA_SECONDS};
|
|
79
|
+
var kind = boot.kind || "building";
|
|
80
|
+
var waitMessages = [
|
|
81
|
+
"The studio is creating your site from the form.",
|
|
82
|
+
"Writing pages and laying out the first draft.",
|
|
83
|
+
"Adding copy, photos, and structure.",
|
|
84
|
+
"Almost there — putting the last pieces in place."
|
|
85
|
+
];
|
|
86
|
+
var lateMessages = [
|
|
87
|
+
"This is taking a bit longer than usual. Still building.",
|
|
88
|
+
"Almost there. Keep this tab open.",
|
|
89
|
+
"Finishing the last details — thanks for waiting.",
|
|
90
|
+
"Hang tight. Shouldn't be much longer."
|
|
91
|
+
];
|
|
92
|
+
function pad(n) { return String(n).padStart(2, "0"); }
|
|
93
|
+
function clock(sec) {
|
|
94
|
+
sec = Math.max(0, Math.floor(sec));
|
|
95
|
+
return Math.floor(sec / 60) + ":" + pad(sec % 60);
|
|
96
|
+
}
|
|
97
|
+
function approxLeft(sec) {
|
|
98
|
+
if (sec <= 20) return "less than a minute left";
|
|
99
|
+
var min = Math.max(1, Math.round(sec / 60));
|
|
100
|
+
return min === 1 ? "about 1 minute left" : "about " + min + " minutes left";
|
|
101
|
+
}
|
|
102
|
+
function setError(message) {
|
|
103
|
+
kind = "error";
|
|
104
|
+
document.getElementById("title").textContent = "We could not finish the site";
|
|
105
|
+
document.getElementById("message").textContent = message || "Try again from the form.";
|
|
106
|
+
document.getElementById("meta").style.display = "none";
|
|
107
|
+
document.querySelector(".mark").style.animation = "none";
|
|
108
|
+
document.querySelector(".mark").style.borderTopColor = "var(--danger)";
|
|
109
|
+
}
|
|
110
|
+
function paint() {
|
|
111
|
+
if (kind === "error") return;
|
|
112
|
+
var elapsed = Math.max(0, (Date.now() - startedAt) / 1000);
|
|
113
|
+
var late = elapsed >= etaSeconds;
|
|
114
|
+
var left = Math.max(0, etaSeconds - elapsed);
|
|
115
|
+
document.getElementById("clock").textContent = clock(elapsed) + " elapsed";
|
|
116
|
+
if (late) {
|
|
117
|
+
var i = Math.floor((elapsed - etaSeconds) / 12) % lateMessages.length;
|
|
118
|
+
document.getElementById("eta").textContent = "Taking a bit longer than usual";
|
|
119
|
+
document.getElementById("message").textContent = lateMessages[i];
|
|
120
|
+
} else {
|
|
121
|
+
var i = Math.min(waitMessages.length - 1, Math.floor((elapsed / etaSeconds) * waitMessages.length));
|
|
122
|
+
document.getElementById("eta").textContent = "Usually about 5 minutes · " + approxLeft(left);
|
|
123
|
+
document.getElementById("message").textContent = waitMessages[i];
|
|
124
|
+
}
|
|
125
|
+
var t = elapsed / etaSeconds;
|
|
126
|
+
var pct = t < 1
|
|
127
|
+
? 8 + 80 * (1 - Math.pow(1 - t, 1.35))
|
|
128
|
+
: 88 + 8 * (1 - Math.exp(-(t - 1)));
|
|
129
|
+
document.getElementById("bar").style.width = Math.min(96, pct).toFixed(1) + "%";
|
|
130
|
+
}
|
|
131
|
+
async function tick() {
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch(intakeUrl("__mp/intake/status"), { cache: "no-store" });
|
|
134
|
+
if (!res.ok) throw new Error("status");
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
if (Number(data.startedAt)) startedAt = Number(data.startedAt);
|
|
137
|
+
if (Number(data.etaSeconds)) etaSeconds = Number(data.etaSeconds);
|
|
138
|
+
if (data.status === "ready") { location.reload(); return; }
|
|
139
|
+
if (data.status === "error") setError(data.message);
|
|
140
|
+
else paint();
|
|
141
|
+
} catch {
|
|
142
|
+
location.reload();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
paint();
|
|
146
|
+
setInterval(paint, 1000);
|
|
147
|
+
setInterval(tick, 3000);
|
|
148
|
+
</script>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function intakeFormHtml(appName) {
|
|
154
|
+
const name = escapeHtml(appName || "your business");
|
|
155
|
+
return `<!doctype html>
|
|
156
|
+
<html lang="en" data-mp-intake>
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="utf-8" />
|
|
159
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
160
|
+
<title>Website intake</title>
|
|
161
|
+
<style>
|
|
162
|
+
:root { --bg:#f3eee4; --card:#fffdf8; --ink:#1c1917; --muted:#6b645b; --line:#ddd4c6; --accent:#0f4c5c; --ok:#1d6a4f; --danger:#9b2c2c; }
|
|
163
|
+
* { box-sizing:border-box; }
|
|
164
|
+
body { margin:0; background:var(--bg); color:var(--ink); font:16px/1.5 system-ui,-apple-system,sans-serif; }
|
|
165
|
+
main { max-width:720px; margin:0 auto; padding:2.5rem 1.25rem 4rem; }
|
|
166
|
+
h1 { font-size:1.75rem; margin:0 0 .35rem; }
|
|
167
|
+
.lede { color:var(--muted); margin:0 0 1.75rem; }
|
|
168
|
+
fieldset { border:1px solid var(--line); background:var(--card); border-radius:8px; padding:1.1rem 1.15rem 1.25rem; margin:0 0 1rem; }
|
|
169
|
+
legend { font-weight:700; padding:0 .35rem; }
|
|
170
|
+
.grid { display:grid; gap:.85rem; }
|
|
171
|
+
@media (min-width:640px) { .two { grid-template-columns:1fr 1fr; } }
|
|
172
|
+
label { display:flex; flex-direction:column; gap:.3rem; font-weight:600; font-size:.92rem; }
|
|
173
|
+
.check { flex-direction:row; align-items:center; gap:.45rem; font-weight:500; }
|
|
174
|
+
input, select, textarea { font:inherit; padding:.55rem .7rem; border:1px solid var(--line); border-radius:6px; background:#fff; }
|
|
175
|
+
textarea { min-height:5.5rem; resize:vertical; }
|
|
176
|
+
.checks { display:flex; flex-wrap:wrap; gap:.45rem 1rem; }
|
|
177
|
+
button { appearance:none; border:0; background:var(--accent); color:#fff; font:inherit; font-weight:700; padding:.85rem 1.2rem; border-radius:6px; cursor:pointer; width:100%; }
|
|
178
|
+
button:disabled { opacity:.65; cursor:wait; }
|
|
179
|
+
.status { display:none; margin-top:.85rem; font-weight:600; text-align:center; }
|
|
180
|
+
.status.show { display:block; }
|
|
181
|
+
.status.ok { color:var(--ok); }
|
|
182
|
+
.status.err { color:var(--danger); }
|
|
183
|
+
</style>
|
|
184
|
+
</head>
|
|
185
|
+
<body>
|
|
186
|
+
<main>
|
|
187
|
+
<h1>Website intake</h1>
|
|
188
|
+
<p class="lede">Tell us about ${name}. Blank is better than a guess — we will build the site from this form.</p>
|
|
189
|
+
<form id="intake">
|
|
190
|
+
<fieldset>
|
|
191
|
+
<legend>Business</legend>
|
|
192
|
+
<div class="grid two">
|
|
193
|
+
<label>Business / trade name *<input name="tradeName" required /></label>
|
|
194
|
+
<label>Legal name<input name="legalName" /></label>
|
|
195
|
+
<label>What do you sell or do? *<input name="industry" required /></label>
|
|
196
|
+
<label>City *<input name="city" required /></label>
|
|
197
|
+
<label>Area / neighbourhood<input name="area" /></label>
|
|
198
|
+
<label>Country<input name="country" /></label>
|
|
199
|
+
<label>Year started<input name="yearStarted" /></label>
|
|
200
|
+
<label>Tagline<input name="tagline" /></label>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="grid" style="margin-top:.85rem">
|
|
203
|
+
<label>Existing website<input name="existingWebsite" placeholder="https://" /></label>
|
|
204
|
+
<label class="check"><input type="checkbox" name="hasNoWebsite" /> We do not have a website yet</label>
|
|
205
|
+
<label>Other public pages<textarea name="referenceUrls" placeholder="Google, IndiaMART, Facebook…"></textarea></label>
|
|
206
|
+
</div>
|
|
207
|
+
<p style="margin:.8rem 0 .3rem; font-weight:700; font-size:.92rem">Languages</p>
|
|
208
|
+
<div class="checks">
|
|
209
|
+
<label class="check"><input type="checkbox" name="languages" value="English" checked /> English</label>
|
|
210
|
+
<label class="check"><input type="checkbox" name="languages" value="Hindi" /> Hindi</label>
|
|
211
|
+
<label class="check"><input type="checkbox" name="languages" value="Gujarati" /> Gujarati</label>
|
|
212
|
+
</div>
|
|
213
|
+
</fieldset>
|
|
214
|
+
<fieldset>
|
|
215
|
+
<legend>Contact</legend>
|
|
216
|
+
<div class="grid two">
|
|
217
|
+
<label>Owner / contact person<input name="ownerName" /></label>
|
|
218
|
+
<label>Role<input name="role" /></label>
|
|
219
|
+
<label>Phone *<input name="phone" required /></label>
|
|
220
|
+
<label>Second phone<input name="phoneAlt" /></label>
|
|
221
|
+
<label>WhatsApp<input name="whatsapp" /></label>
|
|
222
|
+
<label>Email *<input name="email" type="email" required /></label>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="grid" style="margin-top:.85rem">
|
|
225
|
+
<label>Full address<textarea name="address"></textarea></label>
|
|
226
|
+
<label>Hours<input name="hours" /></label>
|
|
227
|
+
<label>Service area<input name="serviceArea" /></label>
|
|
228
|
+
<label>Licenses / GST / certifications<input name="licenses" /></label>
|
|
229
|
+
</div>
|
|
230
|
+
</fieldset>
|
|
231
|
+
<fieldset>
|
|
232
|
+
<legend>Offer</legend>
|
|
233
|
+
<div class="grid">
|
|
234
|
+
<label>Who is it for? *<input name="whoItsFor" required /></label>
|
|
235
|
+
<label>Primary service or product *<input name="primary" required /></label>
|
|
236
|
+
<label>Other services (one per line)<textarea name="secondary"></textarea></label>
|
|
237
|
+
<label>Product or menu names (one per line)<textarea name="products"></textarea></label>
|
|
238
|
+
<label>Why customers pick you<textarea name="whyTheyWin"></textarea></label>
|
|
239
|
+
<label>Pricing notes<input name="priceNotes" /></label>
|
|
240
|
+
</div>
|
|
241
|
+
</fieldset>
|
|
242
|
+
<fieldset>
|
|
243
|
+
<legend>Pages and CTA</legend>
|
|
244
|
+
<p style="margin:0 0 .4rem; font-weight:700; font-size:.92rem">Pages</p>
|
|
245
|
+
<div class="checks">
|
|
246
|
+
${["Home","Services","Products","About","Contact","Gallery","FAQ","Menu","Book a visit","Enquiry"].map((page) =>
|
|
247
|
+
`<label class="check"><input type="checkbox" name="pages" value="${page}" ${["Home","About","Contact"].includes(page) ? "checked" : ""} /> ${page}</label>`
|
|
248
|
+
).join("")}
|
|
249
|
+
</div>
|
|
250
|
+
<div class="grid two" style="margin-top:.85rem">
|
|
251
|
+
<label>Other pages<input name="customPages" /></label>
|
|
252
|
+
<label>Primary call to action
|
|
253
|
+
<select name="cta">
|
|
254
|
+
<option value="Call">Call</option>
|
|
255
|
+
<option value="WhatsApp">WhatsApp</option>
|
|
256
|
+
<option value="Contact form" selected>Contact form</option>
|
|
257
|
+
<option value="Book a visit">Book a visit</option>
|
|
258
|
+
<option value="Request an enquiry">Request an enquiry</option>
|
|
259
|
+
</select>
|
|
260
|
+
</label>
|
|
261
|
+
<label>Button label<input name="ctaLabel" /></label>
|
|
262
|
+
<label>Form leads go to this email<input name="formEmail" type="email" /></label>
|
|
263
|
+
</div>
|
|
264
|
+
</fieldset>
|
|
265
|
+
<fieldset>
|
|
266
|
+
<legend>Look, voice, social</legend>
|
|
267
|
+
<div class="grid two">
|
|
268
|
+
<label>Tone<input name="tone" /></label>
|
|
269
|
+
<label>Visual feel<input name="feel" /></label>
|
|
270
|
+
<label>Words to use<input name="wordsToUse" /></label>
|
|
271
|
+
<label>Words to avoid<input name="wordsToAvoid" /></label>
|
|
272
|
+
<label>Accent colour<input name="accentColor" /></label>
|
|
273
|
+
</div>
|
|
274
|
+
<div class="grid" style="margin-top:.85rem">
|
|
275
|
+
<label>Photo notes<textarea name="photoNotes"></textarea></label>
|
|
276
|
+
<label>Instagram<input name="instagram" /></label>
|
|
277
|
+
<label>YouTube<input name="youtube" /></label>
|
|
278
|
+
<label>Facebook<input name="facebook" /></label>
|
|
279
|
+
<label>LinkedIn<input name="linkedin" /></label>
|
|
280
|
+
</div>
|
|
281
|
+
</fieldset>
|
|
282
|
+
<fieldset>
|
|
283
|
+
<legend>Search and limits</legend>
|
|
284
|
+
<div class="grid two">
|
|
285
|
+
<label>Domain<input name="domain" /></label>
|
|
286
|
+
<label>Main search phrase<input name="primaryKeyword" /></label>
|
|
287
|
+
<label>Other phrases<input name="secondaryKeywords" /></label>
|
|
288
|
+
<label>Deadline<input name="deadline" /></label>
|
|
289
|
+
<label>Competitor 1<input name="competitor1" /></label>
|
|
290
|
+
<label>Competitor 2<input name="competitor2" /></label>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="grid" style="margin-top:.85rem">
|
|
293
|
+
<label>Must appear<textarea name="mustInclude"></textarea></label>
|
|
294
|
+
<label>Must not appear<textarea name="mustExclude"></textarea></label>
|
|
295
|
+
<label>Anything else<textarea name="notes"></textarea></label>
|
|
296
|
+
</div>
|
|
297
|
+
</fieldset>
|
|
298
|
+
<button type="submit">Build my website</button>
|
|
299
|
+
<p id="status" class="status" role="status"></p>
|
|
300
|
+
</form>
|
|
301
|
+
</main>
|
|
302
|
+
<script>
|
|
303
|
+
${intakeClientUrlHelper()}
|
|
304
|
+
const form = document.getElementById("intake");
|
|
305
|
+
const status = document.getElementById("status");
|
|
306
|
+
function val(name) {
|
|
307
|
+
const el = form.elements[name];
|
|
308
|
+
if (!el) return "";
|
|
309
|
+
if (el.type === "checkbox") return el.checked;
|
|
310
|
+
return String(el.value || "").trim();
|
|
311
|
+
}
|
|
312
|
+
function checked(name) {
|
|
313
|
+
return [...form.querySelectorAll('[name="' + name + '"]:checked')].map((el) => el.value);
|
|
314
|
+
}
|
|
315
|
+
function payload() {
|
|
316
|
+
const tradeName = val("tradeName");
|
|
317
|
+
const city = val("city");
|
|
318
|
+
const slug = [tradeName, city].filter(Boolean).join(" ").toLowerCase()
|
|
319
|
+
.replace(/[^\\w\\s-]/g, "").trim().replace(/[\\s_]+/g, "-");
|
|
320
|
+
return {
|
|
321
|
+
schema: "website-builder.intake/v1",
|
|
322
|
+
slug,
|
|
323
|
+
business: {
|
|
324
|
+
tradeName, legalName: val("legalName"), industry: val("industry"), city,
|
|
325
|
+
area: val("area"), country: val("country"), yearStarted: val("yearStarted"),
|
|
326
|
+
tagline: val("tagline"), existingWebsite: val("existingWebsite"),
|
|
327
|
+
hasNoWebsite: val("hasNoWebsite"), referenceUrls: val("referenceUrls"),
|
|
328
|
+
languages: checked("languages")
|
|
329
|
+
},
|
|
330
|
+
contact: {
|
|
331
|
+
ownerName: val("ownerName"), role: val("role"), phone: val("phone"),
|
|
332
|
+
phoneAlt: val("phoneAlt"), whatsapp: val("whatsapp"), email: val("email"),
|
|
333
|
+
address: val("address"), hours: val("hours"), serviceArea: val("serviceArea"),
|
|
334
|
+
licenses: val("licenses")
|
|
335
|
+
},
|
|
336
|
+
offer: {
|
|
337
|
+
whoItsFor: val("whoItsFor"), primary: val("primary"), secondary: val("secondary"),
|
|
338
|
+
products: val("products"), whyTheyWin: val("whyTheyWin"), priceNotes: val("priceNotes")
|
|
339
|
+
},
|
|
340
|
+
pages: checked("pages"),
|
|
341
|
+
customPages: val("customPages"),
|
|
342
|
+
cta: { primary: val("cta"), label: val("ctaLabel"), formEmail: val("formEmail") },
|
|
343
|
+
voice: { tone: val("tone"), wordsToUse: val("wordsToUse"), wordsToAvoid: val("wordsToAvoid") },
|
|
344
|
+
visual: { feel: val("feel"), accentColor: val("accentColor"), photoNotes: val("photoNotes") },
|
|
345
|
+
social: { instagram: val("instagram"), youtube: val("youtube"), facebook: val("facebook"), linkedin: val("linkedin") },
|
|
346
|
+
seo: { domain: val("domain"), primaryKeyword: val("primaryKeyword"), secondaryKeywords: val("secondaryKeywords") },
|
|
347
|
+
competitors: [val("competitor1"), val("competitor2")].filter(Boolean),
|
|
348
|
+
mustInclude: val("mustInclude"),
|
|
349
|
+
mustExclude: val("mustExclude"),
|
|
350
|
+
deadline: val("deadline"),
|
|
351
|
+
notes: val("notes")
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
form.addEventListener("submit", async (event) => {
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
const button = form.querySelector("button[type=submit]");
|
|
357
|
+
button.disabled = true;
|
|
358
|
+
status.className = "status show";
|
|
359
|
+
status.textContent = "Sending…";
|
|
360
|
+
try {
|
|
361
|
+
const res = await fetch(intakeUrl("__mp/intake"), {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: { "content-type": "application/json" },
|
|
364
|
+
body: JSON.stringify(payload())
|
|
365
|
+
});
|
|
366
|
+
const data = await res.json().catch(() => ({}));
|
|
367
|
+
if (!res.ok) throw new Error(data.error || "Could not start the build");
|
|
368
|
+
location.href = location.pathname + (location.search || "");
|
|
369
|
+
} catch (err) {
|
|
370
|
+
button.disabled = false;
|
|
371
|
+
status.className = "status show err";
|
|
372
|
+
status.textContent = err instanceof Error ? err.message : "Could not start the build";
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
</script>
|
|
376
|
+
</body>
|
|
377
|
+
</html>`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function send(res, status, headers, body) {
|
|
381
|
+
res.statusCode = status;
|
|
382
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
383
|
+
res.setHeader(key, value);
|
|
384
|
+
}
|
|
385
|
+
res.end(body);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function json(res, status, data) {
|
|
389
|
+
send(
|
|
390
|
+
res,
|
|
391
|
+
status,
|
|
392
|
+
{ "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
393
|
+
Buffer.from(JSON.stringify(data))
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function html(res, status, body) {
|
|
398
|
+
send(
|
|
399
|
+
res,
|
|
400
|
+
status,
|
|
401
|
+
{ "content-type": "text/html; charset=utf-8", "cache-control": "no-store" },
|
|
402
|
+
Buffer.from(body)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @param {{
|
|
408
|
+
* port: number,
|
|
409
|
+
* appName?: string,
|
|
410
|
+
* getStatus: () => { status: string, message?: string, startedAt?: number, etaSeconds?: number },
|
|
411
|
+
* onSubmit: (intake: unknown) => Promise<void> | void,
|
|
412
|
+
* }} opts
|
|
413
|
+
*/
|
|
414
|
+
export function startIntakeServer(opts) {
|
|
415
|
+
const port = Number(opts.port);
|
|
416
|
+
if (!port) throw new Error("intake server requires a port");
|
|
417
|
+
const appName = opts.appName || "your business";
|
|
418
|
+
|
|
419
|
+
const server = http.createServer((req, res) => {
|
|
420
|
+
void (async () => {
|
|
421
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
422
|
+
const pathname = url.pathname;
|
|
423
|
+
if (req.method === "OPTIONS") {
|
|
424
|
+
send(res, 204, {
|
|
425
|
+
"access-control-allow-origin": "*",
|
|
426
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
427
|
+
"access-control-allow-headers": "content-type",
|
|
428
|
+
}, "");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (pathname === "/__mp/intake/status" && req.method === "GET") {
|
|
432
|
+
json(res, 200, opts.getStatus() || { status: "form" });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (pathname === "/__mp/intake" && req.method === "POST") {
|
|
436
|
+
const chunks = [];
|
|
437
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
438
|
+
let intake = {};
|
|
439
|
+
try {
|
|
440
|
+
intake = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
441
|
+
} catch {
|
|
442
|
+
json(res, 400, { error: "Invalid intake JSON" });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
await opts.onSubmit(intake);
|
|
447
|
+
json(res, 202, { ok: true, status: "building" });
|
|
448
|
+
} catch (err) {
|
|
449
|
+
json(res, 400, {
|
|
450
|
+
error: err instanceof Error ? err.message : "Could not start the build",
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
456
|
+
send(res, 405, { "content-type": "text/plain" }, "Method not allowed");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const state = opts.getStatus() || { status: "form" };
|
|
460
|
+
if (state.status === "building" || state.status === "ready") {
|
|
461
|
+
html(res, 200, statusPage({
|
|
462
|
+
title: "Building your website",
|
|
463
|
+
message: state.message || "This usually takes about 5 minutes.",
|
|
464
|
+
startedAt: state.startedAt,
|
|
465
|
+
etaSeconds: state.etaSeconds || INTAKE_ETA_SECONDS,
|
|
466
|
+
}));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (state.status === "error") {
|
|
470
|
+
html(res, 200, statusPage({
|
|
471
|
+
kind: "error",
|
|
472
|
+
title: "We could not finish the site",
|
|
473
|
+
message: state.message || "Refresh to try the form again.",
|
|
474
|
+
}));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
html(res, 200, intakeFormHtml(appName));
|
|
478
|
+
})().catch(() => {
|
|
479
|
+
if (!res.headersSent) {
|
|
480
|
+
send(res, 500, { "content-type": "text/plain" }, "Internal error");
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
const onError = (err) => {
|
|
487
|
+
server.off("listening", onListen);
|
|
488
|
+
reject(err);
|
|
489
|
+
};
|
|
490
|
+
const onListen = () => {
|
|
491
|
+
server.off("error", onError);
|
|
492
|
+
resolve({
|
|
493
|
+
port,
|
|
494
|
+
close: () =>
|
|
495
|
+
new Promise((done) => {
|
|
496
|
+
server.close(() => done());
|
|
497
|
+
setTimeout(done, 1500);
|
|
498
|
+
}),
|
|
499
|
+
});
|
|
500
|
+
};
|
|
501
|
+
server.once("error", onError);
|
|
502
|
+
server.listen(port, "127.0.0.1", onListen);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function isIntakeArtifactName(name) {
|
|
507
|
+
const n = String(name || "").toLowerCase();
|
|
508
|
+
return (
|
|
509
|
+
n === "intake.json" ||
|
|
510
|
+
n === "brief.md" ||
|
|
511
|
+
n === "notes.md" ||
|
|
512
|
+
n === "readme.md" ||
|
|
513
|
+
n === "license" ||
|
|
514
|
+
n === "license.md"
|
|
515
|
+
);
|
|
516
|
+
}
|
package/src/share-rewrite.mjs
CHANGED
|
@@ -1014,8 +1014,12 @@ function htmlAlreadyHasWidget(html) {
|
|
|
1014
1014
|
);
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
function isIntakeHtml(html) {
|
|
1018
|
+
return /\bdata-mp-intake\b/i.test(String(html || ""));
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1017
1021
|
function injectMaintainerProEmbed(html, aiPublicBase) {
|
|
1018
|
-
if (htmlAlreadyHasWidget(html)) return html;
|
|
1022
|
+
if (htmlAlreadyHasWidget(html) || isIntakeHtml(html)) return html;
|
|
1019
1023
|
const ai = String(aiPublicBase || "").replace(/\/$/, "");
|
|
1020
1024
|
const loader = `<script data-mp-proxy-embed>(function(ai,ws){if(!ai||window.__MP_EMBED_LOADING)return;window.__MP_EMBED_LOADING=1;function load(src){return new Promise(function(resolve,reject){var s=document.createElement("script");s.src=src;s.onload=function(){resolve()};s.onerror=function(){reject(new Error(src))};(document.head||document.documentElement).appendChild(s)})}function boot(){var cfg=window.__MAINTAINER_PRO__||{};if(!window.AiUi||!window.AiUi.init)return false;window.AiUi.init({apiUrl:ai+"/api/chat",aiServerWsUrl:ws,title:"AI Assistant",maintainerProUrl:cfg.maintainerProUrl||undefined,maintainerProApiKey:cfg.maintainerProApiKey||undefined});return true}load(ai+"/embed-config.js").then(function(){return load(ai+"/ai-ui.iife.js")}).then(function(){if(boot())return;var n=0,t=setInterval(function(){n+=1;if(boot()||n>50)clearInterval(t)},100)}).catch(function(){})})(${JSON.stringify(ai)},${JSON.stringify(`${httpToWsOrigin(ai)}/api/ws`)})<\/script>`;
|
|
1021
1025
|
if (/<head[^>]*>/i.test(html)) {
|
|
@@ -1629,6 +1633,7 @@ function injectNextProxyShim(html, publicBase) {
|
|
|
1629
1633
|
* ifNoneMatch?: string,
|
|
1630
1634
|
* port?: number,
|
|
1631
1635
|
* portUrls?: Record<string, string>,
|
|
1636
|
+
* injectChat?: boolean,
|
|
1632
1637
|
* }} opts
|
|
1633
1638
|
* @returns {{ status: number, headers: Record<string, string>, body: Buffer }}
|
|
1634
1639
|
*/
|
|
@@ -1716,7 +1721,12 @@ export function processShareHttpResponse(opts) {
|
|
|
1716
1721
|
mutated = true;
|
|
1717
1722
|
}
|
|
1718
1723
|
}
|
|
1719
|
-
if (
|
|
1724
|
+
if (
|
|
1725
|
+
opts?.injectChat !== false &&
|
|
1726
|
+
slug !== "ai" &&
|
|
1727
|
+
aiPublicBase &&
|
|
1728
|
+
isHtmlDocument(headers, text)
|
|
1729
|
+
) {
|
|
1720
1730
|
const next = injectMaintainerProEmbed(text, aiPublicBase);
|
|
1721
1731
|
if (next !== text) {
|
|
1722
1732
|
text = next;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const WEBSITE_FEEDBACK_FLUSH_MS = 4 * 60 * 60 * 1000;
|
|
5
|
+
export const WEBSITE_FEEDBACK_CHECK_MS = 15 * 60 * 1000;
|
|
6
|
+
const MAX_NOTES = 60;
|
|
7
|
+
|
|
8
|
+
const PERSONAL_INFO_RE =
|
|
9
|
+
/\b(\+?\d[\d\s().-]{7,}\d|[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}|whatsapp|gstin?|aadhaar|passport|ssn)\b/i;
|
|
10
|
+
|
|
11
|
+
function looksLikePersonalInfo(text) {
|
|
12
|
+
const raw = String(text || "").trim();
|
|
13
|
+
if (!raw) return false;
|
|
14
|
+
if (PERSONAL_INFO_RE.test(raw)) return true;
|
|
15
|
+
return /\b(phone|email|address|whatsapp|mobile|owner name|my name is|call me at)\b/i.test(
|
|
16
|
+
raw
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldQueueUserRequest(text) {
|
|
21
|
+
const raw = String(text || "").trim();
|
|
22
|
+
if (raw.length < 12 || raw.length > 2000) return false;
|
|
23
|
+
if (looksLikePersonalInfo(raw)) return false;
|
|
24
|
+
const t = raw.toLowerCase();
|
|
25
|
+
if (
|
|
26
|
+
/^(ok|okay|thanks|thank you|yes|no|got it|cool|great|perfect|hi|hello|hey)\b/.test(
|
|
27
|
+
t
|
|
28
|
+
) &&
|
|
29
|
+
raw.length < 48
|
|
30
|
+
) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function folderLooksIntakeBuilt(folder) {
|
|
37
|
+
const root = String(folder || "").trim();
|
|
38
|
+
if (!root) return false;
|
|
39
|
+
return fs.existsSync(path.join(root, "intake.json"));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isIntakeBuiltWorkspace(ws) {
|
|
43
|
+
if (!ws || typeof ws !== "object") return false;
|
|
44
|
+
if (ws.intakeBuilt === true) return true;
|
|
45
|
+
if (String(ws.intakeStatus || "") === "ready") return true;
|
|
46
|
+
return folderLooksIntakeBuilt(ws.folderPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function markIntakeBuilt(ws) {
|
|
50
|
+
if (!ws || typeof ws !== "object") return ws;
|
|
51
|
+
ws.intakeBuilt = true;
|
|
52
|
+
ws.intakeStatus = ws.intakeStatus || "ready";
|
|
53
|
+
return ws;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function notesOf(ws) {
|
|
57
|
+
return Array.isArray(ws?.websiteFeedback) ? ws.websiteFeedback : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function queueWebsiteFeedback(ws, userMessage) {
|
|
61
|
+
if (!isIntakeBuiltWorkspace(ws)) return false;
|
|
62
|
+
const text = String(userMessage || "").trim();
|
|
63
|
+
if (!shouldQueueUserRequest(text)) return false;
|
|
64
|
+
const next = notesOf(ws);
|
|
65
|
+
const last = next[next.length - 1];
|
|
66
|
+
if (last && String(last.text || "").trim() === text) return false;
|
|
67
|
+
next.push({
|
|
68
|
+
at: Date.now(),
|
|
69
|
+
text: text.slice(0, 400),
|
|
70
|
+
});
|
|
71
|
+
ws.websiteFeedback = next.slice(-MAX_NOTES);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function websiteFeedbackShouldFlush(ws, opts = {}) {
|
|
76
|
+
const notes = notesOf(ws);
|
|
77
|
+
if (notes.length === 0) return false;
|
|
78
|
+
if (opts.force === true) return true;
|
|
79
|
+
const firstAt = Number(notes[0]?.at) || Date.now();
|
|
80
|
+
return Date.now() - firstAt >= WEBSITE_FEEDBACK_FLUSH_MS;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function flushWebsiteFeedback(ws, deps) {
|
|
84
|
+
const notes = notesOf(ws);
|
|
85
|
+
if (notes.length === 0) return { flushed: false, appended: false };
|
|
86
|
+
const requests = notes
|
|
87
|
+
.map((row) => String(row?.text || "").trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
if (requests.length === 0) {
|
|
90
|
+
ws.websiteFeedback = [];
|
|
91
|
+
ws.websiteFeedbackFlushedAt = Date.now();
|
|
92
|
+
return { flushed: true, appended: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cli = await deps.loadAiCli();
|
|
96
|
+
if (typeof cli.mergeWebsiteMemory !== "function") {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"ai-cli is missing mergeWebsiteMemory — update @maintainer-pro/ai-cli"
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (typeof cli.fetchWebsiteMemory !== "function") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"ai-cli is missing fetchWebsiteMemory — update @maintainer-pro/ai-cli"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const current = await cli.fetchWebsiteMemory(deps.memorySource);
|
|
108
|
+
const merged = await cli.mergeWebsiteMemory({
|
|
109
|
+
current,
|
|
110
|
+
requests,
|
|
111
|
+
workspaceDir: ws.folderPath ? path.resolve(ws.folderPath) : undefined,
|
|
112
|
+
});
|
|
113
|
+
if (!merged?.keep || !merged.memory || looksLikePersonalInfo(merged.memory)) {
|
|
114
|
+
ws.websiteFeedback = [];
|
|
115
|
+
ws.websiteFeedbackFlushedAt = Date.now();
|
|
116
|
+
return { flushed: true, appended: false };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const data = await deps.api(
|
|
120
|
+
"POST",
|
|
121
|
+
"/api/v1/bridge/machine/website-memory",
|
|
122
|
+
{
|
|
123
|
+
sandboxId: ws.sandboxId,
|
|
124
|
+
memory: merged.memory,
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
ws.websiteFeedback = [];
|
|
128
|
+
ws.websiteFeedbackFlushedAt = Date.now();
|
|
129
|
+
return { flushed: true, appended: data?.appended === true };
|
|
130
|
+
}
|