@aglyn/plugins-ai 1.0.0-beta.239 → 1.0.0-beta.241

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.
Files changed (99) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-job-links.d.ts +17 -0
  3. package/src/lib/components/ai-job-links.js +63 -0
  4. package/src/lib/components/ai-job-links.js.map +1 -1
  5. package/src/lib/components/ai-job-plan.component.js +8 -1
  6. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  7. package/src/lib/components/ai-job-requests.d.ts +5 -0
  8. package/src/lib/components/ai-job-requests.js +4 -2
  9. package/src/lib/components/ai-job-requests.js.map +1 -1
  10. package/src/lib/components/ai-site-build-page.component.js +7 -1
  11. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  12. package/src/lib/components/ai-store-finish-card.component.d.ts +28 -0
  13. package/src/lib/components/ai-store-finish-card.component.js +116 -0
  14. package/src/lib/components/ai-store-finish-card.component.js.map +1 -0
  15. package/src/lib/components/assist-build-card.component.js +12 -1
  16. package/src/lib/components/assist-build-card.component.js.map +1 -1
  17. package/src/lib/components/assist-panel.component.js +14 -2
  18. package/src/lib/components/assist-panel.component.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.d.ts +45 -1
  20. package/src/lib/jobs/ai-job-build-step.js +283 -7
  21. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  22. package/src/lib/jobs/ai-job-drafts.d.ts +9 -1
  23. package/src/lib/jobs/ai-job-drafts.js +4 -1
  24. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  25. package/src/lib/jobs/ai-job-layout-language.d.ts +3 -0
  26. package/src/lib/jobs/ai-job-layout-language.js +11 -4
  27. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  28. package/src/lib/jobs/ai-job-layout-step.js +3 -1
  29. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-page-language.d.ts +2 -1
  31. package/src/lib/jobs/ai-job-page-language.js +26 -9
  32. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  33. package/src/lib/jobs/ai-job-page-sections.d.ts +11 -0
  34. package/src/lib/jobs/ai-job-page-sections.js +53 -5
  35. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  36. package/src/lib/jobs/ai-job-page-step.d.ts +2 -1
  37. package/src/lib/jobs/ai-job-page-step.js +51 -10
  38. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  39. package/src/lib/jobs/ai-job-plan-step.d.ts +12 -0
  40. package/src/lib/jobs/ai-job-plan-step.js +41 -7
  41. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-site-datasets.d.ts +89 -2
  43. package/src/lib/jobs/ai-job-site-datasets.js +239 -15
  44. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  45. package/src/lib/jobs/ai-job-site-step.d.ts +18 -2
  46. package/src/lib/jobs/ai-job-site-step.js +109 -22
  47. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  48. package/src/lib/jobs/ai-job-site-store-pages.d.ts +75 -0
  49. package/src/lib/jobs/ai-job-site-store-pages.js +195 -0
  50. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -0
  51. package/src/lib/jobs/ai-job-store-links-layout.d.ts +71 -0
  52. package/src/lib/jobs/ai-job-store-links-layout.js +95 -0
  53. package/src/lib/jobs/ai-job-store-links-layout.js.map +1 -0
  54. package/src/lib/jobs/ai-layout-stock-photos.d.ts +69 -3
  55. package/src/lib/jobs/ai-layout-stock-photos.js +603 -42
  56. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  57. package/src/lib/jobs/ai-site-publish.d.ts +18 -0
  58. package/src/lib/jobs/ai-site-publish.js +76 -15
  59. package/src/lib/jobs/ai-site-publish.js.map +1 -1
  60. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.d.ts +43 -0
  61. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js +61 -0
  62. package/src/lib/jobs/fixtures/ai-stock-photo-ember-oak-hits.js.map +1 -0
  63. package/src/lib/layout-language/ai-layout-compiler.js +25 -2
  64. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  65. package/src/lib/layout-language/ai-layout-frame.d.ts +10 -0
  66. package/src/lib/layout-language/ai-layout-frame.js +58 -4
  67. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  68. package/src/lib/layout-language/ai-layout-listings.d.ts +6 -0
  69. package/src/lib/layout-language/ai-layout-listings.js +3 -0
  70. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  71. package/src/lib/layout-language/ai-layout-pictures.js +12 -4
  72. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  73. package/src/lib/model/ai-build-job.d.ts +2 -0
  74. package/src/lib/model/ai-build-job.js +23 -3
  75. package/src/lib/model/ai-build-job.js.map +1 -1
  76. package/src/lib/model/ai-build-plan.d.ts +7 -0
  77. package/src/lib/model/ai-build-plan.js.map +1 -1
  78. package/src/lib/model/ai-build-progress.js +3 -1
  79. package/src/lib/model/ai-build-progress.js.map +1 -1
  80. package/src/lib/model/ai-build-store-pages.d.ts +133 -0
  81. package/src/lib/model/ai-build-store-pages.js +209 -0
  82. package/src/lib/model/ai-build-store-pages.js.map +1 -0
  83. package/src/lib/model/ai-layout-store-links.d.ts +64 -0
  84. package/src/lib/model/ai-layout-store-links.js +165 -0
  85. package/src/lib/model/ai-layout-store-links.js.map +1 -0
  86. package/src/lib/model/ai-site-inventory.d.ts +5 -0
  87. package/src/lib/model/ai-site-inventory.js.map +1 -1
  88. package/src/lib/model/ai-site-store-pages.d.ts +192 -0
  89. package/src/lib/model/ai-site-store-pages.js +383 -0
  90. package/src/lib/model/ai-site-store-pages.js.map +1 -0
  91. package/src/lib/runtime/ai-doctrine-validators.d.ts +44 -0
  92. package/src/lib/runtime/ai-doctrine-validators.js +177 -0
  93. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  94. package/src/lib/runtime/ai-doctrine.js +3 -2
  95. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  96. package/src/lib/runtime/site-inventory.js +8 -3
  97. package/src/lib/runtime/site-inventory.js.map +1 -1
  98. package/src/lib/testing/stand-in-dataset-index.js +8 -4
  99. package/src/lib/testing/stand-in-dataset-index.js.map +1 -1
