@aglyn/plugins-ai 1.0.0-beta.292 → 1.0.0-beta.296
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/package.json +17 -17
- package/src/lib/components/ai-job-links.d.ts +15 -2
- package/src/lib/components/ai-job-links.js +24 -3
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +9 -0
- package/src/lib/components/ai-job-requests.js +34 -0
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +28 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-import-job.component.js +5 -4
- package/src/lib/components/ai-site-import-job.component.js.map +1 -1
- package/src/lib/components/ai-site-import-start.component.js +3 -3
- package/src/lib/components/ai-site-import-start.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.d.ts +1 -1
- package/src/lib/components/ai-site-start-card.component.js +129 -29
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +15 -3
- package/src/lib/components/ai-site-start-steps.component.js +42 -25
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-style-previews.d.ts +17 -0
- package/src/lib/components/ai-site-style-previews.js +26 -95
- package/src/lib/components/ai-site-style-previews.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.d.ts +7 -1
- package/src/lib/components/assist-jobs-drawer.component.js +20 -6
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-page-change-card.component.d.ts +44 -0
- package/src/lib/components/assist-page-change-card.component.js +166 -0
- package/src/lib/components/assist-page-change-card.component.js.map +1 -0
- package/src/lib/components/assist-panel.component.d.ts +105 -1
- package/src/lib/components/assist-panel.component.js +224 -29
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-job-edit-step.d.ts +62 -16
- package/src/lib/jobs/ai-job-edit-step.js +172 -61
- package/src/lib/jobs/ai-job-edit-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +21 -2
- package/src/lib/jobs/ai-job-import-step.js +179 -49
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +27 -3
- package/src/lib/jobs/ai-job-layout-language.js +156 -113
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +56 -17
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.js +4 -2
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +25 -15
- package/src/lib/jobs/ai-job-page-language.js +120 -66
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +27 -7
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +12 -4
- package/src/lib/jobs/ai-job-site-datasets.js +36 -16
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +6 -2
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +50 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +586 -35
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-import-compile.d.ts +147 -0
- package/src/lib/jobs/ai-site-import-compile.js +1143 -0
- package/src/lib/jobs/ai-site-import-compile.js.map +1 -0
- package/src/lib/jobs/ai-site-import-document.d.ts +9 -36
- package/src/lib/jobs/ai-site-import-document.js +23 -489
- package/src/lib/jobs/ai-site-import-document.js.map +1 -1
- package/src/lib/jobs/ai-site-import-look.d.ts +54 -7
- package/src/lib/jobs/ai-site-import-look.js +163 -52
- package/src/lib/jobs/ai-site-import-look.js.map +1 -1
- package/src/lib/jobs/ai-site-import-publish.d.ts +7 -1
- package/src/lib/jobs/ai-site-import-publish.js +19 -5
- package/src/lib/jobs/ai-site-import-publish.js.map +1 -1
- package/src/lib/jobs/ai-site-late-photos.js +4 -2
- package/src/lib/jobs/ai-site-late-photos.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +10 -1
- package/src/lib/jobs/ai-site-publish.js +8 -3
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.d.ts +23 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js +677 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-1029-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +26 -0
- package/src/lib/layout-language/ai-layout-compiler.js +677 -33
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js +5 -1
- package/src/lib/layout-language/ai-layout-frame-hollows.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame-source.d.ts +196 -0
- package/src/lib/layout-language/ai-layout-frame-source.js +726 -0
- package/src/lib/layout-language/ai-layout-frame-source.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +22 -0
- package/src/lib/layout-language/ai-layout-frame.js +121 -29
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +103 -37
- package/src/lib/layout-language/ai-layout-language.js +85 -120
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +11 -7
- package/src/lib/model/ai-credit-estimate.js +11 -7
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-edit-job.d.ts +33 -16
- package/src/lib/model/ai-edit-job.js +50 -34
- package/src/lib/model/ai-edit-job.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +6 -11
- package/src/lib/model/ai-job-activity.js +8 -15
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +6 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +307 -13
- package/src/lib/model/ai-site-import.js +227 -19
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-inventory.d.ts +6 -0
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +65 -1
- package/src/lib/model/ai-site-look.js +141 -23
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-photos.d.ts +2 -0
- package/src/lib/model/ai-site-photos.js.map +1 -1
- package/src/lib/model/ai-site-start-steps.d.ts +4 -3
- package/src/lib/model/ai-site-start-steps.js +11 -3
- package/src/lib/model/ai-site-start-steps.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +23 -2
- package/src/lib/model/ai-site-start.js +25 -3
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/model/assist-edit.d.ts +18 -1
- package/src/lib/model/assist-edit.js +7 -0
- package/src/lib/model/assist-edit.js.map +1 -1
- package/src/lib/model/assist-page-change.d.ts +86 -0
- package/src/lib/model/assist-page-change.js +86 -0
- package/src/lib/model/assist-page-change.js.map +1 -0
- package/src/lib/providers/routing.d.ts +11 -0
- package/src/lib/providers/routing.js +23 -0
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +4 -2
- package/src/lib/runtime/ai-doctrine-validators.js +7 -2
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval.js +3 -1
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +4 -0
- package/src/lib/runtime/ai-icon-library.js +4 -0
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +5 -0
- package/src/lib/runtime/ai-node-tree.js +23 -4
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +19 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +2 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +8 -1
- package/src/lib/runtime/site-inventory.js +18 -0
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/assist-build.js +2 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.js +51 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-edit.d.ts +54 -2
- package/src/lib/server/assist-edit.js +219 -30
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/server/assist-page-change.d.ts +62 -0
- package/src/lib/server/assist-page-change.js +140 -0
- package/src/lib/server/assist-page-change.js.map +1 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.d.ts +50 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js +86 -0
- package/src/lib/site-import/fixtures/site-layout-recordings.js.map +1 -0
- package/src/lib/site-import/site-import-crawl.d.ts +18 -1
- package/src/lib/site-import/site-import-crawl.js +70 -10
- package/src/lib/site-import/site-import-crawl.js.map +1 -1
- package/src/lib/site-import/site-import-css.d.ts +110 -0
- package/src/lib/site-import/site-import-css.js +826 -0
- package/src/lib/site-import/site-import-css.js.map +1 -0
- package/src/lib/site-import/site-import-html.d.ts +116 -1
- package/src/lib/site-import/site-import-html.js +538 -46
- package/src/lib/site-import/site-import-html.js.map +1 -1
- package/src/lib/site-import/site-import-layout.d.ts +79 -0
- package/src/lib/site-import/site-import-layout.js +1608 -0
- package/src/lib/site-import/site-import-layout.js.map +1 -0
- package/src/lib/site-import/site-import-nav.d.ts +40 -0
- package/src/lib/site-import/site-import-nav.js +437 -0
- package/src/lib/site-import/site-import-nav.js.map +1 -0
- package/src/lib/site-import/site-import-page.d.ts +8 -1
- package/src/lib/site-import/site-import-page.js +24 -2
- package/src/lib/site-import/site-import-page.js.map +1 -1
- package/src/lib/assets/site-styles/beauty.webp +0 -0
- package/src/lib/assets/site-styles/blog.webp +0 -0
- package/src/lib/assets/site-styles/business.webp +0 -0
- package/src/lib/assets/site-styles/coming-soon.webp +0 -0
- package/src/lib/assets/site-styles/education.webp +0 -0
- package/src/lib/assets/site-styles/events.webp +0 -0
- package/src/lib/assets/site-styles/fitness.webp +0 -0
- package/src/lib/assets/site-styles/landing.webp +0 -0
- package/src/lib/assets/site-styles/music.webp +0 -0
- package/src/lib/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/personal.webp +0 -0
- package/src/lib/assets/site-styles/photography.webp +0 -0
- package/src/lib/assets/site-styles/portfolio.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/realestate.webp +0 -0
- package/src/lib/assets/site-styles/restaurant.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/studio.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/assets/site-styles/wellness.webp +0 -0
- package/src/lib/assets/site-styles/yoga.webp +0 -0
- package/src/lib/site-import/site-import-review.d.ts +0 -40
- package/src/lib/site-import/site-import-review.js +0 -149
- package/src/lib/site-import/site-import-review.js.map +0 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { ASSIST_PAGE_CHANGE_ACTION_ID, ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS, ASSIST_PAGE_CHANGE_TOOL_NAME } from "../model/assist-page-change.js";
|
|
18
|
+
import { ASSIST_BUILD_TOOL_NAME } from "../model/assist-build.js";
|
|
19
|
+
import { AI_JOB_BRIEF_MAX_CHARS } from "../jobs/ai-job-text-step.js";
|
|
20
|
+
import { assistBuildBriefWithoutGaps } from "./assist-build.js";
|
|
21
|
+
/**
|
|
22
|
+
* A change to a page the site already has, from the Assist chat (AGL-4116):
|
|
23
|
+
* the tool the model asks through, the site's pages it picks from, and the
|
|
24
|
+
* resolution the panel starts an `edit` job from. See
|
|
25
|
+
* `model/assist-page-change.ts`.
|
|
26
|
+
*/ /** The most pages the chat is shown; a site's pages past it are asked about by name. */ export const ASSIST_SITE_PAGES_MAX = 60;
|
|
27
|
+
const STRING = {
|
|
28
|
+
type: 'string'
|
|
29
|
+
};
|
|
30
|
+
export function assistPageChangeTool() {
|
|
31
|
+
return {
|
|
32
|
+
name: ASSIST_PAGE_CHANGE_TOOL_NAME,
|
|
33
|
+
description: 'Change a page this site already has. The change is saved into that page’s draft, or as a new ' + 'version when the user asks for one, and the user is given a link to open it in the Besigner, ' + 'check it and publish it. Call at most once per message, for one page.',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
summary: STRING,
|
|
38
|
+
page: STRING,
|
|
39
|
+
change: STRING,
|
|
40
|
+
newVersion: {
|
|
41
|
+
type: 'boolean'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: [
|
|
45
|
+
'summary',
|
|
46
|
+
'page',
|
|
47
|
+
'change',
|
|
48
|
+
'newVersion'
|
|
49
|
+
],
|
|
50
|
+
additionalProperties: false
|
|
51
|
+
},
|
|
52
|
+
strict: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The protocol: the same for every site, so it is cached. */ export function assistPageChangeBlock() {
|
|
56
|
+
return [
|
|
57
|
+
'Changing a page the site already has:',
|
|
58
|
+
`- When the user asks to change something on one of the pages under "The site's pages" (its words, pictures, sections, layout or look), do not plan a build. Write one sentence saying you will make the change as a draft of that page, then call the ${ASSIST_PAGE_CHANGE_TOOL_NAME} tool once.`,
|
|
59
|
+
'- page: the id of that page from the list. Where two pages share a name or an address, it is the live one unless the user names the other.',
|
|
60
|
+
'- change: exactly what they asked to change on that page, in their words, with what this conversation settled about it. Add nothing they did not ask for.',
|
|
61
|
+
'- summary: one short sentence, such as "Make the top image fill the background on Home".',
|
|
62
|
+
'- The change is saved into that page’s draft, on any plan: the published page stays as it is until they publish. When it is ready, the chat shows Open in the Besigner, where Open draft shows the change and they publish it. A further change to the same page is made on that draft.',
|
|
63
|
+
'- newVersion: true only when they ask for the change as a new or separate version of the page; otherwise false. Never offer a new version they did not ask for.',
|
|
64
|
+
'- Nothing is changed when you call the tool: never say it is done. Never say you cannot use a draft, cannot edit the page or cannot give a link.',
|
|
65
|
+
`- One page per message. For changes to several pages, change the first and say to ask for the next. A request that also asks for new pages, forms or anything else to be made is a build: call ${ASSIST_BUILD_TOOL_NAME} with the whole request instead.`
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The site's pages, per site and per moment, so volatile — and, on a plan
|
|
70
|
+
* without version history, that a new version cannot be made, so a request
|
|
71
|
+
* for one is answered with the draft rather than a promise.
|
|
72
|
+
*/ export function assistSitePagesBlock(pages, options = {
|
|
73
|
+
versioning: true
|
|
74
|
+
}) {
|
|
75
|
+
if (!pages.length) return null;
|
|
76
|
+
return [
|
|
77
|
+
"The site's pages (id · name · address · live or draft):",
|
|
78
|
+
...pages.map((page)=>`- ${page.id} · ${page.name} · ${page.slug || '/'} · ${page.live ? 'live' : 'draft, not live'}${page.home ? ', the home page' : ''}`),
|
|
79
|
+
...options.versioning ? [] : [
|
|
80
|
+
'This workspace’s plan keeps no version history (it comes with Pro), so newVersion is always false: if they ask for a new version, say the change goes into the page’s draft instead.'
|
|
81
|
+
]
|
|
82
|
+
].join('\n');
|
|
83
|
+
}
|
|
84
|
+
function text(value) {
|
|
85
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The site's pages a change may name: not deleted, not a collection's entry
|
|
89
|
+
* template, an email design or an error body. Live ones and the home page
|
|
90
|
+
* first, so a cap keeps the pages a person means.
|
|
91
|
+
*/ export async function readAssistSitePages(firestore, hostId) {
|
|
92
|
+
var _hostDoc_get;
|
|
93
|
+
const host = firestore.collection('hosts').doc(hostId);
|
|
94
|
+
const [hostDoc, screens] = await Promise.all([
|
|
95
|
+
host.get(),
|
|
96
|
+
host.collection('screens').select('displayName', 'name', 'slug', 'kind', 'deletedAt').limit(ASSIST_SITE_PAGES_MAX * 3).get()
|
|
97
|
+
]);
|
|
98
|
+
const routing = (_hostDoc_get = hostDoc.get('screens')) != null ? _hostDoc_get : {};
|
|
99
|
+
const homeId = text(hostDoc.get('defaultHomeScreenId'));
|
|
100
|
+
const pages = [];
|
|
101
|
+
for (const doc of screens.docs){
|
|
102
|
+
const data = doc.data();
|
|
103
|
+
// A page has no kind; an entry template, an email design and an error
|
|
104
|
+
// body each carry theirs, as the site inventory reads them.
|
|
105
|
+
if (data['deletedAt'] || text(data['kind'])) continue;
|
|
106
|
+
const slug = text(data['slug']);
|
|
107
|
+
pages.push({
|
|
108
|
+
id: doc.id,
|
|
109
|
+
name: text(data['displayName']) || text(data['name']) || slug || doc.id,
|
|
110
|
+
slug: slug.startsWith('/') ? slug : `/${slug}`,
|
|
111
|
+
live: typeof routing[doc.id] === 'string',
|
|
112
|
+
home: doc.id === homeId
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const rank = (page)=>page.home ? 0 : page.live ? 1 : 2;
|
|
116
|
+
return pages.sort((a, b)=>rank(a) - rank(b) || a.slug.localeCompare(b.slug)).slice(0, ASSIST_SITE_PAGES_MAX);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The model's call, held to the site's own pages: a page it named that the
|
|
120
|
+
* site does not have, or a change with nothing in it, proposes nothing.
|
|
121
|
+
*/ export function resolveAssistPageChange(input, scope) {
|
|
122
|
+
if (!input) return null;
|
|
123
|
+
const page = scope.pages.find((one)=>one.id === text(input['page']));
|
|
124
|
+
if (!page) return null;
|
|
125
|
+
const change = assistBuildBriefWithoutGaps(text(input['change']));
|
|
126
|
+
if (!change) return null;
|
|
127
|
+
const summary = text(input['summary']);
|
|
128
|
+
return _extends({
|
|
129
|
+
id: ASSIST_PAGE_CHANGE_ACTION_ID,
|
|
130
|
+
hostId: scope.hostId,
|
|
131
|
+
pageId: page.id,
|
|
132
|
+
pageName: page.name,
|
|
133
|
+
brief: change.slice(0, AI_JOB_BRIEF_MAX_CHARS),
|
|
134
|
+
summary: (summary || `Change ${page.name}`).slice(0, ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS)
|
|
135
|
+
}, input['newVersion'] === true ? {
|
|
136
|
+
newVersion: true
|
|
137
|
+
} : {});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=assist-page-change.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/server/assist-page-change.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_PAGE_CHANGE_ACTION_ID,\n ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS,\n ASSIST_PAGE_CHANGE_TOOL_NAME,\n type AssistPageChangeProposal,\n} from '../model/assist-page-change'\nimport { ASSIST_BUILD_TOOL_NAME } from '../model/assist-build'\nimport type { AiTool } from '../providers/contract'\nimport { AI_JOB_BRIEF_MAX_CHARS } from '../jobs/ai-job-text-step'\nimport { assistBuildBriefWithoutGaps } from './assist-build'\n\n/**\n * A change to a page the site already has, from the Assist chat (AGL-4116):\n * the tool the model asks through, the site's pages it picks from, and the\n * resolution the panel starts an `edit` job from. See\n * `model/assist-page-change.ts`.\n */\n\n/** The most pages the chat is shown; a site's pages past it are asked about by name. */\nexport const ASSIST_SITE_PAGES_MAX = 60\n\n/** One of the site's pages, as the chat is shown it. */\nexport interface AssistSitePage {\n id: string\n name: string\n /** The address it is served at, or would be once published. */\n slug: string\n /** Visitors reach it: the site routes its address to it. */\n live: boolean\n /** The site's home page. */\n home: boolean\n}\n\nconst STRING = { type: 'string' } as const\n\nexport function assistPageChangeTool(): AiTool {\n return {\n name: ASSIST_PAGE_CHANGE_TOOL_NAME,\n description:\n 'Change a page this site already has. The change is saved into that page’s draft, or as a new ' +\n 'version when the user asks for one, and the user is given a link to open it in the Besigner, ' +\n 'check it and publish it. Call at most once per message, for one page.',\n inputSchema: {\n type: 'object',\n properties: {\n summary: STRING,\n page: STRING,\n change: STRING,\n newVersion: { type: 'boolean' },\n },\n required: ['summary', 'page', 'change', 'newVersion'],\n additionalProperties: false,\n },\n strict: true,\n }\n}\n\n/** The protocol: the same for every site, so it is cached. */\nexport function assistPageChangeBlock(): string {\n return [\n 'Changing a page the site already has:',\n `- When the user asks to change something on one of the pages under \"The site's pages\" (its words, pictures, sections, layout or look), do not plan a build. Write one sentence saying you will make the change as a draft of that page, then call the ${ASSIST_PAGE_CHANGE_TOOL_NAME} tool once.`,\n '- page: the id of that page from the list. Where two pages share a name or an address, it is the live one unless the user names the other.',\n '- change: exactly what they asked to change on that page, in their words, with what this conversation settled about it. Add nothing they did not ask for.',\n '- summary: one short sentence, such as \"Make the top image fill the background on Home\".',\n '- The change is saved into that page’s draft, on any plan: the published page stays as it is until they publish. When it is ready, the chat shows Open in the Besigner, where Open draft shows the change and they publish it. A further change to the same page is made on that draft.',\n '- newVersion: true only when they ask for the change as a new or separate version of the page; otherwise false. Never offer a new version they did not ask for.',\n '- Nothing is changed when you call the tool: never say it is done. Never say you cannot use a draft, cannot edit the page or cannot give a link.',\n `- One page per message. For changes to several pages, change the first and say to ask for the next. A request that also asks for new pages, forms or anything else to be made is a build: call ${ASSIST_BUILD_TOOL_NAME} with the whole request instead.`,\n ].join('\\n')\n}\n\n/**\n * The site's pages, per site and per moment, so volatile — and, on a plan\n * without version history, that a new version cannot be made, so a request\n * for one is answered with the draft rather than a promise.\n */\nexport function assistSitePagesBlock(\n pages: readonly AssistSitePage[],\n options: { versioning: boolean } = { versioning: true },\n): string | null {\n if (!pages.length) return null\n return [\n \"The site's pages (id · name · address · live or draft):\",\n ...pages.map(\n (page) =>\n `- ${page.id} · ${page.name} · ${page.slug || '/'} · ${page.live ? 'live' : 'draft, not live'}${page.home ? ', the home page' : ''}`,\n ),\n ...(options.versioning\n ? []\n : [\n 'This workspace’s plan keeps no version history (it comes with Pro), so newVersion is always false: if they ask for a new version, say the change goes into the page’s draft instead.',\n ]),\n ].join('\\n')\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\n/**\n * The site's pages a change may name: not deleted, not a collection's entry\n * template, an email design or an error body. Live ones and the home page\n * first, so a cap keeps the pages a person means.\n */\nexport async function readAssistSitePages(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<AssistSitePage[]> {\n const host = firestore.collection('hosts').doc(hostId)\n const [hostDoc, screens] = await Promise.all([\n host.get(),\n host\n .collection('screens')\n .select('displayName', 'name', 'slug', 'kind', 'deletedAt')\n .limit(ASSIST_SITE_PAGES_MAX * 3)\n .get(),\n ])\n const routing = (hostDoc.get('screens') ?? {}) as Record<string, unknown>\n const homeId = text(hostDoc.get('defaultHomeScreenId'))\n const pages: AssistSitePage[] = []\n for (const doc of screens.docs) {\n const data = doc.data()\n // A page has no kind; an entry template, an email design and an error\n // body each carry theirs, as the site inventory reads them.\n if (data['deletedAt'] || text(data['kind'])) continue\n const slug = text(data['slug'])\n pages.push({\n id: doc.id,\n name: text(data['displayName']) || text(data['name']) || slug || doc.id,\n slug: slug.startsWith('/') ? slug : `/${slug}`,\n live: typeof routing[doc.id] === 'string',\n home: doc.id === homeId,\n })\n }\n const rank = (page: AssistSitePage) => (page.home ? 0 : page.live ? 1 : 2)\n return pages.sort((a, b) => rank(a) - rank(b) || a.slug.localeCompare(b.slug)).slice(0, ASSIST_SITE_PAGES_MAX)\n}\n\n/**\n * The model's call, held to the site's own pages: a page it named that the\n * site does not have, or a change with nothing in it, proposes nothing.\n */\nexport function resolveAssistPageChange(\n input: Record<string, unknown> | null,\n scope: { hostId: string; pages: readonly AssistSitePage[] },\n): AssistPageChangeProposal | null {\n if (!input) return null\n const page = scope.pages.find((one) => one.id === text(input['page']))\n if (!page) return null\n const change = assistBuildBriefWithoutGaps(text(input['change']))\n if (!change) return null\n const summary = text(input['summary'])\n return {\n id: ASSIST_PAGE_CHANGE_ACTION_ID,\n hostId: scope.hostId,\n pageId: page.id,\n pageName: page.name,\n brief: change.slice(0, AI_JOB_BRIEF_MAX_CHARS),\n summary: (summary || `Change ${page.name}`).slice(0, ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS),\n ...(input['newVersion'] === true ? { newVersion: true } : {}),\n }\n}\n"],"names":["ASSIST_PAGE_CHANGE_ACTION_ID","ASSIST_PAGE_CHANGE_SUMMARY_MAX_CHARS","ASSIST_PAGE_CHANGE_TOOL_NAME","ASSIST_BUILD_TOOL_NAME","AI_JOB_BRIEF_MAX_CHARS","assistBuildBriefWithoutGaps","ASSIST_SITE_PAGES_MAX","STRING","type","assistPageChangeTool","name","description","inputSchema","properties","summary","page","change","newVersion","required","additionalProperties","strict","assistPageChangeBlock","join","assistSitePagesBlock","pages","options","versioning","length","map","id","slug","live","home","text","value","trim","readAssistSitePages","firestore","hostId","hostDoc","host","collection","doc","screens","Promise","all","get","select","limit","routing","homeId","docs","data","push","startsWith","rank","sort","a","b","localeCompare","slice","resolveAssistPageChange","input","scope","find","one","pageId","pageName","brief"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
export declare const SITE_LAYOUT_AGLYN_ORIGIN = "https://aglyn.com";
|
|
18
|
+
export declare const SITE_LAYOUT_POTTERY_ORIGIN = "https://www.tidewater-pottery.example";
|
|
19
|
+
/** The recorded pages, by name: the file and the address it was read from. */
|
|
20
|
+
export declare const SITE_LAYOUT_PAGES: {
|
|
21
|
+
readonly home: {
|
|
22
|
+
readonly file: "aglyn-home.html";
|
|
23
|
+
readonly url: "https://aglyn.com/";
|
|
24
|
+
};
|
|
25
|
+
readonly product: {
|
|
26
|
+
readonly file: "aglyn-product.html";
|
|
27
|
+
readonly url: "https://aglyn.com/product";
|
|
28
|
+
};
|
|
29
|
+
readonly pricing: {
|
|
30
|
+
readonly file: "aglyn-pricing.html";
|
|
31
|
+
readonly url: "https://aglyn.com/pricing";
|
|
32
|
+
};
|
|
33
|
+
readonly contact: {
|
|
34
|
+
readonly file: "aglyn-contact.html";
|
|
35
|
+
readonly url: "https://aglyn.com/contact";
|
|
36
|
+
};
|
|
37
|
+
readonly post: {
|
|
38
|
+
readonly file: "aglyn-blog-post.html";
|
|
39
|
+
readonly url: "https://aglyn.com/blog/multi-site-cost-sheet-2026";
|
|
40
|
+
};
|
|
41
|
+
readonly pottery: {
|
|
42
|
+
readonly file: "squarespace-pottery.html";
|
|
43
|
+
readonly url: "https://www.tidewater-pottery.example/";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type SiteLayoutPageName = keyof typeof SITE_LAYOUT_PAGES;
|
|
47
|
+
/** One recorded page's markup. */
|
|
48
|
+
export declare function siteLayoutRecording(name: SiteLayoutPageName): string;
|
|
49
|
+
/** Whether an address is on the recorded page's own site. */
|
|
50
|
+
export declare function siteLayoutSameSite(name: SiteLayoutPageName): (url: URL) => boolean;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Pages recorded with their layout (AGL-4106), for the specs of the layout,
|
|
18
|
+
* menu and stylesheet readers, and for the compile and fidelity specs that
|
|
19
|
+
* read what they find.
|
|
20
|
+
*
|
|
21
|
+
* `site-layout/` holds five pages of aglyn.com as AglynBot read them on
|
|
22
|
+
* 2026-10-28 and 2026-10-29 (home, product, pricing, contact, one blog post)
|
|
23
|
+
* and one page in the markup a Squarespace site is built with, written for
|
|
24
|
+
* these specs about a studio that does not exist, so the readers are never
|
|
25
|
+
* tuned to one site's markup.
|
|
26
|
+
*
|
|
27
|
+
* The aglyn.com recordings keep what the `aglyn-com/` ones dropped and the
|
|
28
|
+
* layout is made of: class names, inline icons and the page's own stylesheet.
|
|
29
|
+
* Each was trimmed before it entered the repo: script bodies are gone (one
|
|
30
|
+
* small inline script with a hostile line stands in for them; the
|
|
31
|
+
* structured data stays), so are preload links, `data-*` attributes and
|
|
32
|
+
* `srcset`s; the stylesheet keeps only the rules for classes the page uses
|
|
33
|
+
* and the properties the readers read; class names the kept rules do not name
|
|
34
|
+
* are dropped, a grid framework's own hints kept; the pricing page keeps the
|
|
35
|
+
* first 14 rows of its plan table, and the product, pricing, contact and blog
|
|
36
|
+
* pages their header menu without its panels and their footer's © line. The
|
|
37
|
+
* home page keeps its header and footer whole, for the menu reader. Every
|
|
38
|
+
* address, phone number, author and profile is a placeholder.
|
|
39
|
+
*/ import { readFileSync } from "node:fs";
|
|
40
|
+
import { join } from "node:path";
|
|
41
|
+
const DIR = join(__dirname, 'site-layout');
|
|
42
|
+
export const SITE_LAYOUT_AGLYN_ORIGIN = 'https://aglyn.com';
|
|
43
|
+
export const SITE_LAYOUT_POTTERY_ORIGIN = 'https://www.tidewater-pottery.example';
|
|
44
|
+
/** The recorded pages, by name: the file and the address it was read from. */ export const SITE_LAYOUT_PAGES = {
|
|
45
|
+
home: {
|
|
46
|
+
file: 'aglyn-home.html',
|
|
47
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/`
|
|
48
|
+
},
|
|
49
|
+
product: {
|
|
50
|
+
file: 'aglyn-product.html',
|
|
51
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/product`
|
|
52
|
+
},
|
|
53
|
+
pricing: {
|
|
54
|
+
file: 'aglyn-pricing.html',
|
|
55
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/pricing`
|
|
56
|
+
},
|
|
57
|
+
contact: {
|
|
58
|
+
file: 'aglyn-contact.html',
|
|
59
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/contact`
|
|
60
|
+
},
|
|
61
|
+
post: {
|
|
62
|
+
file: 'aglyn-blog-post.html',
|
|
63
|
+
url: `${SITE_LAYOUT_AGLYN_ORIGIN}/blog/multi-site-cost-sheet-2026`
|
|
64
|
+
},
|
|
65
|
+
pottery: {
|
|
66
|
+
file: 'squarespace-pottery.html',
|
|
67
|
+
url: `${SITE_LAYOUT_POTTERY_ORIGIN}/`
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const cache = new Map();
|
|
71
|
+
/** One recorded page's markup. */ export function siteLayoutRecording(name) {
|
|
72
|
+
const { file } = SITE_LAYOUT_PAGES[name];
|
|
73
|
+
const held = cache.get(file);
|
|
74
|
+
if (held !== undefined) return held;
|
|
75
|
+
const text = readFileSync(join(DIR, file), 'utf8');
|
|
76
|
+
cache.set(file, text);
|
|
77
|
+
return text;
|
|
78
|
+
}
|
|
79
|
+
/** Whether an address is on the recorded page's own site. */ export function siteLayoutSameSite(name) {
|
|
80
|
+
const origin = new URL(SITE_LAYOUT_PAGES[name].url).origin;
|
|
81
|
+
const bare = origin.replace('://www.', '://');
|
|
82
|
+
const www = origin.replace('://', '://www.').replace('://www.www.', '://www.');
|
|
83
|
+
return (url)=>url.origin === origin || url.origin === bare || url.origin === www;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=site-layout-recordings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/ai/src/lib/site-import/fixtures/site-layout-recordings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Pages recorded with their layout (AGL-4106), for the specs of the layout,\n * menu and stylesheet readers, and for the compile and fidelity specs that\n * read what they find.\n *\n * `site-layout/` holds five pages of aglyn.com as AglynBot read them on\n * 2026-10-28 and 2026-10-29 (home, product, pricing, contact, one blog post)\n * and one page in the markup a Squarespace site is built with, written for\n * these specs about a studio that does not exist, so the readers are never\n * tuned to one site's markup.\n *\n * The aglyn.com recordings keep what the `aglyn-com/` ones dropped and the\n * layout is made of: class names, inline icons and the page's own stylesheet.\n * Each was trimmed before it entered the repo: script bodies are gone (one\n * small inline script with a hostile line stands in for them; the\n * structured data stays), so are preload links, `data-*` attributes and\n * `srcset`s; the stylesheet keeps only the rules for classes the page uses\n * and the properties the readers read; class names the kept rules do not name\n * are dropped, a grid framework's own hints kept; the pricing page keeps the\n * first 14 rows of its plan table, and the product, pricing, contact and blog\n * pages their header menu without its panels and their footer's © line. The\n * home page keeps its header and footer whole, for the menu reader. Every\n * address, phone number, author and profile is a placeholder.\n */\n\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst DIR = join(__dirname, 'site-layout')\n\nexport const SITE_LAYOUT_AGLYN_ORIGIN = 'https://aglyn.com'\nexport const SITE_LAYOUT_POTTERY_ORIGIN = 'https://www.tidewater-pottery.example'\n\n/** The recorded pages, by name: the file and the address it was read from. */\nexport const SITE_LAYOUT_PAGES = {\n home: { file: 'aglyn-home.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/` },\n product: { file: 'aglyn-product.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/product` },\n pricing: { file: 'aglyn-pricing.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/pricing` },\n contact: { file: 'aglyn-contact.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/contact` },\n post: { file: 'aglyn-blog-post.html', url: `${SITE_LAYOUT_AGLYN_ORIGIN}/blog/multi-site-cost-sheet-2026` },\n pottery: { file: 'squarespace-pottery.html', url: `${SITE_LAYOUT_POTTERY_ORIGIN}/` },\n} as const\n\nexport type SiteLayoutPageName = keyof typeof SITE_LAYOUT_PAGES\n\nconst cache = new Map<string, string>()\n\n/** One recorded page's markup. */\nexport function siteLayoutRecording(name: SiteLayoutPageName): string {\n const { file } = SITE_LAYOUT_PAGES[name]\n const held = cache.get(file)\n if (held !== undefined) return held\n const text = readFileSync(join(DIR, file), 'utf8')\n cache.set(file, text)\n return text\n}\n\n/** Whether an address is on the recorded page's own site. */\nexport function siteLayoutSameSite(name: SiteLayoutPageName): (url: URL) => boolean {\n const origin = new URL(SITE_LAYOUT_PAGES[name].url).origin\n const bare = origin.replace('://www.', '://')\n const www = origin.replace('://', '://www.').replace('://www.www.', '://www.')\n return (url) => url.origin === origin || url.origin === bare || url.origin === www\n}\n"],"names":["readFileSync","join","DIR","__dirname","SITE_LAYOUT_AGLYN_ORIGIN","SITE_LAYOUT_POTTERY_ORIGIN","SITE_LAYOUT_PAGES","home","file","url","product","pricing","contact","post","pottery","cache","Map","siteLayoutRecording","name","held","get","undefined","text","set","siteLayoutSameSite","origin","URL","bare","replace","www"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GAED,SAASA,YAAY,QAAQ,UAAS;AACtC,SAASC,IAAI,QAAQ,YAAW;AAEhC,MAAMC,MAAMD,KAAKE,WAAW;AAE5B,OAAO,MAAMC,2BAA2B,oBAAmB;AAC3D,OAAO,MAAMC,6BAA6B,wCAAuC;AAEjF,4EAA4E,GAC5E,OAAO,MAAMC,oBAAoB;IAC/BC,MAAM;QAAEC,MAAM;QAAmBC,KAAK,GAAGL,yBAAyB,CAAC,CAAC;IAAC;IACrEM,SAAS;QAAEF,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFO,SAAS;QAAEH,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFQ,SAAS;QAAEJ,MAAM;QAAsBC,KAAK,GAAGL,yBAAyB,QAAQ,CAAC;IAAC;IAClFS,MAAM;QAAEL,MAAM;QAAwBC,KAAK,GAAGL,yBAAyB,gCAAgC,CAAC;IAAC;IACzGU,SAAS;QAAEN,MAAM;QAA4BC,KAAK,GAAGJ,2BAA2B,CAAC,CAAC;IAAC;AACrF,EAAU;AAIV,MAAMU,QAAQ,IAAIC;AAElB,gCAAgC,GAChC,OAAO,SAASC,oBAAoBC,IAAwB;IAC1D,MAAM,EAAEV,IAAI,EAAE,GAAGF,iBAAiB,CAACY,KAAK;IACxC,MAAMC,OAAOJ,MAAMK,GAAG,CAACZ;IACvB,IAAIW,SAASE,WAAW,OAAOF;IAC/B,MAAMG,OAAOtB,aAAaC,KAAKC,KAAKM,OAAO;IAC3CO,MAAMQ,GAAG,CAACf,MAAMc;IAChB,OAAOA;AACT;AAEA,2DAA2D,GAC3D,OAAO,SAASE,mBAAmBN,IAAwB;IACzD,MAAMO,SAAS,IAAIC,IAAIpB,iBAAiB,CAACY,KAAK,CAACT,GAAG,EAAEgB,MAAM;IAC1D,MAAME,OAAOF,OAAOG,OAAO,CAAC,WAAW;IACvC,MAAMC,MAAMJ,OAAOG,OAAO,CAAC,OAAO,WAAWA,OAAO,CAAC,eAAe;IACpE,OAAO,CAACnB,MAAQA,IAAIgB,MAAM,KAAKA,UAAUhB,IAAIgB,MAAM,KAAKE,QAAQlB,IAAIgB,MAAM,KAAKI;AACjF"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type PublicFetchEdge, type PublicFetchOptions, type PublicFetchResult } from '../jobs/ai-theme-brand-inputs';
|
|
18
|
+
import type { SiteImportFooter, SiteImportLayout, SiteImportLookFacts, SiteImportNavTree } from '../model/ai-site-import';
|
|
18
19
|
import { type SiteImportBlock, type SiteImportBrandMark, type SiteImportForm, type SiteImportIcon, type SiteImportImage, type SiteImportNavLink, type SiteImportOrganization } from './site-import-html';
|
|
19
20
|
/**
|
|
20
21
|
* Where the bot's user agent points a site owner who wants to know what it is
|
|
@@ -32,7 +33,10 @@ export declare const AGLYN_BOT_INFO_URL: string;
|
|
|
32
33
|
export declare const AGLYN_BOT_USER_AGENT: string;
|
|
33
34
|
export declare const SITE_IMPORT_DEFAULTS: {
|
|
34
35
|
readonly maxPageBytes: number;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* A whole site's worth (AGL-4083): 54 MB for aglyn.com's 123 pages, with
|
|
38
|
+
* room; 400 pages at its 440 KB average are 176 MB (AGL-4106).
|
|
39
|
+
*/
|
|
36
40
|
readonly maxTotalBytes: number;
|
|
37
41
|
readonly timeBudgetMs: 60000;
|
|
38
42
|
/** One request's own clock, inside the whole crawl's. */
|
|
@@ -102,6 +106,12 @@ export interface SiteImportPage {
|
|
|
102
106
|
removed: number;
|
|
103
107
|
};
|
|
104
108
|
bytes: number;
|
|
109
|
+
/** How the source lays the page out (AGL-4106), on a read asked for it (`layout`): a wave's. */
|
|
110
|
+
layout?: SiteImportLayout | null;
|
|
111
|
+
/** The home page's menu, footer and look, as its source builds them (AGL-4106). */
|
|
112
|
+
nav?: SiteImportNavTree | null;
|
|
113
|
+
footer?: SiteImportFooter | null;
|
|
114
|
+
look?: SiteImportLookFacts | null;
|
|
105
115
|
}
|
|
106
116
|
export type SiteImportStop = 'page-budget' | 'total-bytes' | 'time'
|
|
107
117
|
/** robots.txt could not be read, so nothing was asked of the site. */
|
|
@@ -149,6 +159,13 @@ export interface SiteImportCrawlOptions {
|
|
|
149
159
|
* as for any crawl, and every other rule holds.
|
|
150
160
|
*/
|
|
151
161
|
only?: readonly string[];
|
|
162
|
+
/**
|
|
163
|
+
* Read how each page is laid out (AGL-4106): its structure, through its own
|
|
164
|
+
* stylesheets (the first two it links, each asked once a crawl). On by
|
|
165
|
+
* default for a wave's read (`only`), whose pages are built; the home page's
|
|
166
|
+
* menu, footer and look are read on every crawl.
|
|
167
|
+
*/
|
|
168
|
+
layout?: boolean;
|
|
152
169
|
}
|
|
153
170
|
export interface SiteImportCrawlSeams {
|
|
154
171
|
fetch?: (url: string, options: PublicFetchOptions) => Promise<PublicFetchResult>;
|
|
@@ -62,10 +62,12 @@ var _process_env_SITE_IMPORT_BOT_INFO_URL;
|
|
|
62
62
|
* reach the inventory (site-import-html.ts).
|
|
63
63
|
*=========================================*/ import { lookalikeBrandForHost } from "@aglyn/shared-util-email/outbound-phishing-screen";
|
|
64
64
|
import { fenceUntrustedText } from "@aglyn/shared-util-tools/untrusted-text";
|
|
65
|
-
import { fetchPublicTextDetailed } from "../jobs/ai-theme-brand-inputs.js";
|
|
65
|
+
import { AI_THEME_REFERENCE_STYLESHEETS, AI_THEME_STYLESHEET_MAX_BYTES, fetchPublicTextDetailed } from "../jobs/ai-theme-brand-inputs.js";
|
|
66
66
|
import { extractSiteImportPage, readMetaValues } from "./site-import-html.js";
|
|
67
67
|
import { AGLYN_BOT_TOKEN, parseRobotsTxt, parseSitemap, robotsAllows, robotsRulesFor } from "./site-import-robots.js";
|
|
68
68
|
import { NO_SITE_IMPORT_HEADERS, siteImportEdgeHeadersFor } from "./site-import-edge.js";
|
|
69
|
+
import { siteImportLayout, siteImportLookOf } from "./site-import-layout.js";
|
|
70
|
+
import { siteImportFooterColumns, siteImportNavTree } from "./site-import-nav.js";
|
|
69
71
|
/**
|
|
70
72
|
* Where the bot's user agent points a site owner who wants to know what it is
|
|
71
73
|
* (AGL-3983): the operator's own page about it. `SITE_IMPORT_BOT_INFO_URL` on
|
|
@@ -80,7 +82,10 @@ import { NO_SITE_IMPORT_HEADERS, siteImportEdgeHeadersFor } from "./site-import-
|
|
|
80
82
|
/** What every crawler request says it is. */ export const AGLYN_BOT_USER_AGENT = `AglynBot/1.0 (+${AGLYN_BOT_INFO_URL})`;
|
|
81
83
|
export const SITE_IMPORT_DEFAULTS = {
|
|
82
84
|
maxPageBytes: 2 * 1024 * 1024,
|
|
83
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* A whole site's worth (AGL-4083): 54 MB for aglyn.com's 123 pages, with
|
|
87
|
+
* room; 400 pages at its 440 KB average are 176 MB (AGL-4106).
|
|
88
|
+
*/ maxTotalBytes: 320 * 1024 * 1024,
|
|
84
89
|
timeBudgetMs: 60000,
|
|
85
90
|
/** One request's own clock, inside the whole crawl's. */ requestTimeoutMs: 8000,
|
|
86
91
|
robotsMaxBytes: 256 * 1024,
|
|
@@ -92,6 +97,12 @@ export const SITE_IMPORT_DEFAULTS = {
|
|
|
92
97
|
};
|
|
93
98
|
/** Never a page: files a link may point at that are not HTML. */ const NON_PAGE_EXTENSION = /\.(?:pdf|jpe?g|png|gif|webp|avif|svg|ico|bmp|tiff?|zip|gz|tgz|rar|7z|mp4|m4v|mov|webm|mp3|wav|ogg|m4a|docx?|xlsx?|pptx?|csv|txt|css|js|mjs|json|xml|rss|atom|woff2?|ttf|otf|eot|exe|dmg|apk)$/i;
|
|
94
99
|
const TRACKING_PARAM = /^(?:utm_[a-z]+|fbclid|gclid|msclkid|mc_cid|mc_eid|_ga|ref)$/i;
|
|
100
|
+
/**
|
|
101
|
+
* Never a page, whatever its extension (AGL-4106): an API's or a CDN's path
|
|
102
|
+
* (aglyn.com's press kit links 136 `/api/media/cdn/…?download=1` files) and
|
|
103
|
+
* any address that asks for a download.
|
|
104
|
+
*/ const NON_PAGE_PATH = /^\/(?:api|cdn-cgi|wp-json|_next\/static|_next\/image)(?:\/|$)/i;
|
|
105
|
+
const DOWNLOAD_PARAM = /^(?:download|dl|attachment)$/i;
|
|
95
106
|
const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/;
|
|
96
107
|
/**
|
|
97
108
|
* The per-request headers of a crawl of `domain`: the seam's, else the edge's,
|
|
@@ -124,7 +135,10 @@ const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\.[a-z0-9-]{1,63})+$/;
|
|
|
124
135
|
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
125
136
|
if (url.port && url.port !== '443') return null;
|
|
126
137
|
if (!origins.includes(url.origin)) return null;
|
|
127
|
-
if (NON_PAGE_EXTENSION.test(url.pathname)) return null;
|
|
138
|
+
if (NON_PAGE_EXTENSION.test(url.pathname) || NON_PAGE_PATH.test(url.pathname)) return null;
|
|
139
|
+
if ([
|
|
140
|
+
...url.searchParams.keys()
|
|
141
|
+
].some((key)=>DOWNLOAD_PARAM.test(key))) return null;
|
|
128
142
|
const clean = new URL(url.toString());
|
|
129
143
|
clean.hash = '';
|
|
130
144
|
for (const key of [
|
|
@@ -216,7 +230,7 @@ function skipReasonFor(result) {
|
|
|
216
230
|
else inventory.skippedOverflow += 1;
|
|
217
231
|
};
|
|
218
232
|
try {
|
|
219
|
-
var _robots_status, _robots_status1;
|
|
233
|
+
var _robots_status, _robots_status1, _options_layout;
|
|
220
234
|
// robots.txt, before anything else is asked of the site.
|
|
221
235
|
const home = `${origins[0]}/`;
|
|
222
236
|
let rules = [];
|
|
@@ -347,7 +361,21 @@ function skipReasonFor(result) {
|
|
|
347
361
|
if (remainingBytes() - reserved <= 0) return 'wait';
|
|
348
362
|
return null;
|
|
349
363
|
};
|
|
350
|
-
|
|
364
|
+
const readLayout = (_options_layout = options.layout) != null ? _options_layout : !!only;
|
|
365
|
+
/** Linked stylesheets, each asked once a crawl. */ const sheets = new Map();
|
|
366
|
+
const stylesheet = (url)=>{
|
|
367
|
+
const held = sheets.get(url);
|
|
368
|
+
if (held) return held;
|
|
369
|
+
const room = Math.min(AI_THEME_STYLESHEET_MAX_BYTES, remainingBytes() - reserved);
|
|
370
|
+
const asked = room <= 0 || outOfTime() ? Promise.resolve(null) : request(url, /text\/css|text\/plain/i, 'text/css,*/*;q=0.1', room).then((answer)=>{
|
|
371
|
+
if (!answer.ok) return null;
|
|
372
|
+
inventory.bytesRead += answer.bytes;
|
|
373
|
+
return answer.text;
|
|
374
|
+
}, ()=>null);
|
|
375
|
+
sheets.set(url, asked);
|
|
376
|
+
return asked;
|
|
377
|
+
};
|
|
378
|
+
/** Takes one asked page in: the inventory's next page, a skip, or the reason the read must stop. */ const takeIn = async ({ next, key, cap, page })=>{
|
|
351
379
|
var _siteImportPageKey;
|
|
352
380
|
const foundBy = next.foundBy;
|
|
353
381
|
if (foundBy === 'home') homeIn = true;
|
|
@@ -360,6 +388,8 @@ function skipReasonFor(result) {
|
|
|
360
388
|
cutOff.push(key);
|
|
361
389
|
return 'time';
|
|
362
390
|
}
|
|
391
|
+
// A file is not a page: it takes nothing from the page budget's attempts (AGL-4106).
|
|
392
|
+
if (page.refusal === 'content-type') attempts = Math.max(0, attempts - 1);
|
|
363
393
|
skip(_extends({
|
|
364
394
|
url: key,
|
|
365
395
|
reason: skipReasonFor(page)
|
|
@@ -386,7 +416,11 @@ function skipReasonFor(result) {
|
|
|
386
416
|
return null;
|
|
387
417
|
}
|
|
388
418
|
}
|
|
389
|
-
const
|
|
419
|
+
const isHome = foundBy === 'home';
|
|
420
|
+
const structured = readLayout || isHome;
|
|
421
|
+
const extract = extractSiteImportPage(page.text, page.url, sameSite, {
|
|
422
|
+
structure: structured
|
|
423
|
+
});
|
|
390
424
|
if (extract.loginForm) {
|
|
391
425
|
skip({
|
|
392
426
|
url: finalKey,
|
|
@@ -407,7 +441,33 @@ function skipReasonFor(result) {
|
|
|
407
441
|
});
|
|
408
442
|
}
|
|
409
443
|
}
|
|
410
|
-
|
|
444
|
+
// How the page is built (AGL-4106), through its own stylesheets.
|
|
445
|
+
let built = {};
|
|
446
|
+
if (extract.structure) {
|
|
447
|
+
const linked = await Promise.all(extract.structure.stylesheets.filter((url)=>!/fonts\.googleapis\.com/i.test(url)).slice(0, AI_THEME_REFERENCE_STYLESHEETS).map(stylesheet));
|
|
448
|
+
const css = linked.filter((text)=>typeof text === 'string');
|
|
449
|
+
const source = {
|
|
450
|
+
blocks: extract.blocks,
|
|
451
|
+
organization: extract.organization,
|
|
452
|
+
structure: extract.structure
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
built = _extends({}, readLayout ? {
|
|
456
|
+
layout: siteImportLayout(source, css)
|
|
457
|
+
} : {}, isHome ? {
|
|
458
|
+
nav: siteImportNavTree(source, css),
|
|
459
|
+
footer: siteImportFooterColumns(source, css),
|
|
460
|
+
look: siteImportLookOf(source, css)
|
|
461
|
+
} : {});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
// A page the reader cannot lay out is still its words.
|
|
464
|
+
console.warn('[site-import] the layout could not be read', {
|
|
465
|
+
url: page.url,
|
|
466
|
+
error
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
inventory.pages.push(_extends({}, built, {
|
|
411
471
|
url: finalKey,
|
|
412
472
|
path: new URL(finalKey).pathname,
|
|
413
473
|
foundBy,
|
|
@@ -428,11 +488,11 @@ function skipReasonFor(result) {
|
|
|
428
488
|
],
|
|
429
489
|
forms: extract.forms,
|
|
430
490
|
bytes: page.bytes
|
|
431
|
-
});
|
|
491
|
+
}));
|
|
432
492
|
return null;
|
|
433
493
|
};
|
|
434
494
|
for(;;){
|
|
435
|
-
var
|
|
495
|
+
var _ref;
|
|
436
496
|
const width = homeIn ? SITE_IMPORT_DEFAULTS.concurrency : 1;
|
|
437
497
|
while(!stop && queue.length && window.length < width){
|
|
438
498
|
const why = held();
|
|
@@ -466,7 +526,7 @@ function skipReasonFor(result) {
|
|
|
466
526
|
if (!window.length) break;
|
|
467
527
|
const asked = await window.shift();
|
|
468
528
|
reserved -= asked.cap;
|
|
469
|
-
stop = (
|
|
529
|
+
stop = (_ref = await takeIn(asked)) != null ? _ref : stop;
|
|
470
530
|
}
|
|
471
531
|
inventory.stoppedBy = stop;
|
|
472
532
|
// Whatever a stop left unread is said, never dropped; a wave's own read
|