@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.287
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/assets/site-styles/nonprofit.webp +0 -0
- package/src/lib/assets/site-styles/professional.webp +0 -0
- package/src/lib/assets/site-styles/store.webp +0 -0
- package/src/lib/assets/site-styles/trades.webp +0 -0
- package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/declarations.js +5 -2
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +12 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
- package/src/lib/jobs/ai-build-capabilities.js +27 -2
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-image-reads.d.ts +59 -0
- package/src/lib/jobs/ai-image-reads.js +138 -0
- package/src/lib/jobs/ai-image-reads.js.map +1 -0
- package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
- package/src/lib/jobs/ai-job-build-step.js +83 -12
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
- package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-import-step.js +15 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
- package/src/lib/jobs/ai-job-insight-step.js +23 -5
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +2 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-media-step.js +6 -8
- package/src/lib/jobs/ai-job-media-step.js.map +1 -1
- package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-navigation-step.js +7 -9
- package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +7 -1
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +16 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +9 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
- package/src/lib/jobs/ai-job-plan-step.js +78 -13
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
- package/src/lib/jobs/ai-job-privacy-step.js +14 -15
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.js +5 -3
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-settings-step.js +7 -8
- package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
- package/src/lib/jobs/ai-job-site-content.js +67 -8
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
- package/src/lib/jobs/ai-job-site-datasets.js +298 -30
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +6 -3
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
- package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
- package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
- package/src/lib/jobs/ai-job-tracking-step.js +7 -8
- package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
- package/src/lib/jobs/ai-jobs-beat.js +18 -8
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
- package/src/lib/jobs/ai-jobs-notify.js +136 -3
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
- package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-product-image.d.ts +14 -0
- package/src/lib/jobs/ai-product-image.js +73 -21
- package/src/lib/jobs/ai-product-image.js.map +1 -1
- package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
- package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
- package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
- package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
- package/src/lib/layout-language/ai-layout-compiler.js +194 -18
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
- package/src/lib/layout-language/ai-layout-copy.js +383 -3
- package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
- package/src/lib/layout-language/ai-layout-design.js +31 -11
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
- package/src/lib/layout-language/ai-layout-listings.js +9 -1
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
- package/src/lib/layout-language/ai-layout-pictures.js +131 -3
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
- package/src/lib/layout-language/ai-layout-storefront.js +100 -20
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +7 -0
- package/src/lib/model/ai-build-job.js +36 -7
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +28 -4
- package/src/lib/model/ai-build-plan.js +49 -9
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-build-progress.js +3 -1
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +8 -0
- package/src/lib/model/ai-job-notice.js +33 -0
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
- package/src/lib/model/ai-job-plan-lines.js +6 -1
- package/src/lib/model/ai-job-plan-lines.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +8 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.d.ts +13 -0
- package/src/lib/model/ai-permissions.js +29 -1
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-products.d.ts +18 -0
- package/src/lib/model/ai-products.js +32 -0
- package/src/lib/model/ai-products.js.map +1 -1
- package/src/lib/model/ai-site-inspiration.d.ts +3 -2
- package/src/lib/model/ai-site-inspiration.js +3 -2
- package/src/lib/model/ai-site-inspiration.js.map +1 -1
- package/src/lib/plugin.js +4 -13
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/anthropic.js +9 -2
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/catalog.js +14 -5
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/contract.d.ts +24 -1
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.js +5 -2
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
- package/src/lib/runtime/ai-dataset-generation.js +251 -3
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
- package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
- package/src/lib/runtime/ai-dev-replay.js +4 -2
- package/src/lib/runtime/ai-dev-replay.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
- package/src/lib/runtime/ai-doctrine-validators.js +343 -27
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -2
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-icon-library.d.ts +27 -1
- package/src/lib/runtime/ai-icon-library.js +30 -3
- package/src/lib/runtime/ai-icon-library.js.map +1 -1
- package/src/lib/runtime/ai-products-generation.d.ts +11 -1
- package/src/lib/runtime/ai-products-generation.js +51 -4
- package/src/lib/runtime/ai-products-generation.js.map +1 -1
- package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
- package/src/lib/runtime/assist-answer-cache.js +5 -2
- package/src/lib/runtime/assist-answer-cache.js.map +1 -1
- package/src/lib/server/ai-allotments.js +7 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-experiment-versions.js +5 -5
- package/src/lib/server/ai-experiment-versions.js.map +1 -1
- package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
- package/src/lib/server/ai-host-permissions-roster.js +33 -0
- package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
- package/src/lib/server/ai-host-permissions.js +8 -6
- package/src/lib/server/ai-host-permissions.js.map +1 -1
- package/src/lib/server/ai-image-reads-route.d.ts +17 -0
- package/src/lib/server/ai-image-reads-route.js +106 -0
- package/src/lib/server/ai-image-reads-route.js.map +1 -0
- package/src/lib/server/ai-seo-apply.js +5 -5
- package/src/lib/server/ai-seo-apply.js.map +1 -1
- package/src/lib/server/ai-site-import-route.js +7 -4
- package/src/lib/server/ai-site-import-route.js.map +1 -1
- package/src/lib/server/assist-build.js +1 -0
- package/src/lib/server/assist-build.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +11 -7
- package/src/lib/server/assist-chat.js +185 -48
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-docs-query.d.ts +51 -0
- package/src/lib/server/assist-docs-query.js +57 -0
- package/src/lib/server/assist-docs-query.js.map +1 -0
- package/src/lib/server/assist-jobs-context.d.ts +102 -0
- package/src/lib/server/assist-jobs-context.js +227 -0
- package/src/lib/server/assist-jobs-context.js.map +1 -0
- package/src/lib/server/assist-view-context.d.ts +8 -4
- package/src/lib/server/assist-view-context.js +12 -7
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server.js +12 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
- package/src/lib/tools/ai-dataset-tool.js +24 -7
- package/src/lib/tools/ai-dataset-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
- package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
- package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -26,7 +27,9 @@ import { AI_DATASET_SAID_NAME, detectOffVoiceCopy } from "../runtime/ai-doctrine
|
|
|
26
27
|
* no price the brief does not state, and the storefront copy rules hold every
|
|
27
28
|
* value (no health, financial or legal promise; no award or certification the
|
|
28
29
|
* brief does not give). The planned field names are kept, in order, because
|
|
29
|
-
* the pages planned around the dataset already name them.
|
|
30
|
+
* the pages planned around the dataset already name them. A record the brief
|
|
31
|
+
* gives no entry for is a SAMPLE, marked so, for the owner to replace
|
|
32
|
+
* (AGL-4065); a numbered placeholder is never one.
|
|
30
33
|
*/ export const AI_DATASET_TOOL_NAME = 'submit_dataset';
|
|
31
34
|
/** The field types a dataset's design may use: the data plugin writer's own words for them. */ export const AI_DATASET_FIELD_TYPES = [
|
|
32
35
|
'text',
|
|
@@ -84,12 +87,13 @@ export const AI_DATASET_TOOL = {
|
|
|
84
87
|
},
|
|
85
88
|
records: {
|
|
86
89
|
type: 'array',
|
|
87
|
-
description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records.`,
|
|
90
|
+
description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records: the brief's own, or samples where it gives none for this list.`,
|
|
88
91
|
items: {
|
|
89
92
|
type: 'object',
|
|
90
93
|
additionalProperties: false,
|
|
91
94
|
required: [
|
|
92
|
-
'values'
|
|
95
|
+
'values',
|
|
96
|
+
'sample'
|
|
93
97
|
],
|
|
94
98
|
properties: {
|
|
95
99
|
values: {
|
|
@@ -98,6 +102,10 @@ export const AI_DATASET_TOOL = {
|
|
|
98
102
|
items: {
|
|
99
103
|
type: 'string'
|
|
100
104
|
}
|
|
105
|
+
},
|
|
106
|
+
sample: {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: 'true for a sample the owner replaces, written because the brief gives no entry for this list; false for an entry the brief gives.'
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
}
|
|
@@ -133,11 +141,14 @@ const BOOLEAN = /^(yes|no|true|false)$/i;
|
|
|
133
141
|
* field names the plan gave it; `merchantWords` are the brief and the site's
|
|
134
142
|
* answers, the only source a stated claim or price may come from.
|
|
135
143
|
*/ export function checkAiDataset(answer, context) {
|
|
144
|
+
var _context_recordsMin;
|
|
136
145
|
const findings = {
|
|
137
146
|
violations: [],
|
|
138
147
|
offending: {}
|
|
139
148
|
};
|
|
140
|
-
const limits = AI_DATASET_LIMITS
|
|
149
|
+
const limits = _extends({}, AI_DATASET_LIMITS, {
|
|
150
|
+
recordsMin: (_context_recordsMin = context.recordsMin) != null ? _context_recordsMin : AI_DATASET_LIMITS.recordsMin
|
|
151
|
+
});
|
|
141
152
|
const fields = [];
|
|
142
153
|
const rawFields = Array.isArray(answer['fields']) ? answer['fields'] : [];
|
|
143
154
|
if (!rawFields.length) fail(findings, 'fields', 'missing', 'The dataset needs its fields.', answer['fields']);
|
|
@@ -170,16 +181,20 @@ const BOOLEAN = /^(yes|no|true|false)$/i;
|
|
|
170
181
|
fail(findings, 'fields[0]', 'first-field', 'The first field names each record, so it is text.', rawFields[0]);
|
|
171
182
|
}
|
|
172
183
|
const rawRecords = Array.isArray(answer['records']) ? answer['records'] : [];
|
|
173
|
-
|
|
184
|
+
// No records is an answer (AGL-4065): a list the brief gives no entries for
|
|
185
|
+
// starts empty, for the owner to fill in Data, never with numbered stand-ins.
|
|
186
|
+
if (rawRecords.length > 0 && rawRecords.length < limits.recordsMin || rawRecords.length > limits.recordsMax) {
|
|
174
187
|
fail(findings, RECORDS_AT, 'count', `Write ${limits.recordsMin} to ${limits.recordsMax} records; this has ${rawRecords.length}.`, answer['records']);
|
|
175
188
|
}
|
|
176
189
|
const records = [];
|
|
190
|
+
/** Which records are samples (AGL-4065), by position. */ const sampled = [];
|
|
177
191
|
const samples = [];
|
|
178
192
|
const names = new Set();
|
|
179
193
|
rawRecords.forEach((raw, index)=>{
|
|
180
194
|
var _values_, _ref;
|
|
181
195
|
var _fields_;
|
|
182
196
|
const at = `records[${index}]`;
|
|
197
|
+
sampled.push((raw == null ? void 0 : raw['sample']) === true);
|
|
183
198
|
const values = Array.isArray(raw == null ? void 0 : raw['values']) ? raw['values'].map((value)=>line(value)) : [];
|
|
184
199
|
if (values.length !== fields.length) {
|
|
185
200
|
fail(findings, at, 'values', `Record ${index + 1} has ${values.length} values for ${fields.length} fields; give one per field, "" where there is none.`, raw);
|
|
@@ -219,10 +234,12 @@ const BOOLEAN = /^(yes|no|true|false)$/i;
|
|
|
219
234
|
});
|
|
220
235
|
findings.violations.push(...storefrontClaimViolations(findStorefrontClaims(samples, context.merchantWords), findInventedPrices(samples, context.merchantWords)), ...detectOffVoiceCopy(samples, null));
|
|
221
236
|
return {
|
|
222
|
-
value: findings.violations.length ? null : {
|
|
237
|
+
value: findings.violations.length ? null : _extends({
|
|
223
238
|
fields,
|
|
224
239
|
records
|
|
225
|
-
},
|
|
240
|
+
}, sampled.some(Boolean) ? {
|
|
241
|
+
samples: sampled
|
|
242
|
+
} : {}),
|
|
226
243
|
violations: findings.violations,
|
|
227
244
|
offending: findings.offending
|
|
228
245
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-dataset-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n findInventedPrices,\n findStorefrontClaims,\n storefrontClaimViolations,\n type AiStorefrontCopySample,\n} from '../model/ai-storefront-claims'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport { AI_DATASET_SAID_NAME, detectOffVoiceCopy, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tool a site's datasets are designed through (AGL-3616), and the\n * check that holds each one to what the site publishes as its own words.\n *\n * A dataset is a menu, a team, a list of services or portfolio pieces: like\n * things, each a record a repeat shows on a page the day the site goes live.\n * So nothing in a record may mark a gap for later (no square brackets), link\n * anywhere, or say what nobody said: no review, testimonial, rating or quote,\n * no price the brief does not state, and the storefront copy rules hold every\n * value (no health, financial or legal promise; no award or certification the\n * brief does not give). The planned field names are kept, in order, because\n * the pages planned around the dataset already name them.\n */\n\nexport const AI_DATASET_TOOL_NAME = 'submit_dataset'\n\n/** The field types a dataset's design may use: the data plugin writer's own words for them. */\nexport const AI_DATASET_FIELD_TYPES = ['text', 'number', 'integer', 'boolean', 'list'] as const\nexport type AiDatasetFieldType = (typeof AI_DATASET_FIELD_TYPES)[number]\n\n/** A dataset's bounds, as the tool states them and its check holds them. */\nexport const AI_DATASET_LIMITS = {\n /** The most fields, the planned ones included. */\n fieldsMax: 12,\n /** The most fields it may add to the planned ones. */\n extraFieldsMax: 3,\n fieldNameMax: 40,\n recordsMin: 3,\n recordsMax: 12,\n /** The longest value; a description is a few sentences. */\n valueMax: 400,\n} as const\n\n/** What a list value's items are joined by inside one value. */\nexport const AI_DATASET_LIST_SEPARATOR = '; '\n\n/** A designed dataset as its check reads it. */\nexport interface AiDataset {\n fields: Array<{ name: string; type: AiDatasetFieldType }>\n /** Each record's values in field order; an empty string is no value. */\n records: string[][]\n}\n\nconst string = (description: string) => ({ type: 'string', description })\n\nexport const AI_DATASET_TOOL: AiTool = {\n name: AI_DATASET_TOOL_NAME,\n description: 'Design one dataset of this site: its typed fields and its first records. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['fields', 'records'],\n properties: {\n fields: {\n type: 'array',\n description: `The planned fields first, in order and named as planned, then at most ${AI_DATASET_LIMITS.extraFieldsMax} more; at most ${AI_DATASET_LIMITS.fieldsMax} in all.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type'],\n properties: {\n name: string(`Plain words, at most ${AI_DATASET_LIMITS.fieldNameMax} characters.`),\n type: {\n type: 'string',\n enum: [...AI_DATASET_FIELD_TYPES],\n description: 'text for words (a date or time too, written as a visitor reads it); number or integer for a figure; boolean for yes or no; list for a few short tags.',\n },\n },\n },\n },\n records: {\n type: 'array',\n description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['values'],\n properties: {\n values: {\n type: 'array',\n description: `One value per field, in field order: \"\" where the brief gives nothing for it; a list's items joined by \"${AI_DATASET_LIST_SEPARATOR}\"; a boolean \"yes\" or \"no\".`,\n items: { type: 'string' },\n },\n },\n },\n },\n },\n },\n}\n\ninterface Findings {\n violations: AiDoctrineViolation[]\n offending: Record<string, unknown>\n}\n\nfunction fail(findings: Findings, at: string, code: string, message: string, value: unknown): void {\n findings.violations.push({ rule: null, code, message, paths: [at] })\n if (!(at in findings.offending)) findings.offending[at] = value\n}\n\nconst line = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\nconst HTML = /<\\/?[a-z][^>]*>/i\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n/** A rating or review score nobody gave. */\nconst RATING = /\\b\\d(?:\\.\\d)?\\s*(?:\\/\\s*(?:5|10)\\b|out of (?:5|five|10|ten)\\b|stars?\\b)|\\b(?:five|5|four|4)[- ]star\\b|\\brated\\b/i\n/** A field that would hold what customers said, which no job writes for them. */\nconst SAID_FIELD = AI_DATASET_SAID_NAME\n/** A field holding a price, whose values must be the brief's own. */\nconst PRICE_FIELD = /\\b(price|prices|cost|costs|fee|fees|rate|rates)\\b/i\nconst NUMBER = /^-?\\d+(?:\\.\\d+)?$/\nconst BOOLEAN = /^(yes|no|true|false)$/i\n\n/** Where the records sit in the answer, as a violation names it. */\nconst RECORDS_AT = 'records'\n\n/** A planned field name as names compare: case and spacing aside. */\nconst key = (name: string) => line(name).toLowerCase()\n\n/**\n * A dataset's answer held to its plan and the copy rules. `planned` are the\n * field names the plan gave it; `merchantWords` are the brief and the site's\n * answers, the only source a stated claim or price may come from.\n */\nexport function checkAiDataset(\n answer: Record<string, unknown>,\n context: { planned: readonly string[]; merchantWords: string },\n): AiGenerationCheckResult<AiDataset> {\n const findings: Findings = { violations: [], offending: {} }\n const limits = AI_DATASET_LIMITS\n const fields: AiDataset['fields'] = []\n const rawFields = Array.isArray(answer['fields']) ? (answer['fields'] as unknown[]) : []\n if (!rawFields.length) fail(findings, 'fields', 'missing', 'The dataset needs its fields.', answer['fields'])\n rawFields.forEach((raw, index) => {\n const entry = (raw ?? {}) as Record<string, unknown>\n const name = line(entry['name'])\n const type = entry['type'] as AiDatasetFieldType\n const at = `fields[${index}]`\n if (!name) return fail(findings, at, 'missing', 'Each field needs a name.', raw)\n if (name.length > limits.fieldNameMax) fail(findings, at, 'too-long', `The field \"${name}\" has a name longer than ${limits.fieldNameMax} characters.`, raw)\n if (!AI_DATASET_FIELD_TYPES.includes(type)) fail(findings, at, 'type', `The field \"${name}\" is one of ${AI_DATASET_FIELD_TYPES.join(', ')}.`, raw)\n if (fields.some((field) => key(field.name) === key(name))) fail(findings, at, 'duplicate', `Two fields are both called \"${name}\".`, raw)\n if (SAID_FIELD.test(name)) {\n fail(findings, at, 'said-field', `A dataset holds no \"${name}\": what customers say is theirs to write, never the site's.`, raw)\n }\n fields.push({ name, type })\n })\n if (fields.length > limits.fieldsMax) fail(findings, 'fields', 'too-many', `A dataset has at most ${limits.fieldsMax} fields.`, answer['fields'])\n const planned = context.planned.map(line).filter(Boolean)\n const missing = planned.filter((name) => !fields.some((field) => key(field.name) === key(name)))\n if (missing.length) {\n fail(findings, 'fields', 'planned-field', `Keep the planned fields, named as planned: ${missing.map((name) => `\"${name}\"`).join(', ')}.`, answer['fields'])\n } else if (fields.length - planned.length > limits.extraFieldsMax) {\n fail(findings, 'fields', 'too-many', `Add at most ${limits.extraFieldsMax} fields to the planned ones.`, answer['fields'])\n }\n if (fields[0] && fields[0].type !== 'text') {\n fail(findings, 'fields[0]', 'first-field', 'The first field names each record, so it is text.', rawFields[0])\n }\n\n const rawRecords = Array.isArray(answer['records']) ? (answer['records'] as unknown[]) : []\n if (rawRecords.length < limits.recordsMin || rawRecords.length > limits.recordsMax) {\n fail(findings, RECORDS_AT, 'count', `Write ${limits.recordsMin} to ${limits.recordsMax} records; this has ${rawRecords.length}.`, answer['records'])\n }\n const records: string[][] = []\n const samples: AiStorefrontCopySample[] = []\n const names = new Set<string>()\n rawRecords.forEach((raw, index) => {\n const at = `records[${index}]`\n const values = Array.isArray((raw as Record<string, unknown> | null)?.['values'])\n ? ((raw as Record<string, unknown>)['values'] as unknown[]).map((value) => line(value))\n : []\n if (values.length !== fields.length) {\n fail(findings, at, 'values', `Record ${index + 1} has ${values.length} values for ${fields.length} fields; give one per field, \"\" where there is none.`, raw)\n }\n const first = values[0] ?? ''\n if (!first) fail(findings, at, 'unnamed', `Record ${index + 1} has no ${fields[0]?.name ?? 'name'}.`, raw)\n else if (names.has(first.toLowerCase())) fail(findings, at, 'duplicate', `Two records are both \"${first}\".`, raw)\n names.add(first.toLowerCase())\n values.forEach((value, column) => {\n const field = fields[column]\n if (!value || !field) return\n const where = `${at}.values[${column}]`\n if (value.length > limits.valueMax) fail(findings, where, 'too-long', `\"${field.name}\" of record ${index + 1} is longer than ${limits.valueMax} characters.`, value)\n if (HTML.test(value) || EMOJI.test(value) || URL.test(value)) {\n fail(findings, where, 'markup', `\"${field.name}\" of record ${index + 1} is plain words, with no link, markup or emoji.`, value)\n }\n if (BRACKET.test(value)) {\n fail(findings, where, 'bracket', `\"${field.name}\" of record ${index + 1} is published as written, so it marks no missing fact: leave the value \"\" instead.`, value)\n }\n if ((field.type === 'number' || field.type === 'integer') && !NUMBER.test(value.replace(/,/g, ''))) {\n fail(findings, where, 'number', `\"${field.name}\" of record ${index + 1} is a number, or \"\".`, value)\n }\n if (field.type === 'boolean' && !BOOLEAN.test(value)) fail(findings, where, 'boolean', `\"${field.name}\" of record ${index + 1} is \"yes\", \"no\" or \"\".`, value)\n if (PRICE_FIELD.test(field.name) && !context.merchantWords.includes(value.replace(/[^\\d.]/g, '') || '\\u0000')) {\n fail(findings, where, 'price-invented', `\"${field.name}\" of record ${index + 1} states a price the brief does not give; leave it \"\".`, value)\n }\n const rating = RATING.exec(value)\n if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {\n fail(findings, where, 'rating-invented', `Record ${index + 1} gives a rating or review nobody gave. Remove: \"${rating[0]}\".`, value)\n }\n if (field.type === 'text') samples.push({ at: where, text: value })\n })\n records.push(values)\n })\n findings.violations.push(\n ...storefrontClaimViolations(\n findStorefrontClaims(samples, context.merchantWords),\n findInventedPrices(samples, context.merchantWords),\n ),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { fields, records },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["findInventedPrices","findStorefrontClaims","storefrontClaimViolations","AI_DATASET_SAID_NAME","detectOffVoiceCopy","AI_DATASET_TOOL_NAME","AI_DATASET_FIELD_TYPES","AI_DATASET_LIMITS","fieldsMax","extraFieldsMax","fieldNameMax","recordsMin","recordsMax","valueMax","AI_DATASET_LIST_SEPARATOR","string","description","type","AI_DATASET_TOOL","name","strict","inputSchema","additionalProperties","required","properties","fields","items","enum","records","values","fail","findings","at","code","message","value","violations","push","rule","paths","offending","line","replace","trim","HTML","EMOJI","BRACKET","URL","RATING","SAID_FIELD","PRICE_FIELD","NUMBER","BOOLEAN","RECORDS_AT","key","toLowerCase","checkAiDataset","answer","context","limits","rawFields","Array","isArray","length","forEach","raw","index","entry","includes","join","some","field","test","planned","map","filter","Boolean","missing","rawRecords","samples","names","Set","first","has","add","column","where","merchantWords","rating","exec","text"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-dataset-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n findInventedPrices,\n findStorefrontClaims,\n storefrontClaimViolations,\n type AiStorefrontCopySample,\n} from '../model/ai-storefront-claims'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGenerationCheckResult } from '../runtime/ai-doctrine'\nimport { AI_DATASET_SAID_NAME, detectOffVoiceCopy, type AiDoctrineViolation } from '../runtime/ai-doctrine-validators'\n\n/**\n * The strict tool a site's datasets are designed through (AGL-3616), and the\n * check that holds each one to what the site publishes as its own words.\n *\n * A dataset is a menu, a team, a list of services or portfolio pieces: like\n * things, each a record a repeat shows on a page the day the site goes live.\n * So nothing in a record may mark a gap for later (no square brackets), link\n * anywhere, or say what nobody said: no review, testimonial, rating or quote,\n * no price the brief does not state, and the storefront copy rules hold every\n * value (no health, financial or legal promise; no award or certification the\n * brief does not give). The planned field names are kept, in order, because\n * the pages planned around the dataset already name them. A record the brief\n * gives no entry for is a SAMPLE, marked so, for the owner to replace\n * (AGL-4065); a numbered placeholder is never one.\n */\n\nexport const AI_DATASET_TOOL_NAME = 'submit_dataset'\n\n/** The field types a dataset's design may use: the data plugin writer's own words for them. */\nexport const AI_DATASET_FIELD_TYPES = ['text', 'number', 'integer', 'boolean', 'list'] as const\nexport type AiDatasetFieldType = (typeof AI_DATASET_FIELD_TYPES)[number]\n\n/** A dataset's bounds, as the tool states them and its check holds them. */\nexport const AI_DATASET_LIMITS = {\n /** The most fields, the planned ones included. */\n fieldsMax: 12,\n /** The most fields it may add to the planned ones. */\n extraFieldsMax: 3,\n fieldNameMax: 40,\n recordsMin: 3,\n recordsMax: 12,\n /** The longest value; a description is a few sentences. */\n valueMax: 400,\n} as const\n\n/** What a list value's items are joined by inside one value. */\nexport const AI_DATASET_LIST_SEPARATOR = '; '\n\n/** A designed dataset as its check reads it. */\nexport interface AiDataset {\n fields: Array<{ name: string; type: AiDatasetFieldType }>\n /** Each record's values in field order; an empty string is no value. */\n records: string[][]\n /**\n * Which records are SAMPLES the owner replaces, by position (AGL-4065):\n * the brief gave no entries for the list, so the record is a typical one\n * of its kind. Absent where none is.\n */\n samples?: boolean[]\n}\n\nconst string = (description: string) => ({ type: 'string', description })\n\nexport const AI_DATASET_TOOL: AiTool = {\n name: AI_DATASET_TOOL_NAME,\n description: 'Design one dataset of this site: its typed fields and its first records. Every field is required.',\n strict: true,\n inputSchema: {\n type: 'object',\n additionalProperties: false,\n required: ['fields', 'records'],\n properties: {\n fields: {\n type: 'array',\n description: `The planned fields first, in order and named as planned, then at most ${AI_DATASET_LIMITS.extraFieldsMax} more; at most ${AI_DATASET_LIMITS.fieldsMax} in all.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['name', 'type'],\n properties: {\n name: string(`Plain words, at most ${AI_DATASET_LIMITS.fieldNameMax} characters.`),\n type: {\n type: 'string',\n enum: [...AI_DATASET_FIELD_TYPES],\n description: 'text for words (a date or time too, written as a visitor reads it); number or integer for a figure; boolean for yes or no; list for a few short tags.',\n },\n },\n },\n },\n records: {\n type: 'array',\n description: `${AI_DATASET_LIMITS.recordsMin} to ${AI_DATASET_LIMITS.recordsMax} records: the brief's own, or samples where it gives none for this list.`,\n items: {\n type: 'object',\n additionalProperties: false,\n required: ['values', 'sample'],\n properties: {\n values: {\n type: 'array',\n description: `One value per field, in field order: \"\" where the brief gives nothing for it; a list's items joined by \"${AI_DATASET_LIST_SEPARATOR}\"; a boolean \"yes\" or \"no\".`,\n items: { type: 'string' },\n },\n sample: {\n type: 'boolean',\n description: 'true for a sample the owner replaces, written because the brief gives no entry for this list; false for an entry the brief gives.',\n },\n },\n },\n },\n },\n },\n}\n\ninterface Findings {\n violations: AiDoctrineViolation[]\n offending: Record<string, unknown>\n}\n\nfunction fail(findings: Findings, at: string, code: string, message: string, value: unknown): void {\n findings.violations.push({ rule: null, code, message, paths: [at] })\n if (!(at in findings.offending)) findings.offending[at] = value\n}\n\nconst line = (value: unknown): string => (typeof value === 'string' ? value.replace(/\\s+/g, ' ').trim() : '')\n\nconst HTML = /<\\/?[a-z][^>]*>/i\nconst EMOJI = /\\p{Extended_Pictographic}/u\nconst BRACKET = /[[\\]]/\nconst URL = /\\bhttps?:\\/\\/|\\bwww\\./i\n/** A rating or review score nobody gave. */\nconst RATING = /\\b\\d(?:\\.\\d)?\\s*(?:\\/\\s*(?:5|10)\\b|out of (?:5|five|10|ten)\\b|stars?\\b)|\\b(?:five|5|four|4)[- ]star\\b|\\brated\\b/i\n/** A field that would hold what customers said, which no job writes for them. */\nconst SAID_FIELD = AI_DATASET_SAID_NAME\n/** A field holding a price, whose values must be the brief's own. */\nconst PRICE_FIELD = /\\b(price|prices|cost|costs|fee|fees|rate|rates)\\b/i\nconst NUMBER = /^-?\\d+(?:\\.\\d+)?$/\nconst BOOLEAN = /^(yes|no|true|false)$/i\n\n/** Where the records sit in the answer, as a violation names it. */\nconst RECORDS_AT = 'records'\n\n/** A planned field name as names compare: case and spacing aside. */\nconst key = (name: string) => line(name).toLowerCase()\n\n/**\n * A dataset's answer held to its plan and the copy rules. `planned` are the\n * field names the plan gave it; `merchantWords` are the brief and the site's\n * answers, the only source a stated claim or price may come from.\n */\nexport function checkAiDataset(\n answer: Record<string, unknown>,\n context: {\n planned: readonly string[]\n merchantWords: string\n /** The fewest records a non-empty answer may hold; the list's own minimum by default. */\n recordsMin?: number\n },\n): AiGenerationCheckResult<AiDataset> {\n const findings: Findings = { violations: [], offending: {} }\n const limits = { ...AI_DATASET_LIMITS, recordsMin: context.recordsMin ?? AI_DATASET_LIMITS.recordsMin }\n const fields: AiDataset['fields'] = []\n const rawFields = Array.isArray(answer['fields']) ? (answer['fields'] as unknown[]) : []\n if (!rawFields.length) fail(findings, 'fields', 'missing', 'The dataset needs its fields.', answer['fields'])\n rawFields.forEach((raw, index) => {\n const entry = (raw ?? {}) as Record<string, unknown>\n const name = line(entry['name'])\n const type = entry['type'] as AiDatasetFieldType\n const at = `fields[${index}]`\n if (!name) return fail(findings, at, 'missing', 'Each field needs a name.', raw)\n if (name.length > limits.fieldNameMax) fail(findings, at, 'too-long', `The field \"${name}\" has a name longer than ${limits.fieldNameMax} characters.`, raw)\n if (!AI_DATASET_FIELD_TYPES.includes(type)) fail(findings, at, 'type', `The field \"${name}\" is one of ${AI_DATASET_FIELD_TYPES.join(', ')}.`, raw)\n if (fields.some((field) => key(field.name) === key(name))) fail(findings, at, 'duplicate', `Two fields are both called \"${name}\".`, raw)\n if (SAID_FIELD.test(name)) {\n fail(findings, at, 'said-field', `A dataset holds no \"${name}\": what customers say is theirs to write, never the site's.`, raw)\n }\n fields.push({ name, type })\n })\n if (fields.length > limits.fieldsMax) fail(findings, 'fields', 'too-many', `A dataset has at most ${limits.fieldsMax} fields.`, answer['fields'])\n const planned = context.planned.map(line).filter(Boolean)\n const missing = planned.filter((name) => !fields.some((field) => key(field.name) === key(name)))\n if (missing.length) {\n fail(findings, 'fields', 'planned-field', `Keep the planned fields, named as planned: ${missing.map((name) => `\"${name}\"`).join(', ')}.`, answer['fields'])\n } else if (fields.length - planned.length > limits.extraFieldsMax) {\n fail(findings, 'fields', 'too-many', `Add at most ${limits.extraFieldsMax} fields to the planned ones.`, answer['fields'])\n }\n if (fields[0] && fields[0].type !== 'text') {\n fail(findings, 'fields[0]', 'first-field', 'The first field names each record, so it is text.', rawFields[0])\n }\n\n const rawRecords = Array.isArray(answer['records']) ? (answer['records'] as unknown[]) : []\n // No records is an answer (AGL-4065): a list the brief gives no entries for\n // starts empty, for the owner to fill in Data, never with numbered stand-ins.\n if ((rawRecords.length > 0 && rawRecords.length < limits.recordsMin) || rawRecords.length > limits.recordsMax) {\n fail(findings, RECORDS_AT, 'count', `Write ${limits.recordsMin} to ${limits.recordsMax} records; this has ${rawRecords.length}.`, answer['records'])\n }\n const records: string[][] = []\n /** Which records are samples (AGL-4065), by position. */\n const sampled: boolean[] = []\n const samples: AiStorefrontCopySample[] = []\n const names = new Set<string>()\n rawRecords.forEach((raw, index) => {\n const at = `records[${index}]`\n sampled.push((raw as Record<string, unknown> | null)?.['sample'] === true)\n const values = Array.isArray((raw as Record<string, unknown> | null)?.['values'])\n ? ((raw as Record<string, unknown>)['values'] as unknown[]).map((value) => line(value))\n : []\n if (values.length !== fields.length) {\n fail(findings, at, 'values', `Record ${index + 1} has ${values.length} values for ${fields.length} fields; give one per field, \"\" where there is none.`, raw)\n }\n const first = values[0] ?? ''\n if (!first) fail(findings, at, 'unnamed', `Record ${index + 1} has no ${fields[0]?.name ?? 'name'}.`, raw)\n else if (names.has(first.toLowerCase())) fail(findings, at, 'duplicate', `Two records are both \"${first}\".`, raw)\n names.add(first.toLowerCase())\n values.forEach((value, column) => {\n const field = fields[column]\n if (!value || !field) return\n const where = `${at}.values[${column}]`\n if (value.length > limits.valueMax) fail(findings, where, 'too-long', `\"${field.name}\" of record ${index + 1} is longer than ${limits.valueMax} characters.`, value)\n if (HTML.test(value) || EMOJI.test(value) || URL.test(value)) {\n fail(findings, where, 'markup', `\"${field.name}\" of record ${index + 1} is plain words, with no link, markup or emoji.`, value)\n }\n if (BRACKET.test(value)) {\n fail(findings, where, 'bracket', `\"${field.name}\" of record ${index + 1} is published as written, so it marks no missing fact: leave the value \"\" instead.`, value)\n }\n if ((field.type === 'number' || field.type === 'integer') && !NUMBER.test(value.replace(/,/g, ''))) {\n fail(findings, where, 'number', `\"${field.name}\" of record ${index + 1} is a number, or \"\".`, value)\n }\n if (field.type === 'boolean' && !BOOLEAN.test(value)) fail(findings, where, 'boolean', `\"${field.name}\" of record ${index + 1} is \"yes\", \"no\" or \"\".`, value)\n if (PRICE_FIELD.test(field.name) && !context.merchantWords.includes(value.replace(/[^\\d.]/g, '') || '\\u0000')) {\n fail(findings, where, 'price-invented', `\"${field.name}\" of record ${index + 1} states a price the brief does not give; leave it \"\".`, value)\n }\n const rating = RATING.exec(value)\n if (rating && !context.merchantWords.toLowerCase().includes(rating[0].toLowerCase())) {\n fail(findings, where, 'rating-invented', `Record ${index + 1} gives a rating or review nobody gave. Remove: \"${rating[0]}\".`, value)\n }\n if (field.type === 'text') samples.push({ at: where, text: value })\n })\n records.push(values)\n })\n findings.violations.push(\n ...storefrontClaimViolations(\n findStorefrontClaims(samples, context.merchantWords),\n findInventedPrices(samples, context.merchantWords),\n ),\n ...detectOffVoiceCopy(samples, null),\n )\n return {\n value: findings.violations.length ? null : { fields, records, ...(sampled.some(Boolean) ? { samples: sampled } : {}) },\n violations: findings.violations,\n offending: findings.offending,\n }\n}\n"],"names":["findInventedPrices","findStorefrontClaims","storefrontClaimViolations","AI_DATASET_SAID_NAME","detectOffVoiceCopy","AI_DATASET_TOOL_NAME","AI_DATASET_FIELD_TYPES","AI_DATASET_LIMITS","fieldsMax","extraFieldsMax","fieldNameMax","recordsMin","recordsMax","valueMax","AI_DATASET_LIST_SEPARATOR","string","description","type","AI_DATASET_TOOL","name","strict","inputSchema","additionalProperties","required","properties","fields","items","enum","records","values","sample","fail","findings","at","code","message","value","violations","push","rule","paths","offending","line","replace","trim","HTML","EMOJI","BRACKET","URL","RATING","SAID_FIELD","PRICE_FIELD","NUMBER","BOOLEAN","RECORDS_AT","key","toLowerCase","checkAiDataset","answer","context","limits","rawFields","Array","isArray","length","forEach","raw","index","entry","includes","join","some","field","test","planned","map","filter","Boolean","missing","rawRecords","sampled","samples","names","Set","first","has","add","column","where","merchantWords","rating","exec","text"],"mappings":"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"}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
-
import {
|
|
16
|
+
*/ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
17
|
+
import { DUPLICABLE_HOST_RESOURCE_KINDS, DUPLICATE_NAME_MAX, isDuplicableHostResourceKind } from "@aglyn/aglyn/app-utils/duplicate-resource";
|
|
18
18
|
import { orgPermissionLabel } from "@aglyn/aglyn/app-utils/org-permissions";
|
|
19
19
|
import { duplicateResource } from "@aglyn/tenant-data-admin/server/duplicate-resource";
|
|
20
20
|
import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
@@ -91,7 +91,7 @@ import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-adm
|
|
|
91
91
|
* outcomes, never throws: a model's call that names a missing source or a
|
|
92
92
|
* full band is an answer to relay, not a fault.
|
|
93
93
|
*/ export async function runAiDuplicateResourceTool(context, input) {
|
|
94
|
-
var _context_hostId
|
|
94
|
+
var _context_hostId;
|
|
95
95
|
var _gate_reservation;
|
|
96
96
|
const { gate } = context;
|
|
97
97
|
const hostId = String((_context_hostId = context.hostId) != null ? _context_hostId : '').trim();
|
|
@@ -133,12 +133,11 @@ import { getOrgForUser, memberHasPermissionOnHost } from "@aglyn/tenant-data-adm
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
|
|
137
|
-
if (!hostRoleCanWrite(memberRole)) {
|
|
136
|
+
if (!holdsSiteKey(host, gate.uid, 'site.edit')) {
|
|
138
137
|
return {
|
|
139
138
|
ok: false,
|
|
140
139
|
status: 403,
|
|
141
|
-
error: '
|
|
140
|
+
error: siteKeyRefusal('site.edit')
|
|
142
141
|
};
|
|
143
142
|
}
|
|
144
143
|
const result = await duplicateResource(parsed.kind, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-duplicate-resource-tool.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n DUPLICABLE_HOST_RESOURCE_KINDS,\n DUPLICATE_NAME_MAX,\n isDuplicableHostResourceKind,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport {\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGateContext } from '../runtime/ai-gate'\n\n/**\n * The `duplicate_resource` tool (AGL-2936, AGL-2984): a model that would\n * rebuild something the site already has copies it instead, through core's\n * `duplicateResource` — the same copy the row menus and the console door\n * make, with the same uniqueness rules, the same band arithmetic and the same\n * activity rows.\n *\n * It spends no tokens of its own, so it calls no provider: it runs inside a\n * door or a job step that has already climbed `aiGateLadder`, and takes that\n * ladder's context as its proof — the caller is signed in and verified, the\n * release flag, entitlement and lockdown rungs admitted the request, and the\n * workspace's band and caps admitted a reservation. On top of that it asks\n * what a copy needs whichever door called it: `ai.generate`, because a copy\n * is a generation's output, and a host role that may write the site, because\n * the copy lands there.\n *\n * DRAFTS ONLY, by construction of the copy: a screen copy has no route, a\n * workflow copy no trigger, and nothing places a component, layout or form\n * copy until a person does.\n */\n\nexport const AI_DUPLICATE_RESOURCE_TOOL_NAME = 'duplicate_resource'\n\n/** The structured-output tool a generation offers the model. */\nexport const AI_DUPLICATE_RESOURCE_TOOL: AiTool = {\n name: AI_DUPLICATE_RESOURCE_TOOL_NAME,\n description:\n 'Copy a resource this site already has — a page (kind \"screen\"), an email design, a component, a layout, ' +\n 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' +\n 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: {\n type: 'string',\n enum: [...DUPLICABLE_HOST_RESOURCE_KINDS],\n description: 'The kind of resource to copy.',\n },\n sourceId: {\n type: 'string',\n description: 'The id of the resource to copy, as the site inventory lists it.',\n },\n name: {\n type: ['string', 'null'],\n description: `The copy's name, at most ${DUPLICATE_NAME_MAX} characters, or null for \"Copy of\" the source's name.`,\n },\n },\n required: ['kind', 'sourceId', 'name'],\n additionalProperties: false,\n },\n strict: true,\n}\n\n/** A tool call's input, read. */\nexport type AiDuplicateResourceInput = {\n kind: DuplicableHostResourceKind\n sourceId: string\n name: string | null\n}\n\n/** What the tool answers: the copy, or a refusal the caller can relay. */\nexport type AiDuplicateResourceOutcome =\n | { ok: true; kind: DuplicableHostResourceKind; id: string; versionId: string | null; name: string }\n | { ok: false; status: number; error: string }\n\n/** Reads a tool call's input; `null` when it is not one this tool accepts. */\nexport function parseAiDuplicateResourceInput(\n input: Record<string, unknown>,\n): AiDuplicateResourceInput | null {\n const kind = input['kind']\n const sourceId = typeof input['sourceId'] === 'string' ? input['sourceId'].trim() : ''\n const name = input['name']\n if (!isDuplicableHostResourceKind(kind)) return null\n if (!sourceId || sourceId.length > 64 || sourceId.includes('/')) return null\n if (name !== null && typeof name !== 'string') return null\n const trimmed = typeof name === 'string' ? name.trim().slice(0, DUPLICATE_NAME_MAX) : ''\n return { kind, sourceId, name: trimmed || null }\n}\n\n/**\n * Makes the copy on `hostId` for the person the gate admitted. Refusals are\n * outcomes, never throws: a model's call that names a missing source or a\n * full band is an answer to relay, not a fault.\n */\nexport async function runAiDuplicateResourceTool(\n context: { gate: AiGateContext; hostId: string },\n input: Record<string, unknown>,\n): Promise<AiDuplicateResourceOutcome> {\n const { gate } = context\n const hostId = String(context.hostId ?? '').trim()\n const parsed = parseAiDuplicateResourceInput(input)\n if (!parsed) return { ok: false, status: 400, error: 'That is not a resource this site can copy' }\n if (!gate.reservation?.allowed) {\n return { ok: false, status: 429, error: 'This workspace cannot run AI requests right now' }\n }\n if (!hostId) return { ok: false, status: 400, error: 'Name the site to copy on' }\n\n const host = await gate.firestore.collection('hosts').doc(hostId).get()\n // A site of another workspace is a site this request cannot see.\n if (!host.exists || host.get('orgId') !== gate.orgId) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n if (!gate.staff) {\n const membership = await getOrgForUser(gate.uid, gate.orgId)\n const permitted =\n membership?.orgId === gate.orgId &&\n (await memberHasPermissionOnHost(gate.orgId, hostId, membership.member, 'ai.generate'))\n if (!permitted) {\n return {\n ok: false,\n status: 403,\n error: `Your role does not include \"${orgPermissionLabel('ai.generate')}\" — ask an organization admin`,\n }\n }\n }\n const memberRole = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[gate.uid]\n if (!hostRoleCanWrite(memberRole)) {\n return { ok: false, status: 403, error: 'Editing requires the editor role' }\n }\n\n const result = await duplicateResource(parsed.kind, {\n orgId: gate.orgId,\n hostId,\n sourceId: parsed.sourceId,\n name: parsed.name,\n uid: gate.uid,\n email: gate.decoded.email ? String(gate.decoded.email) : null,\n org: gate.org as Record<string, unknown>,\n attemptKey: null,\n })\n if (result.ok === false) return result\n return {\n ok: true,\n kind: parsed.kind,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n}\n"],"names":["DUPLICABLE_HOST_RESOURCE_KINDS","DUPLICATE_NAME_MAX","isDuplicableHostResourceKind","hostRoleCanWrite","orgPermissionLabel","duplicateResource","getOrgForUser","memberHasPermissionOnHost","AI_DUPLICATE_RESOURCE_TOOL_NAME","AI_DUPLICATE_RESOURCE_TOOL","name","description","inputSchema","type","properties","kind","enum","sourceId","required","additionalProperties","strict","parseAiDuplicateResourceInput","input","trim","length","includes","trimmed","slice","runAiDuplicateResourceTool","context","host","gate","hostId","String","parsed","ok","status","error","reservation","allowed","firestore","collection","doc","get","exists","orgId","staff","membership","uid","permitted","member","memberRole","result","email","decoded","org","attemptKey","id","versionId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/tools/ai-duplicate-resource-tool.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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\n DUPLICABLE_HOST_RESOURCE_KINDS,\n DUPLICATE_NAME_MAX,\n isDuplicableHostResourceKind,\n type DuplicableHostResourceKind,\n} from '@aglyn/aglyn/app-utils/duplicate-resource'\nimport { orgPermissionLabel } from '@aglyn/aglyn/app-utils/org-permissions'\nimport { duplicateResource } from '@aglyn/tenant-data-admin/server/duplicate-resource'\nimport {\n getOrgForUser,\n memberHasPermissionOnHost,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport type { AiTool } from '../providers/contract'\nimport type { AiGateContext } from '../runtime/ai-gate'\n\n/**\n * The `duplicate_resource` tool (AGL-2936, AGL-2984): a model that would\n * rebuild something the site already has copies it instead, through core's\n * `duplicateResource` — the same copy the row menus and the console door\n * make, with the same uniqueness rules, the same band arithmetic and the same\n * activity rows.\n *\n * It spends no tokens of its own, so it calls no provider: it runs inside a\n * door or a job step that has already climbed `aiGateLadder`, and takes that\n * ladder's context as its proof — the caller is signed in and verified, the\n * release flag, entitlement and lockdown rungs admitted the request, and the\n * workspace's band and caps admitted a reservation. On top of that it asks\n * what a copy needs whichever door called it: `ai.generate`, because a copy\n * is a generation's output, and a host role that may write the site, because\n * the copy lands there.\n *\n * DRAFTS ONLY, by construction of the copy: a screen copy has no route, a\n * workflow copy no trigger, and nothing places a component, layout or form\n * copy until a person does.\n */\n\nexport const AI_DUPLICATE_RESOURCE_TOOL_NAME = 'duplicate_resource'\n\n/** The structured-output tool a generation offers the model. */\nexport const AI_DUPLICATE_RESOURCE_TOOL: AiTool = {\n name: AI_DUPLICATE_RESOURCE_TOOL_NAME,\n description:\n 'Copy a resource this site already has — a page (kind \"screen\"), an email design, a component, a layout, ' +\n 'a template, a form or a workflow — as a new draft to build from, instead of creating it again. ' +\n 'The copy is unpublished: it has no address, runs nothing and is placed nowhere until a person does.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: {\n type: 'string',\n enum: [...DUPLICABLE_HOST_RESOURCE_KINDS],\n description: 'The kind of resource to copy.',\n },\n sourceId: {\n type: 'string',\n description: 'The id of the resource to copy, as the site inventory lists it.',\n },\n name: {\n type: ['string', 'null'],\n description: `The copy's name, at most ${DUPLICATE_NAME_MAX} characters, or null for \"Copy of\" the source's name.`,\n },\n },\n required: ['kind', 'sourceId', 'name'],\n additionalProperties: false,\n },\n strict: true,\n}\n\n/** A tool call's input, read. */\nexport type AiDuplicateResourceInput = {\n kind: DuplicableHostResourceKind\n sourceId: string\n name: string | null\n}\n\n/** What the tool answers: the copy, or a refusal the caller can relay. */\nexport type AiDuplicateResourceOutcome =\n | { ok: true; kind: DuplicableHostResourceKind; id: string; versionId: string | null; name: string }\n | { ok: false; status: number; error: string }\n\n/** Reads a tool call's input; `null` when it is not one this tool accepts. */\nexport function parseAiDuplicateResourceInput(\n input: Record<string, unknown>,\n): AiDuplicateResourceInput | null {\n const kind = input['kind']\n const sourceId = typeof input['sourceId'] === 'string' ? input['sourceId'].trim() : ''\n const name = input['name']\n if (!isDuplicableHostResourceKind(kind)) return null\n if (!sourceId || sourceId.length > 64 || sourceId.includes('/')) return null\n if (name !== null && typeof name !== 'string') return null\n const trimmed = typeof name === 'string' ? name.trim().slice(0, DUPLICATE_NAME_MAX) : ''\n return { kind, sourceId, name: trimmed || null }\n}\n\n/**\n * Makes the copy on `hostId` for the person the gate admitted. Refusals are\n * outcomes, never throws: a model's call that names a missing source or a\n * full band is an answer to relay, not a fault.\n */\nexport async function runAiDuplicateResourceTool(\n context: { gate: AiGateContext; hostId: string },\n input: Record<string, unknown>,\n): Promise<AiDuplicateResourceOutcome> {\n const { gate } = context\n const hostId = String(context.hostId ?? '').trim()\n const parsed = parseAiDuplicateResourceInput(input)\n if (!parsed) return { ok: false, status: 400, error: 'That is not a resource this site can copy' }\n if (!gate.reservation?.allowed) {\n return { ok: false, status: 429, error: 'This workspace cannot run AI requests right now' }\n }\n if (!hostId) return { ok: false, status: 400, error: 'Name the site to copy on' }\n\n const host = await gate.firestore.collection('hosts').doc(hostId).get()\n // A site of another workspace is a site this request cannot see.\n if (!host.exists || host.get('orgId') !== gate.orgId) {\n return { ok: false, status: 404, error: 'Unknown site' }\n }\n if (!gate.staff) {\n const membership = await getOrgForUser(gate.uid, gate.orgId)\n const permitted =\n membership?.orgId === gate.orgId &&\n (await memberHasPermissionOnHost(gate.orgId, hostId, membership.member, 'ai.generate'))\n if (!permitted) {\n return {\n ok: false,\n status: 403,\n error: `Your role does not include \"${orgPermissionLabel('ai.generate')}\" — ask an organization admin`,\n }\n }\n }\n if (!holdsSiteKey(host, gate.uid, 'site.edit')) {\n return { ok: false, status: 403, error: siteKeyRefusal('site.edit') }\n }\n\n const result = await duplicateResource(parsed.kind, {\n orgId: gate.orgId,\n hostId,\n sourceId: parsed.sourceId,\n name: parsed.name,\n uid: gate.uid,\n email: gate.decoded.email ? String(gate.decoded.email) : null,\n org: gate.org as Record<string, unknown>,\n attemptKey: null,\n })\n if (result.ok === false) return result\n return {\n ok: true,\n kind: parsed.kind,\n id: result.id,\n versionId: result.versionId,\n name: result.name,\n }\n}\n"],"names":["holdsSiteKey","siteKeyRefusal","DUPLICABLE_HOST_RESOURCE_KINDS","DUPLICATE_NAME_MAX","isDuplicableHostResourceKind","orgPermissionLabel","duplicateResource","getOrgForUser","memberHasPermissionOnHost","AI_DUPLICATE_RESOURCE_TOOL_NAME","AI_DUPLICATE_RESOURCE_TOOL","name","description","inputSchema","type","properties","kind","enum","sourceId","required","additionalProperties","strict","parseAiDuplicateResourceInput","input","trim","length","includes","trimmed","slice","runAiDuplicateResourceTool","context","gate","hostId","String","parsed","ok","status","error","reservation","allowed","host","firestore","collection","doc","get","exists","orgId","staff","membership","uid","permitted","member","result","email","decoded","org","attemptKey","id","versionId"],"mappings":"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"}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { type AiPermissionVerdict } from '../model/ai-permissions';
|
|
2
|
-
/** A roster row as the collaborators table hands it to a column. */
|
|
3
|
-
interface CollaboratorRow {
|
|
4
|
-
$id?: string;
|
|
5
|
-
uid?: string;
|
|
6
|
-
role?: string;
|
|
7
|
-
status?: string;
|
|
8
|
-
aiPermissions?: Partial<AiPermissionVerdict>;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* A collaborator's AI verdict on the site, as the row shows it: the roster
|
|
12
|
-
* copy the toggle door wrote, else the host role's default — which is what
|
|
13
|
-
* the doors resolve for a member document the toggle has never touched, so
|
|
14
|
-
* a row never shows a state the server does not hold.
|
|
15
|
-
*/
|
|
16
|
-
export declare function collaboratorAiPermissions(member: CollaboratorRow | undefined): AiPermissionVerdict;
|
|
17
|
-
/**
|
|
18
|
-
* THE COLLABORATORS TABLE'S AI COLUMN (AGL-2927, AGL-2984): whether this
|
|
19
|
-
* person may open the AI doors ON THIS SITE, through the `hostMembers`
|
|
20
|
-
* zone. The host role sets the default; the boxes refine it, and the doors
|
|
21
|
-
* read the same answer, so an unticked box is a closed door rather than a
|
|
22
|
-
* hidden button. The owner is decided by the org role, and says so.
|
|
23
|
-
*/
|
|
24
|
-
export declare function AiCollaboratorPermissionsCell(props: {
|
|
25
|
-
hostId?: string;
|
|
26
|
-
canManage?: boolean;
|
|
27
|
-
member?: CollaboratorRow;
|
|
28
|
-
}): import("react").JSX.Element;
|
|
29
|
-
export declare namespace AiCollaboratorPermissionsCell {
|
|
30
|
-
var displayName: string;
|
|
31
|
-
}
|
|
32
|
-
export {};
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Aglyn LLC
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*/ 'use client';
|
|
17
|
-
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
-
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
19
|
-
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
20
|
-
import { useUser } from "@aglyn/tenant-feature-instance";
|
|
21
|
-
import { Checkbox, FormControlLabel, Stack } from "@mui/material";
|
|
22
|
-
import { useCallback, useState } from "react";
|
|
23
|
-
import { AI_PERMISSION_KEYS, HOST_ROLE_AI_PERMISSIONS } from "../model/ai-permissions.js";
|
|
24
|
-
/** What each toggle is called in the row. */ const TOGGLE_LABELS = {
|
|
25
|
-
'ai.use': 'Assist',
|
|
26
|
-
'ai.generate': 'Generate'
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* A collaborator's AI verdict on the site, as the row shows it: the roster
|
|
30
|
-
* copy the toggle door wrote, else the host role's default — which is what
|
|
31
|
-
* the doors resolve for a member document the toggle has never touched, so
|
|
32
|
-
* a row never shows a state the server does not hold.
|
|
33
|
-
*/ export function collaboratorAiPermissions(member) {
|
|
34
|
-
var _ref, _HOST_ROLE_AI_PERMISSIONS_role, _ref1, _ref2;
|
|
35
|
-
const role = (_ref = member == null ? void 0 : member.role) != null ? _ref : 'editor';
|
|
36
|
-
const base = (_HOST_ROLE_AI_PERMISSIONS_role = HOST_ROLE_AI_PERMISSIONS[role]) != null ? _HOST_ROLE_AI_PERMISSIONS_role : HOST_ROLE_AI_PERMISSIONS.viewer;
|
|
37
|
-
const stored = member == null ? void 0 : member.aiPermissions;
|
|
38
|
-
return {
|
|
39
|
-
'ai.use': (_ref1 = stored == null ? void 0 : stored['ai.use']) != null ? _ref1 : base['ai.use'],
|
|
40
|
-
'ai.generate': (_ref2 = stored == null ? void 0 : stored['ai.generate']) != null ? _ref2 : base['ai.generate']
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* THE COLLABORATORS TABLE'S AI COLUMN (AGL-2927, AGL-2984): whether this
|
|
45
|
-
* person may open the AI doors ON THIS SITE, through the `hostMembers`
|
|
46
|
-
* zone. The host role sets the default; the boxes refine it, and the doors
|
|
47
|
-
* read the same answer, so an unticked box is a closed door rather than a
|
|
48
|
-
* hidden button. The owner is decided by the org role, and says so.
|
|
49
|
-
*/ export function AiCollaboratorPermissionsCell(props) {
|
|
50
|
-
const { hostId, canManage, member } = props;
|
|
51
|
-
const { data: user } = useUser();
|
|
52
|
-
const { enqueueSnackbar } = useSnackbar();
|
|
53
|
-
const [busy, setBusy] = useState(false);
|
|
54
|
-
const memberId = member == null ? void 0 : member.$id;
|
|
55
|
-
const handleToggle = useCallback((key)=>async (_event, checked)=>{
|
|
56
|
-
if (!memberId || !hostId) return;
|
|
57
|
-
setBusy(true);
|
|
58
|
-
try {
|
|
59
|
-
const response = await authorizedFetch(user, '/api/ai/host-permissions', {
|
|
60
|
-
method: 'PATCH',
|
|
61
|
-
headers: {
|
|
62
|
-
'Content-Type': 'application/json'
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
hostId,
|
|
66
|
-
memberId,
|
|
67
|
-
aiPermissions: {
|
|
68
|
-
[key]: checked
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
});
|
|
72
|
-
const payload = await response.json().catch(()=>({}));
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
var _ref;
|
|
75
|
-
enqueueSnackbar((_ref = payload == null ? void 0 : payload.error) != null ? _ref : 'Member operation failed', {
|
|
76
|
-
variant: 'warning',
|
|
77
|
-
persist: false
|
|
78
|
-
});
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
enqueueSnackbar('AI access updated', {
|
|
82
|
-
variant: 'success',
|
|
83
|
-
persist: false
|
|
84
|
-
});
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error(error);
|
|
87
|
-
enqueueSnackbar('Member operation failed', {
|
|
88
|
-
variant: 'error'
|
|
89
|
-
});
|
|
90
|
-
} finally{
|
|
91
|
-
setBusy(false);
|
|
92
|
-
}
|
|
93
|
-
}, [
|
|
94
|
-
user,
|
|
95
|
-
hostId,
|
|
96
|
-
memberId,
|
|
97
|
-
enqueueSnackbar
|
|
98
|
-
]);
|
|
99
|
-
if ((member == null ? void 0 : member.role) === 'owner') return /*#__PURE__*/ _jsx(_Fragment, {
|
|
100
|
-
children: 'By org role'
|
|
101
|
-
});
|
|
102
|
-
const verdict = collaboratorAiPermissions(member);
|
|
103
|
-
return(// An invited row has no member document to carry a toggle yet; it
|
|
104
|
-
// becomes settable once the invite is accepted, which is when the person
|
|
105
|
-
// gains the role the default derives from.
|
|
106
|
-
/*#__PURE__*/ _jsx(Stack, {
|
|
107
|
-
direction: "row",
|
|
108
|
-
spacing: 0,
|
|
109
|
-
children: AI_PERMISSION_KEYS.map((key)=>/*#__PURE__*/ _jsx(FormControlLabel, {
|
|
110
|
-
label: TOGGLE_LABELS[key],
|
|
111
|
-
slotProps: {
|
|
112
|
-
typography: {
|
|
113
|
-
variant: 'caption'
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
control: /*#__PURE__*/ _jsx(Checkbox, {
|
|
117
|
-
size: "small",
|
|
118
|
-
checked: verdict[key],
|
|
119
|
-
onChange: handleToggle(key),
|
|
120
|
-
disabled: busy || !canManage || (member == null ? void 0 : member.status) === 'invited',
|
|
121
|
-
slotProps: {
|
|
122
|
-
input: {
|
|
123
|
-
'aria-label': `${TOGGLE_LABELS[key]} with AI`
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
}, key))
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
AiCollaboratorPermissionsCell.displayName = 'AiCollaboratorPermissionsCell';
|
|
131
|
-
|
|
132
|
-
//# sourceMappingURL=ai-collaborator-permissions-column.component.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/components/ai-collaborator-permissions-column.component.tsx"],"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'use client'\n\nimport type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport { Checkbox, FormControlLabel, Stack } from '@mui/material'\nimport { useCallback, useState } from 'react'\nimport {\n AI_PERMISSION_KEYS,\n HOST_ROLE_AI_PERMISSIONS,\n type AiPermission,\n type AiPermissionVerdict,\n} from '../model/ai-permissions'\n\n/** A roster row as the collaborators table hands it to a column. */\ninterface CollaboratorRow {\n $id?: string\n uid?: string\n role?: string\n status?: string\n aiPermissions?: Partial<AiPermissionVerdict>\n}\n\n/** What each toggle is called in the row. */\nconst TOGGLE_LABELS: Record<AiPermission, string> = {\n 'ai.use': 'Assist',\n 'ai.generate': 'Generate',\n}\n\n/**\n * A collaborator's AI verdict on the site, as the row shows it: the roster\n * copy the toggle door wrote, else the host role's default — which is what\n * the doors resolve for a member document the toggle has never touched, so\n * a row never shows a state the server does not hold.\n */\nexport function collaboratorAiPermissions(member: CollaboratorRow | undefined): AiPermissionVerdict {\n const role = (member?.role ?? 'editor') as HostAccessRole\n const base = HOST_ROLE_AI_PERMISSIONS[role] ?? HOST_ROLE_AI_PERMISSIONS.viewer\n const stored = member?.aiPermissions\n return {\n 'ai.use': stored?.['ai.use'] ?? base['ai.use'],\n 'ai.generate': stored?.['ai.generate'] ?? base['ai.generate'],\n }\n}\n\n/**\n * THE COLLABORATORS TABLE'S AI COLUMN (AGL-2927, AGL-2984): whether this\n * person may open the AI doors ON THIS SITE, through the `hostMembers`\n * zone. The host role sets the default; the boxes refine it, and the doors\n * read the same answer, so an unticked box is a closed door rather than a\n * hidden button. The owner is decided by the org role, and says so.\n */\nexport function AiCollaboratorPermissionsCell(props: {\n hostId?: string\n canManage?: boolean\n member?: CollaboratorRow\n}) {\n const { hostId, canManage, member } = props\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const [busy, setBusy] = useState(false)\n const memberId = member?.$id\n\n const handleToggle = useCallback(\n (key: AiPermission) => async (_event: unknown, checked: boolean) => {\n if (!memberId || !hostId) return\n setBusy(true)\n try {\n const response = await authorizedFetch(user, '/api/ai/host-permissions', {\n method: 'PATCH',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, memberId, aiPermissions: { [key]: checked } }),\n })\n const payload = await response.json().catch(() => ({}))\n if (!response.ok) {\n enqueueSnackbar(payload?.error ?? 'Member operation failed', {\n variant: 'warning',\n persist: false,\n })\n return\n }\n enqueueSnackbar('AI access updated', { variant: 'success', persist: false })\n } catch (error) {\n console.error(error)\n enqueueSnackbar('Member operation failed', { variant: 'error' })\n } finally {\n setBusy(false)\n }\n },\n [user, hostId, memberId, enqueueSnackbar],\n )\n\n if (member?.role === 'owner') return <>{'By org role'}</>\n const verdict = collaboratorAiPermissions(member)\n return (\n // An invited row has no member document to carry a toggle yet; it\n // becomes settable once the invite is accepted, which is when the person\n // gains the role the default derives from.\n <Stack direction=\"row\" spacing={0}>\n {AI_PERMISSION_KEYS.map((key) => (\n <FormControlLabel\n key={key}\n label={TOGGLE_LABELS[key]}\n slotProps={{ typography: { variant: 'caption' } }}\n control={\n <Checkbox\n size=\"small\"\n checked={verdict[key]}\n onChange={handleToggle(key)}\n disabled={busy || !canManage || member?.status === 'invited'}\n slotProps={{\n input: { 'aria-label': `${TOGGLE_LABELS[key]} with AI` },\n }}\n />\n }\n />\n ))}\n </Stack>\n )\n}\nAiCollaboratorPermissionsCell.displayName = 'AiCollaboratorPermissionsCell'\n"],"names":["useSnackbar","authorizedFetch","useUser","Checkbox","FormControlLabel","Stack","useCallback","useState","AI_PERMISSION_KEYS","HOST_ROLE_AI_PERMISSIONS","TOGGLE_LABELS","collaboratorAiPermissions","member","role","base","viewer","stored","aiPermissions","AiCollaboratorPermissionsCell","props","hostId","canManage","data","user","enqueueSnackbar","busy","setBusy","memberId","$id","handleToggle","key","_event","checked","response","method","headers","body","JSON","stringify","payload","json","catch","ok","error","variant","persist","console","verdict","direction","spacing","map","label","slotProps","typography","control","size","onChange","disabled","status","input","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;AAGA,SAASA,WAAW,QAAQ,8BAA6B;AACzD,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,OAAO,QAAQ,iCAAgC;AACxD,SAASC,QAAQ,EAAEC,gBAAgB,EAAEC,KAAK,QAAQ,gBAAe;AACjE,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AAC7C,SACEC,kBAAkB,EAClBC,wBAAwB,QAGnB,6BAAyB;AAWhC,2CAA2C,GAC3C,MAAMC,gBAA8C;IAClD,UAAU;IACV,eAAe;AACjB;AAEA;;;;;CAKC,GACD,OAAO,SAASC,0BAA0BC,MAAmC;cAE9DH;IADb,MAAMI,eAAQD,0BAAAA,OAAQC,IAAI,mBAAI;IAC9B,MAAMC,QAAOL,iCAAAA,wBAAwB,CAACI,KAAK,YAA9BJ,iCAAkCA,yBAAyBM,MAAM;IAC9E,MAAMC,SAASJ,0BAAAA,OAAQK,aAAa;IACpC,OAAO;QACL,QAAQ,WAAED,0BAAAA,MAAQ,CAAC,SAAS,oBAAIF,IAAI,CAAC,SAAS;QAC9C,aAAa,WAAEE,0BAAAA,MAAQ,CAAC,cAAc,oBAAIF,IAAI,CAAC,cAAc;IAC/D;AACF;AAEA;;;;;;CAMC,GACD,OAAO,SAASI,8BAA8BC,KAI7C;IACC,MAAM,EAAEC,MAAM,EAAEC,SAAS,EAAET,MAAM,EAAE,GAAGO;IACtC,MAAM,EAAEG,MAAMC,IAAI,EAAE,GAAGrB;IACvB,MAAM,EAAEsB,eAAe,EAAE,GAAGxB;IAC5B,MAAM,CAACyB,MAAMC,QAAQ,GAAGnB,SAAS;IACjC,MAAMoB,WAAWf,0BAAAA,OAAQgB,GAAG;IAE5B,MAAMC,eAAevB,YACnB,CAACwB,MAAsB,OAAOC,QAAiBC;YAC7C,IAAI,CAACL,YAAY,CAACP,QAAQ;YAC1BM,QAAQ;YACR,IAAI;gBACF,MAAMO,WAAW,MAAMhC,gBAAgBsB,MAAM,4BAA4B;oBACvEW,QAAQ;oBACRC,SAAS;wBAAE,gBAAgB;oBAAmB;oBAC9CC,MAAMC,KAAKC,SAAS,CAAC;wBAAElB;wBAAQO;wBAAUV,eAAe;4BAAE,CAACa,IAAI,EAAEE;wBAAQ;oBAAE;gBAC7E;gBACA,MAAMO,UAAU,MAAMN,SAASO,IAAI,GAAGC,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;gBACpD,IAAI,CAACR,SAASS,EAAE,EAAE;;oBAChBlB,wBAAgBe,2BAAAA,QAASI,KAAK,mBAAI,2BAA2B;wBAC3DC,SAAS;wBACTC,SAAS;oBACX;oBACA;gBACF;gBACArB,gBAAgB,qBAAqB;oBAAEoB,SAAS;oBAAWC,SAAS;gBAAM;YAC5E,EAAE,OAAOF,OAAO;gBACdG,QAAQH,KAAK,CAACA;gBACdnB,gBAAgB,2BAA2B;oBAAEoB,SAAS;gBAAQ;YAChE,SAAU;gBACRlB,QAAQ;YACV;QACF,GACA;QAACH;QAAMH;QAAQO;QAAUH;KAAgB;IAG3C,IAAIZ,CAAAA,0BAAAA,OAAQC,IAAI,MAAK,SAAS,qBAAO;kBAAG;;IACxC,MAAMkC,UAAUpC,0BAA0BC;IAC1C,OACE,kEAAkE;IAClE,yEAAyE;IACzE,2CAA2C;kBAC3C,KAACP;QAAM2C,WAAU;QAAMC,SAAS;kBAC7BzC,mBAAmB0C,GAAG,CAAC,CAACpB,oBACvB,KAAC1B;gBAEC+C,OAAOzC,aAAa,CAACoB,IAAI;gBACzBsB,WAAW;oBAAEC,YAAY;wBAAET,SAAS;oBAAU;gBAAE;gBAChDU,uBACE,KAACnD;oBACCoD,MAAK;oBACLvB,SAASe,OAAO,CAACjB,IAAI;oBACrB0B,UAAU3B,aAAaC;oBACvB2B,UAAUhC,QAAQ,CAACJ,aAAaT,CAAAA,0BAAAA,OAAQ8C,MAAM,MAAK;oBACnDN,WAAW;wBACTO,OAAO;4BAAE,cAAc,GAAGjD,aAAa,CAACoB,IAAI,CAAC,QAAQ,CAAC;wBAAC;oBACzD;;eAXCA;;AAkBf;AACAZ,8BAA8B0C,WAAW,GAAG"}
|