@@ -22,6 +22,11 @@ import { AI_STEP_TIERS } from "../providers/catalog.js";
22
22
  import { aiModelForStep } from "../providers/routing.js";
23
23
  import { AI_DATASET_MAX_TOKENS, AI_DATASET_STEP, aiDatasetDraftContent, generateAiDataset } from "../runtime/ai-dataset-generation.js";
24
24
  import { aiLayoutListingId, aiLayoutRecordsPlacements } from "../layout-language/ai-layout-listings.js";
25
+ import { aiLayoutStarterPhotos } from "../layout-language/ai-layout-pictures.js";
26
+ import { aiSiteWords } from "../model/ai-site-job.js";
27
+ import { aiOriginJobId } from "./ai-job-draft-ids.js";
28
+ import { aiSiteProductPhotoSrc } from "./ai-job-site-content.js";
29
+ import { aiLayoutStockPhotoSource } from "./ai-layout-stock-photos.js";
25
30
  import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
26
31
  import { aiJobStepBudget } from "./ai-job-budget.js";
27
32
  import { aiGenerationSpent, aiUnspentOutcome } from "./ai-job-generation.js";
@@ -49,10 +54,20 @@ import { aiPluginDraftAdmissionRefusal } from "./ai-job-plugin-drafts.js";
49
54
  * has: the member's role and `data.manage`, `release_data_store`,
50
55
  * `dataStore` (Starter and up), `datasetsPerOrg` counted inside the create,
51
56
  * `recordsPerDataset` and the storage band.
57
+ * - PICTURED where the plan lists it the way a gallery is listed (Zach,
58
+ * 2026-10-10: a ceramic artist's "Selected work" came back as text-only
59
+ * cards, where it had been an image-led gallery): a portfolio's pieces, a
60
+ * team, a menu, classes and events, and services or offerings whose
61
+ * section says it shows them in pictures (`aiSiteDatasetPicturesOf`). Such
62
+ * a dataset gets an Image field, and each record a stock photo of its own
63
+ * name in the site's craft, copied into the site's library — never one the
64
+ * job already placed, else a starter photo — searched in code, so it
65
+ * costs no credits (`aiSiteDatasetRecordPhotos`).
52
66
  * - LISTED by the pages built after it: each section naming it repeats over
