@aglyn/plugins-ai 1.0.0-beta.232 → 1.0.0-beta.234
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 +6 -0
- package/src/lib/components/ai-job-links.js +9 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +41 -3
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +91 -8
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +7 -4
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +8 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-admission.d.ts +2 -1
- package/src/lib/jobs/ai-job-admission.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +39 -0
- package/src/lib/jobs/ai-job-email-step.js +116 -5
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
- package/src/lib/jobs/ai-job-layout-language.js +3 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-site-pages.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-site-pages.js +23 -11
- package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +3 -2
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +16 -0
- package/src/lib/jobs/ai-job-page-language.js +35 -6
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +3 -0
- package/src/lib/jobs/ai-job-page-step.js +18 -4
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +40 -1
- package/src/lib/jobs/ai-job-plan-step.js +121 -16
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +6 -6
- package/src/lib/jobs/ai-job-site-content.js +6 -12
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +11 -0
- package/src/lib/jobs/ai-job-site-step.js +96 -44
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +14 -4
- package/src/lib/jobs/ai-jobs-notify.js +70 -4
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +22 -4
- package/src/lib/jobs/ai-jobs.js +47 -12
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +98 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +248 -0
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -0
- package/src/lib/jobs/ai-site-publish.d.ts +11 -0
- package/src/lib/jobs/ai-site-publish.js +31 -1
- package/src/lib/jobs/ai-site-publish.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.d.ts +151 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js +183 -0
- package/src/lib/jobs/fixtures/ai-juniper-portfolio-page-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js +192 -0
- package/src/lib/jobs/fixtures/ai-slow-roads-site-plan-recording.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js +222 -0
- package/src/lib/jobs/fixtures/ai-yoga-site-plan-recording.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +29 -0
- package/src/lib/layout-language/ai-layout-compiler.js +698 -12
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +107 -0
- package/src/lib/layout-language/ai-layout-design.js +197 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -0
- package/src/lib/layout-language/ai-layout-frame.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-frame.js +22 -6
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-language.d.ts +2 -0
- package/src/lib/layout-language/ai-layout-language.js +15 -1
- package/src/lib/layout-language/ai-layout-language.js.map +1 -1
- package/src/lib/layout-language/ai-layout-links.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-links.js +4 -0
- package/src/lib/layout-language/ai-layout-links.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +11 -5
- package/src/lib/layout-language/ai-layout-pictures.js +80 -17
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +9 -1
- package/src/lib/model/ai-job-notice.js +23 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +23 -0
- package/src/lib/model/ai-plan-capabilities.js +5 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +12 -2
- package/src/lib/model/ai-site-build-progress.js +78 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +209 -22
- package/src/lib/model/ai-site-job.js +247 -20
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.js +6 -1
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/model/ai-site-look.d.ts +21 -0
- package/src/lib/model/ai-site-look.js +63 -14
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/operator-alerts.d.ts +18 -0
- package/src/lib/operator-alerts.js +42 -0
- package/src/lib/operator-alerts.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +39 -1
- package/src/lib/runtime/ai-doctrine-validators.js +236 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +2 -0
- package/src/lib/runtime/ai-palette.generated.js +18 -2
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-resume.js.map +1 -1
- package/src/lib/server/ai-jobs-route.js +15 -3
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-edit.js +5 -2
- package/src/lib/server/assist-edit.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +54 -0
- package/src/lib/usage/free-ai-credits-left.js +62 -0
- package/src/lib/usage/free-ai-credits-left.js.map +1 -0
|
@@ -49,9 +49,10 @@ import type { AiJobStepContext, AiJobStepRunner } from './ai-job-text-step';
|
|
|
49
49
|
*
|
|
50
50
|
* ── Not on the Free taste ────────────────────────────────────────────────
|
|
51
51
|
*
|
|
52
|
-
* A
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* A guided start's Free site, built on an empty site with its layout and
|
|
53
|
+
* form, leaves 18 credits once its eight sections and its retried page's
|
|
54
|
+
* room are held (`aiFreeSiteSectionsWithin`, AGL-3660), under the 23 one post
|
|
55
|
+
* costs at its worst; and a Free plan includes neither
|
|
55
56
|
* the `commerce` feature nor a product (`productsPerHost` is 0).
|
|
56
57
|
* `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages
|
|
57
58
|
* their kind already describes; the owner adds posts and products after.
|
|
@@ -66,9 +67,8 @@ export declare const AI_SITE_PRODUCTS: {
|
|
|
66
67
|
/** What each part's row reads. */
|
|
67
68
|
export declare const AI_SITE_POSTS_LABEL = "Writing your first posts";
|
|
68
69
|
export declare const AI_SITE_PRODUCTS_LABEL = "Adding your first products";
|
|
69
|
-
/** The blog the posts go in, and the addresses it may answer at
|
|
70
|
-
export
|
|
71
|
-
export declare const AI_SITE_BLOG_SLUGS: readonly ["blog", "posts", "journal", "articles", "writing", "stories"];
|
|
70
|
+
/** The blog the posts go in, and the addresses it may answer at (`model/ai-site-job.ts`). */
|
|
71
|
+
export { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job';
|
|
72
72
|
/** The input a site unit's own job carries what the part needs in. */
|
|
73
73
|
export declare const AI_SITE_CONTENT_INPUT = "siteContent";
|
|
74
74
|
/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */
|
|
@@ -19,6 +19,7 @@ import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-re
|
|
|
19
19
|
import { publishContentEntries, writeContentCollection, writeContentEntryDraft } from "@aglyn/tenant-data-admin/server/content-entry-drafts";
|
|
20
20
|
import { aiProductsProposalOf } from "../model/ai-products.js";
|
|
21
21
|
import { aiSiteKindOfInputs } from "../model/ai-site-kinds.js";
|
|
22
|
+
import { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
|
|
22
23
|
import { AI_STEP_TIERS } from "../providers/catalog.js";
|
|
23
24
|
import { aiModelForStep } from "../providers/routing.js";
|
|
24
25
|
import { AI_BLOG_POST_MAX_TOKENS, AI_BLOG_POST_STEP, generateAiBlogPost } from "../runtime/ai-blog-post-generation.js";
|
|
@@ -55,9 +56,10 @@ import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
|
55
56
|
*
|
|
56
57
|
* ── Not on the Free taste ────────────────────────────────────────────────
|
|
57
58
|
*
|
|
58
|
-
* A
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* A guided start's Free site, built on an empty site with its layout and
|
|
60
|
+
* form, leaves 18 credits once its eight sections and its retried page's
|
|
61
|
+
* room are held (`aiFreeSiteSectionsWithin`, AGL-3660), under the 23 one post
|
|
62
|
+
* costs at its worst; and a Free plan includes neither
|
|
61
63
|
* the `commerce` feature nor a product (`productsPerHost` is 0).
|
|
62
64
|
* `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages
|
|
63
65
|
* their kind already describes; the owner adds posts and products after.
|
|
@@ -68,15 +70,7 @@ import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
|
68
70
|
};
|
|
69
71
|
/** What each part's row reads. */ export const AI_SITE_POSTS_LABEL = 'Writing your first posts';
|
|
70
72
|
export const AI_SITE_PRODUCTS_LABEL = 'Adding your first products';
|
|
71
|
-
/** The blog the posts go in, and the addresses it may answer at
|
|
72
|
-
export const AI_SITE_BLOG_SLUGS = [
|
|
73
|
-
'blog',
|
|
74
|
-
'posts',
|
|
75
|
-
'journal',
|
|
76
|
-
'articles',
|
|
77
|
-
'writing',
|
|
78
|
-
'stories'
|
|
79
|
-
];
|
|
73
|
+
/** The blog the posts go in, and the addresses it may answer at (`model/ai-site-job.ts`). */ export { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from "../model/ai-site-job.js";
|
|
80
74
|
/** The input a site unit's own job carries what the part needs in. */ export const AI_SITE_CONTENT_INPUT = 'siteContent';
|
|
81
75
|
/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */ export function aiSiteContentPart(inputs, freeTaste) {
|
|
82
76
|
var _aiSiteKindOfInputs;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-content.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n publishContentEntries,\n writeContentCollection,\n writeContentEntryDraft,\n} from '@aglyn/tenant-data-admin/server/content-entry-drafts'\nimport { aiProductsProposalOf } from '../model/ai-products'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiSiteKindOfInputs } from '../model/ai-site-kinds'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport {\n AI_BLOG_POST_MAX_TOKENS,\n AI_BLOG_POST_STEP,\n generateAiBlogPost,\n} from '../runtime/ai-blog-post-generation'\nimport { aiLayoutStarterPhotos } from '../layout-language/ai-layout-pictures'\nimport { aiJobAdmissionFor, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * A GUIDED START'S FIRST POSTS AND FIRST PRODUCTS (AGL-3676).\n *\n * A site of the kind \"Blog & writing\" or \"Online store\" got the right look\n * and pages, and nothing real in them: the writing page's cards and the\n * shop's grid named posts and products nobody had written. On a paid plan,\n * the scaffold now builds one more part for those two kinds, before the\n * pages, each its own row with its own timer and credits:\n *\n * - WRITING YOUR FIRST POSTS: a content collection \"Blog\" — the platform's\n * own blog, served at `/{slug}` with an entry page per post — and\n * `AI_SITE_POSTS` posts in it, one generation a pass. Each is written as a\n * draft through the same rules the console's routes hold\n * (`content-entry-drafts.ts`), bylined with the business's name the person\n * gave, and given a starter photo as its cover. They go live with the\n * site: the guided start's publish publishes them with its pages\n * (`aiPublishSitePosts`), as a person's Publish would.\n * - ADDING YOUR FIRST PRODUCTS: the `products` step's catalog, asked for\n * `AI_SITE_PRODUCTS` products, each written through the commerce plugin's\n * `product` draft writer — a DRAFT with no price, off the storefront until\n * the owner prices it, since no price is ever invented. No photo either:\n * the writer takes none, and a stock picture of something else would show\n * a shopper a product that is not the one for sale.\n *\n * The pages built after them are told their titles or names, so the writing\n * page and the shop feature what exists rather than inventing their own.\n *\n * ── Not on the Free taste ────────────────────────────────────────────────\n *\n * A Free site's sections cap is DERIVED to spend the whole taste less one\n * retried section (`aiFreeSiteSectionsWithin`), so there is no slack a post\n * could use without taking a section away; and a Free plan includes neither\n * the `commerce` feature nor a product (`productsPerHost` is 0).\n * `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages\n * their kind already describes; the owner adds posts and products after.\n */\n\n/** How many first posts a paid blog gets. */\nexport const AI_SITE_POSTS = 3\n\n/** How many first products a paid store gets: asked for, and the most written. */\nexport const AI_SITE_PRODUCTS = { min: 3, max: 6 } as const\n\n/** What each part's row reads. */\nexport const AI_SITE_POSTS_LABEL = 'Writing your first posts'\nexport const AI_SITE_PRODUCTS_LABEL = 'Adding your first products'\n\n/** The blog the posts go in, and the addresses it may answer at, in order of preference. */\nexport const AI_SITE_BLOG_NAME = 'Blog'\nexport const AI_SITE_BLOG_SLUGS = ['blog', 'posts', 'journal', 'articles', 'writing', 'stories'] as const\n\n/** The input a site unit's own job carries what the part needs in. */\nexport const AI_SITE_CONTENT_INPUT = 'siteContent'\n\n/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */\nexport function aiSiteContentPart(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n freeTaste: boolean,\n): 'posts' | 'products' | null {\n if (freeTaste) return null\n const kind = aiSiteKindOfInputs(inputs)?.id\n if (kind === 'blog') return 'posts'\n if (kind === 'store') return 'products'\n return null\n}\n\n/**\n * ASSUMED: what one post pass reads and writes beside its generation: the\n * host, the collection's slug claim and the entry's transaction.\n */\nexport const AI_SITE_POST_WRITES_MS = 1_500\n\n/** One post's pass: its generation and re-ask at the `copy.blog` ceiling, and its writes. */\nexport const AI_SITE_POST_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS[AI_BLOG_POST_STEP],\n maxTokens: AI_BLOG_POST_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_SITE_POST_WRITES_MS,\n})\n\n/** What a posts unit's job is told: the posts already written and the addresses the blog must not take. */\nexport interface AiSitePostsInput {\n written: Array<{ id: string; title: string }>\n total: number\n /** Addresses the site's own pages answer at, which the blog must not shadow. */\n avoidSlugs: string[]\n /** Who the posts are by: the business's name as given, else empty. */\n byline: string\n}\n\n/** What a products unit's job is told. */\nexport interface AiSiteProductsInput {\n min: number\n max: number\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nfunction postsInputOf(job: Pick<AiJob, 'inputs'>): AiSitePostsInput {\n const raw = (job.inputs?.[AI_SITE_CONTENT_INPUT] ?? {}) as Partial<AiSitePostsInput>\n return {\n written: Array.isArray(raw.written) ? raw.written.filter((one) => one && typeof one.id === 'string') : [],\n total: typeof raw.total === 'number' && raw.total > 0 ? Math.floor(raw.total) : AI_SITE_POSTS,\n avoidSlugs: Array.isArray(raw.avoidSlugs) ? raw.avoidSlugs.map(str).filter(Boolean) : [],\n byline: str(raw.byline),\n }\n}\n\n/** The id of the `index`th post, of the blog its unit writes under `unitJobId`. */\nexport function aiSitePostId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** The id of the `index`th product a products unit writes. */\nexport function aiSiteProductId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** A site's subdomain and name, as its outputs and its posts' byline read them. */\nasync function readSite(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<{ subdomain: string | null; name: string } | null> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const seo = (host.get('seo') ?? {}) as Record<string, unknown>\n const subdomain = str(host.get('subdomain')) || null\n return { subdomain, name: str(host.get('displayName')) || str(seo['title']) }\n}\n\nconst limitReview = (message: string) => ({ reason: 'limit' as const, message, findings: [] })\n\nexport interface AiSitePostsRunnerDeps {\n generate?: typeof generateAiBlogPost\n writeCollection?: typeof writeContentCollection\n writeEntry?: typeof writeContentEntryDraft\n}\n\n/**\n * The posts unit's runner, on the job the scaffold derived for it: one post a\n * pass, continuing while posts remain. Its first pass makes the blog; every\n * pass finds it again under the unit's own id, and a post under its own id,\n * so a pass repeated after its write reports the post it already wrote.\n */\nexport function createAiSitePostsRunner(deps: AiSitePostsRunnerDeps = {}): AiJobStepRunner {\n const generate = deps.generate ?? generateAiBlogPost\n const writeCollection = deps.writeCollection ?? writeContentCollection\n const writeEntry = deps.writeEntry ?? writeContentEntryDraft\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, signal } = context\n const model = context.modelFor?.(AI_BLOG_POST_STEP) ?? aiModelForStep(AI_BLOG_POST_STEP)\n if (!job.hostId) return { ...aiUnspentOutcome(model), failure: 'Open the site before writing its posts.' }\n const input = postsInputOf(job)\n const index = input.written.length\n if (index >= input.total) return aiUnspentOutcome(model)\n const site = await readSite(firestore, job.hostId)\n if (!site) return { ...aiUnspentOutcome(model), failure: 'This site no longer exists.' }\n const avoid = new Set(input.avoidSlugs.map((slug) => slug.toLowerCase()))\n const blog = await writeCollection(firestore, {\n hostId: job.hostId,\n uid: job.createdBy,\n id: job.$id,\n displayName: AI_SITE_BLOG_NAME,\n slugs: AI_SITE_BLOG_SLUGS.filter((slug) => !avoid.has(slug)),\n now: context.now,\n })\n if (blog.ok === false) return aiUnspentOutcome(model, { review: limitReview(blog.error) })\n\n const generation = await generate({\n brief: job.brief,\n merchantWords: job.brief,\n earlierTitles: input.written.map((post) => post.title),\n index: index + 1,\n total: input.total,\n model,\n maxTokens: AI_SITE_POST_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const post = generation.value\n // A starter photo as its cover, turned by the job so blogs differ.\n const covers = aiLayoutStarterPhotos(\n Array.from({ length: input.total }, (_, slot) => ({\n imageId: `cover${slot}`,\n frameId: null,\n iconId: null,\n alt: '',\n aspect: 16 / 9,\n sectionIndex: slot + 1,\n role: 'gallery' as const,\n })),\n job.$id,\n )\n const byline = input.byline || site.name\n const written = await writeEntry(firestore, {\n hostId: job.hostId,\n collectionId: blog.id,\n uid: job.createdBy,\n id: aiSitePostId(job.$id, index),\n content: {\n title: post.title,\n excerpt: post.excerpt,\n body: post.body,\n seoDescription: post.seoDescription,\n coverImage: covers[index]?.src,\n ...(byline ? { authorName: byline } : {}),\n },\n now: context.now,\n })\n if (written.ok === false) return { ...spent, review: limitReview(written.error) }\n const output: AiJobOutput = {\n resource: 'entry',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: site.subdomain,\n label: written.title,\n note: `A post in ${blog.displayName}, at /${blog.slug}/${written.slug}. It goes live with the site.`,\n proposal: { collectionId: blog.id, collectionSlug: blog.slug, slug: written.slug },\n }\n return { ...spent, outputs: [output], ...(index + 1 < input.total ? { continue: true } : {}) }\n }\n}\n\nexport const runAiSitePostsUnit = createAiSitePostsRunner()\n\nexport interface AiSiteProductsRunnerDeps {\n /** The catalog's runner: the `products` step's. */\n catalog: AiJobStepRunner\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/** The sentence the products unit's brief ends with, which the catalog's rules read as how many. */\nexport function aiSiteProductsBriefLine(input: AiSiteProductsInput = AI_SITE_PRODUCTS): string {\n return `Propose between ${input.min} and ${input.max} products: the store's first ones.`\n}\n\n/**\n * The products unit's runner: the `products` step's catalog for the store's\n * brief, then each proposed product, up to the most a site starts with,\n * written as an unpriced draft by the plugin that keeps products. A product\n * past the plan's allowance stops the writing there, and the row says how\n * many were added; none at all is the allowance's review.\n */\nexport function createAiSiteProductsRunner(deps: AiSiteProductsRunnerDeps): AiJobStepRunner {\n const writerFor = deps.writerFor ?? pluginResourceDraftWriter\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n const keeper = writerFor('product')\n const model = context.modelFor?.('job.products') ?? aiModelForStep('job.products')\n if (!keeper || !job.hostId) return { ...aiUnspentOutcome(model), failure: 'Products are not available on this site.' }\n const outcome = await deps.catalog({\n ...context,\n job: { ...job, kind: 'products', inputs: { ...job.inputs, target: 'catalog' } },\n })\n if (outcome.refused || outcome.failure || outcome.review) return outcome\n const catalog = outcome.outputs.map((output) => ({ output, proposal: aiProductsProposalOf(output) })).find(\n (entry) => entry.proposal?.kind === 'catalog',\n )\n if (!catalog || catalog.proposal?.kind !== 'catalog') return { ...outcome, outputs: [] }\n const proposed = catalog.proposal.products.slice(0, AI_SITE_PRODUCTS.max)\n const outputs: AiJobOutput[] = []\n let stopped: string | null = null\n for (const [index, product] of proposed.entries()) {\n const written = await keeper.writer.write({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n id: aiSiteProductId(job.$id, index),\n name: product.name,\n // No price: the owner sets it. No photo: the writer takes none.\n content: {\n name: product.name,\n type: product.type,\n description: product.description,\n tags: product.tags,\n options: product.options,\n seoTitle: product.seoTitle,\n seoDescription: product.seoDescription,\n },\n })\n if (written.ok === false) {\n stopped = written.error\n break\n }\n outputs.push({\n resource: 'product',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: catalog.output.hostSubdomain ?? null,\n label: written.name,\n note: 'A draft with no price. Set its price and photo in Products to put it on sale.',\n })\n }\n if (!outputs.length) {\n return { ...outcome, outputs: [], review: limitReview(stopped ?? 'No product could be added.') }\n }\n // The allowance that stopped the rest is said where the last one added is.\n if (stopped) {\n const last = outputs[outputs.length - 1]\n last.note = `${last.note} ${outputs.length} of ${proposed.length} added: ${stopped}.`\n }\n return { ...outcome, outputs }\n }\n}\n\n/**\n * Whether a part may be built for this member on this site, asked before its\n * first pass so a refusal spends nothing; `null` admits. Products ask what a\n * `products` job asks (the plan's commerce feature, Commerce on and released\n * for the site) and the writer's own refusal (role, allowance); posts ask\n * that the member may write the site's content.\n */\nexport async function aiSiteContentRefusal(\n part: 'posts' | 'products',\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy' | 'inputs'> },\n deps: { writerFor?: typeof pluginResourceDraftWriter; admissionFor?: typeof aiJobAdmissionFor } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (part === 'posts') {\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n }\n const admission = (deps.admissionFor ?? aiJobAdmissionFor)('products')\n const refused: AiJobAdmissionRefusal | null = admission\n ? await admission({\n firestore,\n orgId: job.orgId,\n hostId: job.hostId,\n inputs: { target: 'catalog' },\n org: context.org ?? null,\n uid: job.createdBy,\n })\n : { status: 403, error: 'Products are not available on this site.' }\n if (refused) return refused.error\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)('product')\n if (!keeper) return 'Products are not available on this site.'\n const room = await keeper.writer.refusal({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n })\n return room?.error ?? null\n}\n\n/** The guided start's posts, published with its pages (AGL-3676). */\nexport interface AiSitePostsPublish {\n published: number\n kept: number\n /** The blog's own addresses, for the publish's cache drop. */\n paths: string[]\n}\n\n/**\n * Publishes the posts a guided start wrote, as the entry editor's Publish\n * does, once its pages are live. Never throws: a post that stays a draft is\n * the owner's to publish from Content.\n */\nexport async function aiPublishSitePosts(\n firestore: FirebaseFirestore.Firestore,\n input: { job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'createdBy'>; outputs: readonly AiJobOutput[]; now: Date },\n publish: typeof publishContentEntries = publishContentEntries,\n): Promise<AiSitePostsPublish | null> {\n const posts = input.outputs.filter((output) => output.resource === 'entry')\n const collectionId = str(posts[0]?.proposal?.['collectionId'])\n const collectionSlug = str(posts[0]?.proposal?.['collectionSlug'])\n if (!input.job.hostId || !posts.length || !collectionId) return null\n try {\n const result = await publish(firestore, {\n hostId: input.job.hostId,\n collectionId,\n uid: input.job.createdBy,\n ids: posts.map((post) => post.id),\n now: input.now,\n })\n if ('ok' in result) {\n console.warn('ai site posts not published', { jobId: input.job.$id, error: result.error })\n return { published: 0, kept: posts.length, paths: [] }\n }\n const slugs = posts\n .filter((post) => result.published.includes(post.id))\n .map((post) => str(post.proposal?.['slug']))\n .filter(Boolean)\n return {\n published: result.published.length,\n kept: result.kept.length,\n paths: collectionSlug ? [`/${collectionSlug}`, ...slugs.map((slug) => `/${collectionSlug}/${slug}`)] : [],\n }\n } catch (error) {\n console.error('ai site posts publish threw', { orgId: input.job.orgId, jobId: input.job.$id, error })\n return null\n }\n}\n\n/** What a page of a blog or a store is told about the posts or products built before it. */\nexport function aiSiteContentBriefLines(outputs: readonly AiJobOutput[]): string[] {\n const posts = outputs.filter((output) => output.resource === 'entry')\n const products = outputs.filter((output) => output.resource === 'product' && !output.proposal)\n const lines: string[] = []\n if (posts.length) {\n const slug = str(posts[0].proposal?.['collectionSlug'])\n lines.push(\n `This site's blog${slug ? ` at /${slug}` : ''} has these posts: ${posts.map((post) => `“${post.label}”`).join(', ')}.`,\n 'Where a page features writing, feature these posts by their titles; never name a post that is not in this list.',\n )\n }\n if (products.length) {\n lines.push(\n `This store's products are: ${products.map((product) => `“${product.label}”`).join(', ')}.`,\n 'Where a page features products, feature these by their names; never name another product, and never state a price.',\n )\n }\n return lines\n}\n"],"names":["hostRoleCanWrite","pluginResourceDraftWriter","publishContentEntries","writeContentCollection","writeContentEntryDraft","aiProductsProposalOf","aiSiteKindOfInputs","AI_STEP_TIERS","aiModelForStep","AI_BLOG_POST_MAX_TOKENS","AI_BLOG_POST_STEP","generateAiBlogPost","aiLayoutStarterPhotos","aiJobAdmissionFor","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_SITE_POSTS","AI_SITE_PRODUCTS","min","max","AI_SITE_POSTS_LABEL","AI_SITE_PRODUCTS_LABEL","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","AI_SITE_CONTENT_INPUT","aiSiteContentPart","inputs","freeTaste","kind","id","AI_SITE_POST_WRITES_MS","AI_SITE_POST_BUDGET","tier","maxTokens","lookups","ownReadsMs","str","value","trim","postsInputOf","job","raw","written","Array","isArray","filter","one","total","Math","floor","avoidSlugs","map","Boolean","byline","aiSitePostId","unitJobId","index","aiSiteProductId","readSite","firestore","hostId","host","collection","doc","get","exists","seo","subdomain","name","limitReview","message","reason","findings","createAiSitePostsRunner","deps","generate","writeCollection","writeEntry","context","covers","signal","model","modelFor","failure","input","length","site","avoid","Set","slug","toLowerCase","blog","uid","createdBy","$id","displayName","slugs","has","now","ok","review","error","generation","brief","merchantWords","earlierTitles","post","title","spent","status","refused","from","_","slot","imageId","frameId","iconId","alt","aspect","sectionIndex","role","collectionId","content","excerpt","body","seoDescription","coverImage","src","authorName","output","resource","hostSubdomain","label","note","proposal","collectionSlug","outputs","continue","runAiSitePostsUnit","aiSiteProductsBriefLine","createAiSiteProductsRunner","writerFor","catalog","keeper","outcome","target","find","entry","proposed","products","slice","stopped","product","entries","writer","write","orgId","org","type","description","tags","options","seoTitle","push","last","aiSiteContentRefusal","part","admission","admissionFor","room","refusal","aiPublishSitePosts","publish","posts","result","ids","console","warn","jobId","published","kept","paths","includes","aiSiteContentBriefLines","lines","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-site-content.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport {\n publishContentEntries,\n writeContentCollection,\n writeContentEntryDraft,\n} from '@aglyn/tenant-data-admin/server/content-entry-drafts'\nimport { aiProductsProposalOf } from '../model/ai-products'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiSiteKindOfInputs } from '../model/ai-site-kinds'\nimport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\nimport { AI_STEP_TIERS } from '../providers/catalog'\nimport { aiModelForStep } from '../providers/routing'\nimport {\n AI_BLOG_POST_MAX_TOKENS,\n AI_BLOG_POST_STEP,\n generateAiBlogPost,\n} from '../runtime/ai-blog-post-generation'\nimport { aiLayoutStarterPhotos } from '../layout-language/ai-layout-pictures'\nimport { aiJobAdmissionFor, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { aiJobStepBudget } from './ai-job-budget'\nimport { aiGenerationSpent, aiUnspentOutcome } from './ai-job-generation'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\n\n/**\n * A GUIDED START'S FIRST POSTS AND FIRST PRODUCTS (AGL-3676).\n *\n * A site of the kind \"Blog & writing\" or \"Online store\" got the right look\n * and pages, and nothing real in them: the writing page's cards and the\n * shop's grid named posts and products nobody had written. On a paid plan,\n * the scaffold now builds one more part for those two kinds, before the\n * pages, each its own row with its own timer and credits:\n *\n * - WRITING YOUR FIRST POSTS: a content collection \"Blog\" — the platform's\n * own blog, served at `/{slug}` with an entry page per post — and\n * `AI_SITE_POSTS` posts in it, one generation a pass. Each is written as a\n * draft through the same rules the console's routes hold\n * (`content-entry-drafts.ts`), bylined with the business's name the person\n * gave, and given a starter photo as its cover. They go live with the\n * site: the guided start's publish publishes them with its pages\n * (`aiPublishSitePosts`), as a person's Publish would.\n * - ADDING YOUR FIRST PRODUCTS: the `products` step's catalog, asked for\n * `AI_SITE_PRODUCTS` products, each written through the commerce plugin's\n * `product` draft writer — a DRAFT with no price, off the storefront until\n * the owner prices it, since no price is ever invented. No photo either:\n * the writer takes none, and a stock picture of something else would show\n * a shopper a product that is not the one for sale.\n *\n * The pages built after them are told their titles or names, so the writing\n * page and the shop feature what exists rather than inventing their own.\n *\n * ── Not on the Free taste ────────────────────────────────────────────────\n *\n * A guided start's Free site, built on an empty site with its layout and\n * form, leaves 18 credits once its eight sections and its retried page's\n * room are held (`aiFreeSiteSectionsWithin`, AGL-3660), under the 23 one post\n * costs at its worst; and a Free plan includes neither\n * the `commerce` feature nor a product (`productsPerHost` is 0).\n * `ai-job-free-site.spec.ts` shows the arithmetic. Free sites keep the pages\n * their kind already describes; the owner adds posts and products after.\n */\n\n/** How many first posts a paid blog gets. */\nexport const AI_SITE_POSTS = 3\n\n/** How many first products a paid store gets: asked for, and the most written. */\nexport const AI_SITE_PRODUCTS = { min: 3, max: 6 } as const\n\n/** What each part's row reads. */\nexport const AI_SITE_POSTS_LABEL = 'Writing your first posts'\nexport const AI_SITE_PRODUCTS_LABEL = 'Adding your first products'\n\n/** The blog the posts go in, and the addresses it may answer at (`model/ai-site-job.ts`). */\nexport { AI_SITE_BLOG_NAME, AI_SITE_BLOG_SLUGS } from '../model/ai-site-job'\n\n/** The input a site unit's own job carries what the part needs in. */\nexport const AI_SITE_CONTENT_INPUT = 'siteContent'\n\n/** Which part a site of these answers gets; `null` for the Free taste and every other kind. */\nexport function aiSiteContentPart(\n inputs: Readonly<Record<string, unknown>> | null | undefined,\n freeTaste: boolean,\n): 'posts' | 'products' | null {\n if (freeTaste) return null\n const kind = aiSiteKindOfInputs(inputs)?.id\n if (kind === 'blog') return 'posts'\n if (kind === 'store') return 'products'\n return null\n}\n\n/**\n * ASSUMED: what one post pass reads and writes beside its generation: the\n * host, the collection's slug claim and the entry's transaction.\n */\nexport const AI_SITE_POST_WRITES_MS = 1_500\n\n/** One post's pass: its generation and re-ask at the `copy.blog` ceiling, and its writes. */\nexport const AI_SITE_POST_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS[AI_BLOG_POST_STEP],\n maxTokens: AI_BLOG_POST_MAX_TOKENS,\n lookups: 0,\n ownReadsMs: AI_SITE_POST_WRITES_MS,\n})\n\n/** What a posts unit's job is told: the posts already written and the addresses the blog must not take. */\nexport interface AiSitePostsInput {\n written: Array<{ id: string; title: string }>\n total: number\n /** Addresses the site's own pages answer at, which the blog must not shadow. */\n avoidSlugs: string[]\n /** Who the posts are by: the business's name as given, else empty. */\n byline: string\n}\n\n/** What a products unit's job is told. */\nexport interface AiSiteProductsInput {\n min: number\n max: number\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nfunction postsInputOf(job: Pick<AiJob, 'inputs'>): AiSitePostsInput {\n const raw = (job.inputs?.[AI_SITE_CONTENT_INPUT] ?? {}) as Partial<AiSitePostsInput>\n return {\n written: Array.isArray(raw.written) ? raw.written.filter((one) => one && typeof one.id === 'string') : [],\n total: typeof raw.total === 'number' && raw.total > 0 ? Math.floor(raw.total) : AI_SITE_POSTS,\n avoidSlugs: Array.isArray(raw.avoidSlugs) ? raw.avoidSlugs.map(str).filter(Boolean) : [],\n byline: str(raw.byline),\n }\n}\n\n/** The id of the `index`th post, of the blog its unit writes under `unitJobId`. */\nexport function aiSitePostId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** The id of the `index`th product a products unit writes. */\nexport function aiSiteProductId(unitJobId: string, index: number): string {\n return `${unitJobId}-${index}`\n}\n\n/** A site's subdomain and name, as its outputs and its posts' byline read them. */\nasync function readSite(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n): Promise<{ subdomain: string | null; name: string } | null> {\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return null\n const seo = (host.get('seo') ?? {}) as Record<string, unknown>\n const subdomain = str(host.get('subdomain')) || null\n return { subdomain, name: str(host.get('displayName')) || str(seo['title']) }\n}\n\nconst limitReview = (message: string) => ({ reason: 'limit' as const, message, findings: [] })\n\nexport interface AiSitePostsRunnerDeps {\n generate?: typeof generateAiBlogPost\n writeCollection?: typeof writeContentCollection\n writeEntry?: typeof writeContentEntryDraft\n}\n\n/**\n * The posts unit's runner, on the job the scaffold derived for it: one post a\n * pass, continuing while posts remain. Its first pass makes the blog; every\n * pass finds it again under the unit's own id, and a post under its own id,\n * so a pass repeated after its write reports the post it already wrote.\n */\nexport function createAiSitePostsRunner(deps: AiSitePostsRunnerDeps = {}): AiJobStepRunner {\n const generate = deps.generate ?? generateAiBlogPost\n const writeCollection = deps.writeCollection ?? writeContentCollection\n const writeEntry = deps.writeEntry ?? writeContentEntryDraft\n return async (context): Promise<AiJobStepOutcome> => {\n const { job, firestore, signal } = context\n const model = context.modelFor?.(AI_BLOG_POST_STEP) ?? aiModelForStep(AI_BLOG_POST_STEP)\n if (!job.hostId) return { ...aiUnspentOutcome(model), failure: 'Open the site before writing its posts.' }\n const input = postsInputOf(job)\n const index = input.written.length\n if (index >= input.total) return aiUnspentOutcome(model)\n const site = await readSite(firestore, job.hostId)\n if (!site) return { ...aiUnspentOutcome(model), failure: 'This site no longer exists.' }\n const avoid = new Set(input.avoidSlugs.map((slug) => slug.toLowerCase()))\n const blog = await writeCollection(firestore, {\n hostId: job.hostId,\n uid: job.createdBy,\n id: job.$id,\n displayName: AI_SITE_BLOG_NAME,\n slugs: AI_SITE_BLOG_SLUGS.filter((slug) => !avoid.has(slug)),\n now: context.now,\n })\n if (blog.ok === false) return aiUnspentOutcome(model, { review: limitReview(blog.error) })\n\n const generation = await generate({\n brief: job.brief,\n merchantWords: job.brief,\n earlierTitles: input.written.map((post) => post.title),\n index: index + 1,\n total: input.total,\n model,\n maxTokens: AI_SITE_POST_BUDGET.maxTokens(model),\n ...(signal ? { signal } : {}),\n })\n const spent = aiGenerationSpent(generation)\n if (generation.status === 'refused') return { ...spent, refused: true }\n if (generation.status === 'needs_input') return { ...spent, failure: generation.message }\n const post = generation.value\n // A starter photo as its cover, turned by the job so blogs differ.\n const covers = aiLayoutStarterPhotos(\n Array.from({ length: input.total }, (_, slot) => ({\n imageId: `cover${slot}`,\n frameId: null,\n iconId: null,\n alt: '',\n aspect: 16 / 9,\n sectionIndex: slot + 1,\n role: 'gallery' as const,\n })),\n job.$id,\n )\n const byline = input.byline || site.name\n const written = await writeEntry(firestore, {\n hostId: job.hostId,\n collectionId: blog.id,\n uid: job.createdBy,\n id: aiSitePostId(job.$id, index),\n content: {\n title: post.title,\n excerpt: post.excerpt,\n body: post.body,\n seoDescription: post.seoDescription,\n coverImage: covers[index]?.src,\n ...(byline ? { authorName: byline } : {}),\n },\n now: context.now,\n })\n if (written.ok === false) return { ...spent, review: limitReview(written.error) }\n const output: AiJobOutput = {\n resource: 'entry',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: site.subdomain,\n label: written.title,\n note: `A post in ${blog.displayName}, at /${blog.slug}/${written.slug}. It goes live with the site.`,\n proposal: { collectionId: blog.id, collectionSlug: blog.slug, slug: written.slug },\n }\n return { ...spent, outputs: [output], ...(index + 1 < input.total ? { continue: true } : {}) }\n }\n}\n\nexport const runAiSitePostsUnit = createAiSitePostsRunner()\n\nexport interface AiSiteProductsRunnerDeps {\n /** The catalog's runner: the `products` step's. */\n catalog: AiJobStepRunner\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/** The sentence the products unit's brief ends with, which the catalog's rules read as how many. */\nexport function aiSiteProductsBriefLine(input: AiSiteProductsInput = AI_SITE_PRODUCTS): string {\n return `Propose between ${input.min} and ${input.max} products: the store's first ones.`\n}\n\n/**\n * The products unit's runner: the `products` step's catalog for the store's\n * brief, then each proposed product, up to the most a site starts with,\n * written as an unpriced draft by the plugin that keeps products. A product\n * past the plan's allowance stops the writing there, and the row says how\n * many were added; none at all is the allowance's review.\n */\nexport function createAiSiteProductsRunner(deps: AiSiteProductsRunnerDeps): AiJobStepRunner {\n const writerFor = deps.writerFor ?? pluginResourceDraftWriter\n return async (context): Promise<AiJobStepOutcome> => {\n const { job } = context\n const keeper = writerFor('product')\n const model = context.modelFor?.('job.products') ?? aiModelForStep('job.products')\n if (!keeper || !job.hostId) return { ...aiUnspentOutcome(model), failure: 'Products are not available on this site.' }\n const outcome = await deps.catalog({\n ...context,\n job: { ...job, kind: 'products', inputs: { ...job.inputs, target: 'catalog' } },\n })\n if (outcome.refused || outcome.failure || outcome.review) return outcome\n const catalog = outcome.outputs.map((output) => ({ output, proposal: aiProductsProposalOf(output) })).find(\n (entry) => entry.proposal?.kind === 'catalog',\n )\n if (!catalog || catalog.proposal?.kind !== 'catalog') return { ...outcome, outputs: [] }\n const proposed = catalog.proposal.products.slice(0, AI_SITE_PRODUCTS.max)\n const outputs: AiJobOutput[] = []\n let stopped: string | null = null\n for (const [index, product] of proposed.entries()) {\n const written = await keeper.writer.write({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n id: aiSiteProductId(job.$id, index),\n name: product.name,\n // No price: the owner sets it. No photo: the writer takes none.\n content: {\n name: product.name,\n type: product.type,\n description: product.description,\n tags: product.tags,\n options: product.options,\n seoTitle: product.seoTitle,\n seoDescription: product.seoDescription,\n },\n })\n if (written.ok === false) {\n stopped = written.error\n break\n }\n outputs.push({\n resource: 'product',\n id: written.id,\n hostId: job.hostId,\n hostSubdomain: catalog.output.hostSubdomain ?? null,\n label: written.name,\n note: 'A draft with no price. Set its price and photo in Products to put it on sale.',\n })\n }\n if (!outputs.length) {\n return { ...outcome, outputs: [], review: limitReview(stopped ?? 'No product could be added.') }\n }\n // The allowance that stopped the rest is said where the last one added is.\n if (stopped) {\n const last = outputs[outputs.length - 1]\n last.note = `${last.note} ${outputs.length} of ${proposed.length} added: ${stopped}.`\n }\n return { ...outcome, outputs }\n }\n}\n\n/**\n * Whether a part may be built for this member on this site, asked before its\n * first pass so a refusal spends nothing; `null` admits. Products ask what a\n * `products` job asks (the plan's commerce feature, Commerce on and released\n * for the site) and the writer's own refusal (role, allowance); posts ask\n * that the member may write the site's content.\n */\nexport async function aiSiteContentRefusal(\n part: 'posts' | 'products',\n context: Pick<AiJobStepContext, 'firestore' | 'org' | 'now'> & { job: Pick<AiJob, 'orgId' | 'hostId' | 'createdBy' | 'inputs'> },\n deps: { writerFor?: typeof pluginResourceDraftWriter; admissionFor?: typeof aiJobAdmissionFor } = {},\n): Promise<string | null> {\n const { job, firestore } = context\n if (!job.hostId) return 'Open the site first.'\n if (part === 'posts') {\n const host = await firestore.collection('hosts').doc(job.hostId).get()\n return hostRoleCanWrite((host.get('memberRoles') ?? {})[job.createdBy]) ? null : 'Editing requires the editor role'\n }\n const admission = (deps.admissionFor ?? aiJobAdmissionFor)('products')\n const refused: AiJobAdmissionRefusal | null = admission\n ? await admission({\n firestore,\n orgId: job.orgId,\n hostId: job.hostId,\n inputs: { target: 'catalog' },\n org: context.org ?? null,\n uid: job.createdBy,\n })\n : { status: 403, error: 'Products are not available on this site.' }\n if (refused) return refused.error\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)('product')\n if (!keeper) return 'Products are not available on this site.'\n const room = await keeper.writer.refusal({\n orgId: job.orgId,\n hostId: job.hostId,\n uid: job.createdBy,\n org: (context.org ?? null) as Readonly<Record<string, unknown>> | null,\n now: context.now,\n })\n return room?.error ?? null\n}\n\n/** The guided start's posts, published with its pages (AGL-3676). */\nexport interface AiSitePostsPublish {\n published: number\n kept: number\n /** The blog's own addresses, for the publish's cache drop. */\n paths: string[]\n}\n\n/**\n * Publishes the posts a guided start wrote, as the entry editor's Publish\n * does, once its pages are live. Never throws: a post that stays a draft is\n * the owner's to publish from Content.\n */\nexport async function aiPublishSitePosts(\n firestore: FirebaseFirestore.Firestore,\n input: { job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'createdBy'>; outputs: readonly AiJobOutput[]; now: Date },\n publish: typeof publishContentEntries = publishContentEntries,\n): Promise<AiSitePostsPublish | null> {\n const posts = input.outputs.filter((output) => output.resource === 'entry')\n const collectionId = str(posts[0]?.proposal?.['collectionId'])\n const collectionSlug = str(posts[0]?.proposal?.['collectionSlug'])\n if (!input.job.hostId || !posts.length || !collectionId) return null\n try {\n const result = await publish(firestore, {\n hostId: input.job.hostId,\n collectionId,\n uid: input.job.createdBy,\n ids: posts.map((post) => post.id),\n now: input.now,\n })\n if ('ok' in result) {\n console.warn('ai site posts not published', { jobId: input.job.$id, error: result.error })\n return { published: 0, kept: posts.length, paths: [] }\n }\n const slugs = posts\n .filter((post) => result.published.includes(post.id))\n .map((post) => str(post.proposal?.['slug']))\n .filter(Boolean)\n return {\n published: result.published.length,\n kept: result.kept.length,\n paths: collectionSlug ? [`/${collectionSlug}`, ...slugs.map((slug) => `/${collectionSlug}/${slug}`)] : [],\n }\n } catch (error) {\n console.error('ai site posts publish threw', { orgId: input.job.orgId, jobId: input.job.$id, error })\n return null\n }\n}\n\n/** What a page of a blog or a store is told about the posts or products built before it. */\nexport function aiSiteContentBriefLines(outputs: readonly AiJobOutput[]): string[] {\n const posts = outputs.filter((output) => output.resource === 'entry')\n const products = outputs.filter((output) => output.resource === 'product' && !output.proposal)\n const lines: string[] = []\n if (posts.length) {\n const slug = str(posts[0].proposal?.['collectionSlug'])\n lines.push(\n `This site's blog${slug ? ` at /${slug}` : ''} has these posts: ${posts.map((post) => `“${post.label}”`).join(', ')}.`,\n 'Where a page features writing, feature these posts by their titles; never name a post that is not in this list.',\n )\n }\n if (products.length) {\n lines.push(\n `This store's products are: ${products.map((product) => `“${product.label}”`).join(', ')}.`,\n 'Where a page features products, feature these by their names; never name another product, and never state a price.',\n )\n }\n return lines\n}\n"],"names":["hostRoleCanWrite","pluginResourceDraftWriter","publishContentEntries","writeContentCollection","writeContentEntryDraft","aiProductsProposalOf","aiSiteKindOfInputs","AI_SITE_BLOG_NAME","AI_SITE_BLOG_SLUGS","AI_STEP_TIERS","aiModelForStep","AI_BLOG_POST_MAX_TOKENS","AI_BLOG_POST_STEP","generateAiBlogPost","aiLayoutStarterPhotos","aiJobAdmissionFor","aiJobStepBudget","aiGenerationSpent","aiUnspentOutcome","AI_SITE_POSTS","AI_SITE_PRODUCTS","min","max","AI_SITE_POSTS_LABEL","AI_SITE_PRODUCTS_LABEL","AI_SITE_CONTENT_INPUT","aiSiteContentPart","inputs","freeTaste","kind","id","AI_SITE_POST_WRITES_MS","AI_SITE_POST_BUDGET","tier","maxTokens","lookups","ownReadsMs","str","value","trim","postsInputOf","job","raw","written","Array","isArray","filter","one","total","Math","floor","avoidSlugs","map","Boolean","byline","aiSitePostId","unitJobId","index","aiSiteProductId","readSite","firestore","hostId","host","collection","doc","get","exists","seo","subdomain","name","limitReview","message","reason","findings","createAiSitePostsRunner","deps","generate","writeCollection","writeEntry","context","covers","signal","model","modelFor","failure","input","length","site","avoid","Set","slug","toLowerCase","blog","uid","createdBy","$id","displayName","slugs","has","now","ok","review","error","generation","brief","merchantWords","earlierTitles","post","title","spent","status","refused","from","_","slot","imageId","frameId","iconId","alt","aspect","sectionIndex","role","collectionId","content","excerpt","body","seoDescription","coverImage","src","authorName","output","resource","hostSubdomain","label","note","proposal","collectionSlug","outputs","continue","runAiSitePostsUnit","aiSiteProductsBriefLine","createAiSiteProductsRunner","writerFor","catalog","keeper","outcome","target","find","entry","proposed","products","slice","stopped","product","entries","writer","write","orgId","org","type","description","tags","options","seoTitle","push","last","aiSiteContentRefusal","part","admission","admissionFor","room","refusal","aiPublishSitePosts","publish","posts","result","ids","console","warn","jobId","published","kept","paths","includes","aiSiteContentBriefLines","lines","join"],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import { type AiBuildPlanCreate, type AiBuildPlanScreen } from '../model/ai-buil
|
|
|
18
18
|
import type { AiJob, AiJobItemLedger, AiJobKind, AiJobOutput, AiJobOutputResource, AiJobPlan } from '../model/ai-jobs.types';
|
|
19
19
|
import { type AiBuildUnit } from '../model/ai-build-job';
|
|
20
20
|
import { type AiSiteJobInputs } from '../model/ai-site-job';
|
|
21
|
+
import { readFreeAiCreditsLeft } from '../usage/free-ai-credits-left';
|
|
21
22
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
22
23
|
import { readAiDraftNodes } from './ai-job-drafts';
|
|
23
24
|
import { aiPublishGuidedSite } from './ai-site-publish';
|
|
@@ -219,11 +220,21 @@ export declare function aiSiteUnitJobId(job: Pick<AiJob, '$id' | 'steps'>, unit:
|
|
|
219
220
|
* for leaving it out.
|
|
220
221
|
*/
|
|
221
222
|
export declare function aiSiteUnitJob(job: AiJob, unit: AiSiteUnit, built: BuiltRefs): AiJob;
|
|
223
|
+
/**
|
|
224
|
+
* Whether this site start writes its first posts (AGL-3676): its ledger owes
|
|
225
|
+
* the posts part and has not given up on it. The ledger is written before the
|
|
226
|
+
* layout is built, so the header knows the blog before it exists.
|
|
227
|
+
*/
|
|
228
|
+
export declare function aiSiteWritesPosts(job: Pick<AiJob, 'items'>): boolean;
|
|
222
229
|
/**
|
|
223
230
|
* A scaffold is admitted with inputs that read, for a site of the job's own
|
|
224
231
|
* org, and only where this deployment has loaded the step that builds a page:
|
|
225
232
|
* a scaffold whose pages nothing can build is not a scaffold.
|
|
226
233
|
*/
|
|
234
|
+
export declare function createAiSiteJobAdmission(deps?: {
|
|
235
|
+
freeCreditsLeft?: typeof readFreeAiCreditsLeft;
|
|
236
|
+
now?: () => Date;
|
|
237
|
+
}): AiJobAdmission;
|
|
227
238
|
export declare const aiSiteJobAdmission: AiJobAdmission;
|
|
228
239
|
/** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */
|
|
229
240
|
export declare function aiSiteFreeTaste(org: object | null | undefined): boolean;
|
|
@@ -19,13 +19,14 @@ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizatio
|
|
|
19
19
|
import { isAiPlanNewRef } from "../model/ai-build-plan.js";
|
|
20
20
|
import { aiBuildDegradation, aiBuildInitialLedger, aiBuildItemDelivered, aiBuildItemOpen, aiBuildNextUnit } from "../model/ai-build-job.js";
|
|
21
21
|
import { aiBuildBuiltRefs, aiUnitErrorRetryable, aiUnitFailure, aiUnitSpend } from "./ai-build-unit-outcome.js";
|
|
22
|
-
import { AI_SITE_EMAIL_TYPE, AI_SITE_MAX_SECTIONS, AI_SITE_PAGES, aiSiteNameSentence, aiSitePagesRefusal, aiSitePlanRefusal, aiSiteSubmissions, aiSiteWords, parseAiSiteJobInputs } from "../model/ai-site-job.js";
|
|
22
|
+
import { AI_SITE_EMAIL_TYPE, AI_SITE_MAX_SECTIONS, AI_SITE_PAGES, aiFreeSiteShortfall, aiFreeSiteShortfallText, aiSiteNameSentence, aiSitePagesRefusal, aiSitePlanRefusal, aiSiteSubmissions, aiSiteWords, parseAiSiteJobInputs, aiSiteBlogNavPage } from "../model/ai-site-job.js";
|
|
23
|
+
import { readFreeAiCreditsLeft } from "../usage/free-ai-credits-left.js";
|
|
23
24
|
import { AI_SITE_SEO_OUTPUT_ID, aiSiteSeoProposalForInputs } from "../model/ai-site-start-seo.js";
|
|
24
25
|
import { aiModelForStep } from "../providers/routing.js";
|
|
25
26
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
26
27
|
import { aiOriginJobId, aiRecordedJobDraftId } from "./ai-job-draft-ids.js";
|
|
27
28
|
import { readAiDraftNodes } from "./ai-job-drafts.js";
|
|
28
|
-
import { AI_LAYOUT_SITE_PAGES_INPUT, aiLayoutSitePagesOfPlan } from "./ai-job-layout-site-pages.js";
|
|
29
|
+
import { AI_LAYOUT_SITE_PAGES_INPUT, AI_LAYOUT_SITE_PAGES_MAX, aiLayoutIsHomeSlug, aiLayoutSitePagesOfPlan } from "./ai-job-layout-site-pages.js";
|
|
29
30
|
import { aiPageSectionNodeId } from "./ai-job-page-sections.js";
|
|
30
31
|
import { AI_LAYOUT_FORM_PAGE_INPUT, AI_LAYOUT_LANGUAGE_INPUT, aiLayoutFormPageOfPlan } from "./ai-job-page-language.js";
|
|
31
32
|
import { aiJobPublishesSite, aiPublishGuidedSite } from "./ai-site-publish.js";
|
|
@@ -429,10 +430,22 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
|
|
|
429
430
|
// ids are minted on the plan, and the platform writes the header's links.
|
|
430
431
|
// A page is told them too, so its buttons may go to a page built after it.
|
|
431
432
|
if (job.kind === 'site' && (unit.kind === 'layout' || unit.kind === 'page')) {
|
|
432
|
-
// A guided start links every page the person asked for (AGL-3660)
|
|
433
|
-
|
|
433
|
+
// A guided start links every page the person asked for (AGL-3660), and
|
|
434
|
+
// the blog its first posts are written into, by its path, second after
|
|
435
|
+
// Home (AGL-3676): the live Slow Roads start linked an "Articles" page
|
|
436
|
+
// and never the blog.
|
|
437
|
+
const planned = aiLayoutSitePagesOfPlan(plan.screens, {
|
|
434
438
|
guided: true
|
|
435
439
|
});
|
|
440
|
+
const blog = aiSiteWritesPosts(job) ? [
|
|
441
|
+
aiSiteBlogNavPage(plan.screens)
|
|
442
|
+
] : [];
|
|
443
|
+
const homes = planned.filter((page)=>aiLayoutIsHomeSlug(page.slug));
|
|
444
|
+
const pages = [
|
|
445
|
+
...homes,
|
|
446
|
+
...blog,
|
|
447
|
+
...planned.filter((page)=>!aiLayoutIsHomeSlug(page.slug))
|
|
448
|
+
].slice(0, AI_LAYOUT_SITE_PAGES_MAX);
|
|
436
449
|
if (pages.length) unitInputs[AI_LAYOUT_SITE_PAGES_INPUT] = pages;
|
|
437
450
|
}
|
|
438
451
|
// A site's pages and its layout are designed in the layout language and
|
|
@@ -445,7 +458,8 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
|
|
|
445
458
|
const style = look == null ? void 0 : (_look_proposal = look.proposal) == null ? void 0 : _look_proposal['style'];
|
|
446
459
|
if (style) unitInputs['siteStyle'] = {
|
|
447
460
|
headerAlign: style['headerAlign'],
|
|
448
|
-
rhythm: style['rhythm']
|
|
461
|
+
rhythm: style['rhythm'],
|
|
462
|
+
seed: style['seed']
|
|
449
463
|
};
|
|
450
464
|
unitInputs[AI_LAYOUT_LANGUAGE_INPUT] = true;
|
|
451
465
|
const formPage = aiLayoutFormPageOfPlan(plan);
|
|
@@ -484,52 +498,84 @@ export const AI_SITE_NO_PLAN_COPY = 'This site job has no confirmed plan to buil
|
|
|
484
498
|
brief: brief.join('\n').slice(0, AI_JOB_BRIEF_MAX_CHARS)
|
|
485
499
|
});
|
|
486
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Whether this site start writes its first posts (AGL-3676): its ledger owes
|
|
503
|
+
* the posts part and has not given up on it. The ledger is written before the
|
|
504
|
+
* layout is built, so the header knows the blog before it exists.
|
|
505
|
+
*/ export function aiSiteWritesPosts(job) {
|
|
506
|
+
var _job_items;
|
|
507
|
+
return ((_job_items = job.items) != null ? _job_items : []).some((row)=>row.slot === 'posts' && row.status !== 'skipped' && row.status !== 'failed');
|
|
508
|
+
}
|
|
487
509
|
/**
|
|
488
510
|
* A scaffold is admitted with inputs that read, for a site of the job's own
|
|
489
511
|
* org, and only where this deployment has loaded the step that builds a page:
|
|
490
512
|
* a scaffold whose pages nothing can build is not a scaffold.
|
|
491
|
-
*/ export
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// taste, four to eight on a paid plan.
|
|
499
|
-
const freeTaste = aiSiteFreeTaste(context.org);
|
|
500
|
-
const pages = aiSitePagesRefusal(inputs.pages, freeTaste);
|
|
501
|
-
if (pages) return {
|
|
502
|
-
status: 400,
|
|
503
|
-
error: pages
|
|
504
|
-
};
|
|
505
|
-
if (context.plan) {
|
|
506
|
-
const shape = aiSitePlanRefusal(context.plan, {
|
|
507
|
-
freeTaste
|
|
508
|
-
});
|
|
509
|
-
if (shape) return {
|
|
513
|
+
*/ export function createAiSiteJobAdmission(deps = {}) {
|
|
514
|
+
var _deps_freeCreditsLeft, _deps_now;
|
|
515
|
+
const freeCreditsLeft = (_deps_freeCreditsLeft = deps.freeCreditsLeft) != null ? _deps_freeCreditsLeft : readFreeAiCreditsLeft;
|
|
516
|
+
const now = (_deps_now = deps.now) != null ? _deps_now : ()=>new Date();
|
|
517
|
+
return async (context)=>{
|
|
518
|
+
const inputs = parseAiSiteJobInputs(context.inputs);
|
|
519
|
+
if (typeof inputs === 'string') return {
|
|
510
520
|
status: 400,
|
|
511
|
-
error:
|
|
521
|
+
error: inputs
|
|
512
522
|
};
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
523
|
+
// The workspace's own page band (AGL-3594): one or two pages on the Free
|
|
524
|
+
// taste, four to eight on a paid plan.
|
|
525
|
+
const freeTaste = aiSiteFreeTaste(context.org);
|
|
526
|
+
const pages = aiSitePagesRefusal(inputs.pages, freeTaste);
|
|
527
|
+
if (pages) return {
|
|
516
528
|
status: 400,
|
|
517
|
-
error:
|
|
529
|
+
error: pages
|
|
518
530
|
};
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
531
|
+
// A Free start that what is left of the month's Free credits cannot pay for
|
|
532
|
+
// is refused before it spends (AGL-3660), on the figure the dialog quotes —
|
|
533
|
+
// the dialog asks the same, and this is what a stale dialog meets. Only at
|
|
534
|
+
// creation: a resume is the same job carrying on from where it paused.
|
|
535
|
+
if (freeTaste && !context.plan) {
|
|
536
|
+
const credits = await freeCreditsLeft(context.firestore, {
|
|
537
|
+
orgId: context.orgId,
|
|
538
|
+
org: context.org,
|
|
539
|
+
now: now()
|
|
540
|
+
});
|
|
541
|
+
const shortfall = aiFreeSiteShortfall(credits, inputs.pages);
|
|
542
|
+
if (credits && shortfall) {
|
|
543
|
+
return {
|
|
544
|
+
status: 429,
|
|
545
|
+
error: aiFreeSiteShortfallText(shortfall, credits.resetsOn)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (context.plan) {
|
|
550
|
+
const shape = aiSitePlanRefusal(context.plan, {
|
|
551
|
+
freeTaste
|
|
552
|
+
});
|
|
553
|
+
if (shape) return {
|
|
554
|
+
status: 400,
|
|
555
|
+
error: shape
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (!context.hostId) {
|
|
559
|
+
return {
|
|
560
|
+
status: 400,
|
|
561
|
+
error: 'Open the site the scaffold is for before starting the job'
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
const owner = await resolveOrgIdForHost(context.hostId);
|
|
565
|
+
if (!owner || owner !== context.orgId) return {
|
|
566
|
+
status: 404,
|
|
567
|
+
error: 'Unknown site'
|
|
529
568
|
};
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
569
|
+
if (!aiJobStepRunnerFor('page')) {
|
|
570
|
+
return {
|
|
571
|
+
status: 400,
|
|
572
|
+
error: AI_SITE_NO_PAGE_STEP_COPY
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
export const aiSiteJobAdmission = createAiSiteJobAdmission();
|
|
533
579
|
/** Whether a scaffold's workspace spends the Free taste (AGL-3594); a missing org reads as paid, as the band has always been. */ export function aiSiteFreeTaste(org) {
|
|
534
580
|
return Boolean(org) && resolveEffectivePlan(org) === 'free';
|
|
535
581
|
}
|
|
@@ -709,12 +755,18 @@ export function createAiJobSiteStep(deps = {}) {
|
|
|
709
755
|
]));
|
|
710
756
|
const next = aiBuildNextUnit(ledgerUnits, ledger);
|
|
711
757
|
/** A guided start's last pass puts what it built on the site, once (AGL-3596): only its pages that were built. */ const finish = async (outcome, pages)=>{
|
|
758
|
+
var _postsRow_outputs;
|
|
712
759
|
if (!aiJobPublishesSite(job) || job.sitePublish || !job.hostId || !pages.length) return outcome;
|
|
713
|
-
|
|
760
|
+
// A blog the header links by path before its posts exist (AGL-3660):
|
|
761
|
+
// owed and not delivered, its links come out of what is published.
|
|
762
|
+
const postsRow = rows.get('posts');
|
|
763
|
+
const sitePublish = await publish(context.firestore, _extends({
|
|
714
764
|
job,
|
|
715
765
|
outputs: pages,
|
|
716
766
|
now: context.now
|
|
717
|
-
}
|
|
767
|
+
}, postsRow ? {
|
|
768
|
+
blogUnwritten: !aiBuildItemDelivered(postsRow) || !((_postsRow_outputs = postsRow.outputs) == null ? void 0 : _postsRow_outputs.length)
|
|
769
|
+
} : {})).catch((error)=>{
|
|
718
770
|
// The site is built either way; the pages stay drafts and say so.
|
|
719
771
|
console.error('ai site publish threw', {
|
|
720
772
|
orgId: job.orgId,
|