53
67
  * the dataset (`aiSiteDatasetListings`, a `records` listing the layout
54
- * compiler draws), and a page the plan made its record template binds its
55
- * fields by id and keeps the address field the writer added.
68
+ * compiler draws, its card led by the record's photo where it has one),
69
+ * and a page the plan made its record template binds its fields by id,
70
+ * its photo the record's, and keeps the address field the writer added.
56
71
  *
57
72
  * Where datasets cannot be created — the Free taste, Starter's two used up,
58
73
  * the data store not released — the plan is told so and creates none, so its
@@ -63,6 +78,29 @@ import { aiPluginDraftAdmissionRefusal } from "./ai-job-plugin-drafts.js";
63
78
  /** The resource the data plugin's writer is registered under. */ export const AI_SITE_DATASET_RESOURCE = AI_DATASET_DRAFT_RESOURCE;
64
79
  /** What the site's plugin list calls the plugin, for "Turn on Data for this site". */ const DATA_PLUGIN_LABEL = 'Data';
65
80
  /** The input a dataset unit's own job carries what it needs in. */ export const AI_SITE_DATASET_INPUT = 'siteDataset';
81
+ /** A list of questions, voices or plain facts: read, never pictured. */ const READ_ONLY_LIST = /\b(faqs?|questions?|q ?& ?a|answers?|testimonials?|reviews?|quotes?|voices|hours|opening|steps?|process|policies|policy|terms|stats?|numbers|figures|milestones|timeline|values|benefits|reasons|features|requirements|checklist|ingredients|donations?|needs|partners|sponsors|supporters|roles|positions|jobs|openings)\b/i;
82
+ /** A list of people: its photos are of the business's people. */ const PEOPLE_LIST = /\b(team|staff|people|crew|instructors?|teachers?|tutors?|stylists?|barbers?|therapists?|coaches?|trainers?|chefs?|doctors?|dentists?|hygienists?|practitioners?|artists?|musicians?|performers?|speakers?|founders?|members?)\b/i;
83
+ /** A list a visitor looks at before reading: pictured wherever it is listed. */ const PICTURED_LIST = /\b(portfolio|pieces?|works?|projects?|gallery|galleries|collections?|series|menus?|dishes|plates|drinks|cocktails|wines?|beers?|coffees?|pastries|cakes|products?|items|events?|classes|class|workshops?|courses?|retreats?|tours?|trips?|adventures?|experiences?|rooms?|suites?|cabins?|properties|listings|homes|venues?|spaces?|recipes?|artworks?|paintings?|prints?|photos?|photographs?|designs?|commissions?|builds?|vehicles?|animals?|pets?|plants?|flowers?|bouquets?|arrangements?|case studies)\b/i;
84
+ /** A section that says it shows its items in pictures: a gallery, a grid of photos, tiles. */ const PICTURED_SECTION = /\b(gallery|galleries|grid|images?|photos?|photographs?|pictures?|lightbox|portfolio|showcase|tiles?|visuals?|lookbook|carousel|slideshow|mosaic|thumbnails?)\b/i;
85
+ /**
86
+ * Whether a dataset's records carry a photo, read off the plan (AGL-3616):
87
+ * its name and the names of the sections that list it. Questions, voices and
88
+ * plain facts never; people as people; a list a visitor looks at — pieces,
89
+ * dishes, classes, events, rooms — always; any other list (services,
90
+ * offerings, programs) only where a section that lists it says it shows them
91
+ * in pictures, or names one of those. A dataset no page lists is not
92
+ * pictured. Pure.
93
+ */ export function aiSiteDatasetPicturesOf(dataset, sections) {
94
+ if (!sections.length) return null;
95
+ const own = dataset.name;
96
+ if (READ_ONLY_LIST.test(own)) return null;
97
+ if (PEOPLE_LIST.test(own)) return 'people';
98
+ if (PICTURED_LIST.test(own)) return 'things';
99
+ if (sections.some((section)=>READ_ONLY_LIST.test(section) && !PICTURED_SECTION.test(section))) return null;
100
+ if (sections.some((section)=>PEOPLE_LIST.test(section))) return 'people';
101
+ if (sections.some((section)=>PICTURED_LIST.test(section) || PICTURED_SECTION.test(section))) return 'things';
102
+ return null;
103
+ }
66
104
  /** What a dataset row says when its design could not be written: our failure, refunded. */ export const AI_SITE_DATASET_NOT_WRITTEN_COPY = 'This dataset could not be added this time.';
67
105
  /** The note a written dataset carries on the job's page. */ export function aiSiteDatasetNote(records) {
68
106
  return `${records} ${records === 1 ? 'record' : 'records'} to start, in Data. Edit them there and every page that lists them follows.`;
@@ -84,14 +122,18 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
84
122
  }
85
123
  /** Where the plan lists a dataset, in words its design reads, whether a page is its record template, and the form that writes to it. */ export function aiSiteDatasetInputOf(dataset, screens, create = []) {
86
124
  const shownIn = [];
125
+ const sectionNames = [];
87
126
  for (const screen of screens){
88
127
  for (const section of screen.sections){
89
128
  if (!section.uses.some((ref)=>namesDataset(ref, dataset.name))) continue;
90
129
  shownIn.push(`${screen.title} › ${section.name}${section.items ? ` (${section.items} items)` : ''}`);
130
+ sectionNames.push(section.name);
91
131
  }
92
132
  }
93
133
  const recordPages = screens.some((screen)=>!!screen.record && namesDataset(screen.record.dataset, dataset.name));
94
134
  const form = create.find((entry)=>entry.kind === 'form' && !!entry.writesTo && namesDataset(entry.writesTo, dataset.name));
135
+ // A form's dataset holds its submissions: nobody's photo.
136
+ const pictures = form ? null : aiSiteDatasetPicturesOf(dataset, sectionNames);
95
137
  return _extends({
96
138
  shownIn: shownIn.slice(0, 8),
97
139
  recordPages
@@ -100,6 +142,8 @@ const str = (value)=>typeof value === 'string' ? value.trim() : '';
100
142
  name: form.name,
101
143
  fields: form.fields.slice(0, AI_SITE_FORM_DATASET_FIELDS_MAX)
102
144
  }
145
+ } : {}, pictures ? {
146
+ pictures
103
147
  } : {});
104
148
  }
105
149
  /** Whether a plan screen is the record template of a dataset the plan creates, and which. */ export function aiSiteRecordTemplateOf(screen) {
@@ -120,6 +164,8 @@ function datasetInputOf(job) {
120
164
  name: str(form.name),
121
165
  fields: Array.isArray(form.fields) ? form.fields.map(str).filter(Boolean).slice(0, AI_SITE_FORM_DATASET_FIELDS_MAX) : []
122
166
  }
167
+ } : {}, raw.pictures === 'things' || raw.pictures === 'people' ? {
168
+ pictures: raw.pictures
123
169
  } : {});
124
170
  }
125
171
  /** The most fields a form's dataset is designed with: the data plugin writer's own ceiling is higher. */ export const AI_SITE_FORM_DATASET_FIELDS_MAX = 12;
@@ -171,6 +217,135 @@ const limitReview = (message)=>({
171
217
  message,
172
218
  findings: []
173
219
  });
220
+ /** The wall clock a dataset's photos may take inside its pass: the 20 seconds a page's pictures had (AGL-3660). */ export const AI_SITE_DATASET_PHOTOS_BUDGET_MS = 20000;
221
+ /** The field a pictured dataset's photo is kept in, where the plan gave it none. */ export const AI_SITE_DATASET_IMAGE_FIELD = 'Image';
222
+ /** A planned field that already names a record's photo. */ const IMAGE_FIELD_NAME = /^(?:main |cover |hero |featured? )?(?:image|photo|picture|photograph|thumbnail|headshot|portrait|cover)s?$/i;
223
+ /**
224
+ * A photo for each record of a pictured dataset (AGL-3616), in order: a stock
225
+ * photo of the record's own name, in the site's craft ("ceramic speckled
226
+ * serving bowl"), copied into the site's library, where the deployment has a
227
+ * stock library — never twice in the dataset nor one another part of the job
228
+ * placed, inside {@link AI_SITE_DATASET_PHOTOS_BUDGET_MS} — else a starter
229
+ * photo, as a page's empty picture takes one. A team is searched as the
230
+ * business's people. The photo is kept as the asset's CDN path, the form a
231
+ * product keeps (`aiSiteProductPhotoSrc`), so a replace in the library still
232
+ * reaches it. No AI model is asked, so no credits; never throws.
233
+ */ export async function aiSiteDatasetRecordPhotos(input) {
234
+ const { job } = input;
235
+ if (!input.names.length || !job.hostId) return input.names.map(()=>({
236
+ src: null,
237
+ placed: null
238
+ }));
239
+ const people = input.pictures === 'people';
240
+ const slots = input.names.map((name, index)=>({
241
+ imageId: `record${index}`,
242
+ frameId: null,
243
+ iconId: null,
244
+ alt: name.trim() || input.name,
245
+ // The record card's own shape: a team's portraits stand, pieces sit wide.
246
+ aspect: people ? 4 / 5 : 4 / 3,
247
+ sectionIndex: 0,
248
+ role: people ? 'about' : 'gallery'
249
+ }));
250
+ const seed = `${job.$id}:records`;
251
+ let found = [];
252
+ try {
253
+ var _input_stockPhotos;
254
+ const source = ((_input_stockPhotos = input.stockPhotos) != null ? _input_stockPhotos : aiLayoutStockPhotoSource)(_extends({
255
+ hostId: job.hostId,
256
+ uid: job.createdBy,
257
+ seed,
258
+ business: aiSiteWords(job.inputs).about || job.brief,
259
+ sectionNames: [
260
+ input.name
261
+ ],
262
+ jobId: aiOriginJobId(job),
263
+ // Each record may try its name, its object and the site's world.
264
+ searches: slots.length * 3
265
+ }, input.signal ? {
266
+ signal: input.signal
267
+ } : {}), {
268
+ budgetMs: AI_SITE_DATASET_PHOTOS_BUDGET_MS
269
+ });
270
+ if (source) found = await source(slots);
271
+ } catch (error) {
272
+ console.warn('ai site dataset: the stock photos failed; starter photos fill them', {
273
+ error: String(error)
274
+ });
275
+ }
276
+ const starters = aiLayoutStarterPhotos(slots, seed);
277
+ const hostId = job.hostId;
278
+ return slots.map((_slot, index)=>{
279
+ var _ref;
280
+ var _found_index, _found_index1, _starters_index;
281
+ const stock = aiSiteProductPhotoSrc((_found_index = found[index]) == null ? void 0 : _found_index.src, hostId);
282
+ if (stock) return {
283
+ src: stock,
284
+ placed: (_ref = (_found_index1 = found[index]) == null ? void 0 : _found_index1.src) != null ? _ref : null
285
+ };
286
+ return {
287
+ src: aiSiteProductPhotoSrc((_starters_index = starters[index]) == null ? void 0 : _starters_index.src, hostId),
288
+ placed: null
289
+ };
290
+ });
291
+ }
292
+ /**
293
+ * A designed dataset's content with its records' photos (AGL-3616): the
294
+ * planned field that names a photo ("Image", "Photo") becomes the data
295
+ * plugin's `image` field, else one called "Image" is added after the others,
296
+ * and each record holds its photo there; a record with none holds nothing.
297
+ * Pure.
298
+ */ export function aiSiteDatasetContentWithPhotos(content, photos) {
299
+ const fields = Array.isArray(content['fields']) ? content['fields'] : [];
300
+ const named = fields.find((field)=>IMAGE_FIELD_NAME.test(str(field.name)));
301
+ const fieldName = named ? named.name : AI_SITE_DATASET_IMAGE_FIELD;
302
+ const nextFields = named ? fields.map((field)=>field === named ? _extends({}, field, {
303
+ type: 'image'
304
+ }) : field) : [
305
+ ...fields,
306
+ {
307
+ name: AI_SITE_DATASET_IMAGE_FIELD,
308
+ type: 'image'
309
+ }
310
+ ];
311
+ const records = Array.isArray(content['records']) ? content['records'] : [];
312
+ return _extends({}, content, {
313
+ fields: nextFields,
314
+ records: records.map((record, index)=>{
315
+ const next = _extends({}, record);
316
+ delete next[fieldName];
317
+ const photo = photos[index];
318
+ return photo ? _extends({}, next, {
319
+ [fieldName]: photo
320
+ }) : next;
321
+ })
322
+ });
323
+ }
324
+ /**
325
+ * The fields of a designed dataset that its records leave empty on most of
326
+ * them — every record, or more than half — by the id the data plugin gave
327
+ * each (AGL-3616). Its first field is the record's name and is never one. The
328
+ * live low-tide-hollow start (beta.239) seeded its tour dates with no venue,
329
+ * rightly inventing none, and its cards bound the venue as their eyebrow.
330
+ */ export function aiDatasetSparseFields(dataset, written) {
331
+ if (!dataset.records.length) return [];
332
+ const idOf = (name, column)=>{
333
+ var _ref, _ref1;
334
+ var _written_find, _written_column;
335
+ return (_ref = (_ref1 = (_written_find = written.find((field)=>field.name.trim().toLowerCase() === name.trim().toLowerCase())) == null ? void 0 : _written_find.id) != null ? _ref1 : (_written_column = written[column]) == null ? void 0 : _written_column.id) != null ? _ref : null;
336
+ };
337
+ return dataset.fields.flatMap((field, column)=>{
338
+ if (column === 0) return [];
339
+ const filled = dataset.records.filter((values)=>{
340
+ var _values_column;
341
+ return ((_values_column = values[column]) != null ? _values_column : '').trim();
342
+ }).length;
343
+ const id = idOf(field.name, column);
344
+ return id && filled * 2 <= dataset.records.length ? [
345
+ id
346
+ ] : [];
347
+ });
348
+ }
174
349
  function proposalOf(output) {
175
350
  var _output_proposal;
176
351
  const raw = (_output_proposal = output.proposal) != null ? _output_proposal : {};
@@ -181,7 +356,10 @@ function proposalOf(output) {
181
356
  type: str(field.type) || 'text'
182
357
  })) : [],
183
358
  recordNames: Array.isArray(raw.recordNames) ? raw.recordNames.map(str).filter(Boolean).slice(0, 12) : [],
184
- addressField: str(raw.addressField) || null
359
+ addressField: str(raw.addressField) || null,
360
+ imageField: str(raw.imageField) || null,
361
+ photos: Array.isArray(raw.photos) ? raw.photos.map(str).filter((src)=>src.startsWith('media:')).slice(0, 24) : [],
362
+ sparseFields: Array.isArray(raw.sparseFields) ? raw.sparseFields.map(str).filter(Boolean) : []
185
363
  };
186
364
  }
187
365
  /**
@@ -190,11 +368,12 @@ function proposalOf(output) {
190
368
  * plugin, under the creation's own id. A pass repeated after its write finds
191
369
  * the dataset the writer wrote and reports it again, spending nothing.
192
370
  */ export function createAiSiteDatasetRunner(deps = {}) {
193
- var _deps_generate, _deps_writerFor;
371
+ var _deps_generate, _deps_writerFor, _deps_photos;
194
372
  const generate = (_deps_generate = deps.generate) != null ? _deps_generate : generateAiDataset;
195
373
  const writerFor = (_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter;
374
+ const photosFor = (_deps_photos = deps.photos) != null ? _deps_photos : aiSiteDatasetRecordPhotos;
196
375
  return async (context)=>{
197
- var _ref, _context_org, _facts_fields, _facts_addressField, _facts_records;
376
+ var _ref, _context_org, _facts_fields, _facts_addressField, _facts_fields1, _facts_records;
198
377
  var _job_plan_create, _job_plan;
199
378
  const { job, firestore, signal } = context;
200
379
  const model = (_ref = context.modelFor == null ? void 0 : context.modelFor.call(context, AI_DATASET_STEP)) != null ? _ref : aiModelForStep(AI_DATASET_STEP);
@@ -228,12 +407,13 @@ function proposalOf(output) {
228
407
  id: job.$id
229
408
  });
230
409
  if (written) {
231
- var _facts_fields1, _facts_addressField1;
410
+ var _facts_fields2, _facts_addressField1, _facts_imageField;
232
411
  const facts = written.facts;
233
412
  const again = output(written.id, written.name || creation.name, {
234
- fields: (_facts_fields1 = facts.fields) != null ? _facts_fields1 : [],
413
+ fields: (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [],
235
414
  recordNames: [],
236
- addressField: (_facts_addressField1 = facts.addressField) != null ? _facts_addressField1 : null
415
+ addressField: (_facts_addressField1 = facts.addressField) != null ? _facts_addressField1 : null,
416
+ imageField: (_facts_imageField = facts.imageField) != null ? _facts_imageField : null
237
417
  }, 0);
238
418
  return aiUnspentOutcome(model, {
239
419
  outputs: [
@@ -246,7 +426,7 @@ function proposalOf(output) {
246
426
  // A form's dataset (AGL-3616): its records are the submissions, so it is
247
427
  // designed from the plan with no model, and starts empty.
248
428
  if (input.forForm) {
249
- var _context_org1, _facts_fields2;
429
+ var _context_org1, _facts_fields3;
250
430
  const content = aiSiteFormDatasetContent(creation.name, creation.fields, input.forForm);
251
431
  const checked = keeper.writer.check(content, {
252
432
  hostId: job.hostId
@@ -283,7 +463,7 @@ function proposalOf(output) {
283
463
  return aiUnspentOutcome(model, {
284
464
  outputs: [
285
465
  _extends({}, output(result.id, result.name, {
286
- fields: (_facts_fields2 = facts.fields) != null ? _facts_fields2 : [],
466
+ fields: (_facts_fields3 = facts.fields) != null ? _facts_fields3 : [],
287
467
  recordNames: [],
288
468
  addressField: null
289
469
  }, 0), {
@@ -312,9 +492,27 @@ function proposalOf(output) {
312
492
  if (generation.status === 'needs_input') return _extends({}, spent, {
313
493
  failure: generation.message
314
494
  });
315
- const content = aiDatasetDraftContent(creation.name, generation.value, {
495
+ const designed = aiDatasetDraftContent(creation.name, generation.value, {
316
496
  recordPages: input.recordPages
317
497
  });
498
+ // A list a visitor looks at keeps its records' photos (AGL-3616): searched
499
+ // in code, never asked of the model, so they cost no credits.
500
+ const photos = input.pictures ? await photosFor(_extends({
501
+ job,
502
+ name: creation.name,
503
+ names: generation.value.records.map((values)=>{
504
+ var _values_;
505
+ return (_values_ = values[0]) != null ? _values_ : '';
506
+ }),
507
+ pictures: input.pictures
508
+ }, signal ? {
509
+ signal
510
+ } : {})).catch(()=>[]) : [];
511
+ const content = input.pictures ? aiSiteDatasetContentWithPhotos(designed, generation.value.records.map((_values, index)=>{
512
+ var _ref;
513
+ var _photos_index;
514
+ return (_ref = (_photos_index = photos[index]) == null ? void 0 : _photos_index.src) != null ? _ref : null;
515
+ })) : designed;
318
516
  // The data plugin's own rules, before anything is written: content we
319
517
  // made that its model refuses is our failure, refunded.
320
518
  const checked = keeper.writer.check(content, {
@@ -350,14 +548,23 @@ function proposalOf(output) {
350
548
  });
351
549
  }
352
550
  const facts = result.facts;
353
- const proposal = {
551
+ const placed = photos.flatMap((photo)=>photo.placed ? [
552
+ photo.placed
553
+ ] : []);
554
+ const proposal = _extends({
354
555
  fields: (_facts_fields = facts.fields) != null ? _facts_fields : [],
355
556
  recordNames: generation.value.records.map((values)=>{
356
557
  var _values_;
357
558
  return (_values_ = values[0]) != null ? _values_ : '';
358
559
  }).filter(Boolean).slice(0, 12),
359
560
  addressField: (_facts_addressField = facts.addressField) != null ? _facts_addressField : null
360
- };
561
+ }, facts.imageField ? {
562
+ imageField: facts.imageField
563
+ } : {}, placed.length ? {
564
+ photos: placed
565
+ } : {}, {
566
+ sparseFields: aiDatasetSparseFields(generation.value, (_facts_fields1 = facts.fields) != null ? _facts_fields1 : [])
567
+ });
361
568
  return _extends({}, spent, {
362
569
  outputs: [
363
570
  output(result.id, result.name, proposal, (_facts_records = facts.records) != null ? _facts_records : generation.value.records.length)
@@ -456,6 +663,8 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
456
663
  datasetId: dataset.id
457
664
  }, ((_dataset_fields = dataset.fields) == null ? void 0 : _dataset_fields.length) ? {
458
665
  fields: dataset.fields
666
+ } : {}, dataset.imageField ? {
667
+ imageField: dataset.imageField
459
668
  } : {}, {
460
669
  placements
461
670
  })
@@ -469,6 +678,7 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
469
678
  * nowhere: the pages built without it write the items out.
470
679
  */ export function aiSiteDatasetListings(input) {
471
680
  const made = input.datasets.flatMap((dataset)=>{
681
+ var _proposal_imageField;
472
682
  const id = input.delivered.get(dataset.name.trim().toLowerCase());
473
683
  const output = id ? input.outputs.find((entry)=>entry.resource === 'draft' && entry.draftResource === AI_SITE_DATASET_RESOURCE && entry.id === id) : undefined;
474
684
  if (!id || !output) return [];
@@ -479,13 +689,27 @@ export const runAiSiteDatasetUnit = createAiSiteDatasetRunner();
479
689
  // Placed by the name the plan's sections gave it.
480
690
  name: dataset.name,
481
691
  records: proposal.recordNames,
482
- // The address field is the record page's, never a card's words.
483
- fields: proposal.fields.filter((field)=>field.id !== proposal.addressField)
692
+ // The address field is the record page's, never a card's words; the
693
+ // photo is the card's picture, never its words; a field most records
694
+ // leave empty is no card's either (AGL-3616).
695
+ fields: proposal.fields.filter((field, index)=>{
696
+ var _proposal_sparseFields;
697
+ return field.id !== proposal.addressField && field.id !== proposal.imageField && (index === 0 || !((_proposal_sparseFields = proposal.sparseFields) == null ? void 0 : _proposal_sparseFields.includes(field.id)));
698
+ }),
699
+ imageField: (_proposal_imageField = proposal.imageField) != null ? _proposal_imageField : null
484
700
  }
485
701
  ];
486
702
  });
487
703
  return aiDatasetListingsOf(made, input.screens);
488
704
  }
705
+ /** The library photos the site's datasets' records show (AGL-3616), which a page does not place again. */ export function aiSiteDatasetPlacedPhotos(outputs) {
706
+ return [
707
+ ...new Set(outputs.filter((output)=>output.resource === 'draft' && output.draftResource === AI_SITE_DATASET_RESOURCE).flatMap((output)=>{
708
+ var _proposalOf_photos;
709
+ return (_proposalOf_photos = proposalOf(output).photos) != null ? _proposalOf_photos : [];
710
+ }))
711
+ ];
712
+ }
489
713
  /** What a page is told about the datasets built before it. */ export function aiSiteDatasetBriefLines(outputs) {
490
714
  const datasets = outputs.filter((output)=>output.resource === 'draft' && output.draftResource === AI_SITE_DATASET_RESOURCE);
491
715
  return datasets.flatMap((output)=>{