@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.288
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-business-profile-prefill.js +6 -3
- package/src/lib/jobs/ai-business-profile-prefill.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 +300 -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-text-step.js +2 -1
- package/src/lib/jobs/ai-job-text-step.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 +155 -7
- package/src/lib/jobs/ai-layout-stock-photos.js +1517 -220
- 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-site-agl4092-datasets.d.ts +120 -0
- package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js +410 -0
- package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.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/jobs/fixtures/ai-stock-photo-agl4092-recordings.d.ts +26 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js +714 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-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 +39 -1
- package/src/lib/layout-language/ai-layout-pictures.js +141 -4
- 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-plan-said-sections.d.ts +12 -3
- package/src/lib/model/ai-plan-said-sections.js +22 -5
- package/src/lib/model/ai-plan-said-sections.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-context.d.ts +6 -2
- package/src/lib/model/ai-site-context.js +8 -2
- package/src/lib/model/ai-site-context.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 +120 -1
- package/src/lib/runtime/ai-dataset-generation.js +352 -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 +345 -28
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.d.ts +5 -4
- package/src/lib/runtime/ai-doctrine.js +10 -3
- 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.d.ts +15 -1
- package/src/lib/server/assist-build.js +30 -3
- 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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-icon-library.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The icons a page built by the platform's own code may draw (AGL-3660).\n *\n * An icon element draws the path its picker stores beside the id, which a\n * model can never supply, so a tree a model writes keeps no icon\n * (`ai-node-tree.ts` drops the id). The layout language compiler is code: the\n * model names one of these by its word, and the compiler writes the Material\n * Design Icons id and the path copied here, exactly as the icon picker stores\n * them — so the published page draws it without loading the icon catalog.\n *\n * Paths are Material Design Icons 6.5.95 (Apache-2.0), the same set the\n * picker offers (`@aglyn/shared-data-mdi`). The words are the vocabulary the\n * language's schema enumerates; a word added here is a word the model may\n * write.\n */\n\n/** The palette element an icon is drawn with. */\nexport const AI_ICON_COMPONENT_ID = 'icon'\n\nexport interface AiIconLibraryEntry {\n /** The id the icon picker stores. */\n id: string\n /** The SVG path the picker stores beside it. */\n path: string\n}\n\nexport const AI_ICON_LIBRARY = {\n check: {\n id: 'check-circle-outline',\n path: 'M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z',\n },\n star: {\n id: 'star-outline',\n path: 'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',\n },\n heart: {\n id: 'heart-outline',\n path: 'M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z',\n },\n clock: {\n id: 'clock-outline',\n path: 'M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z',\n },\n calendar: {\n id: 'calendar-check-outline',\n path: 'M19,4H18V2H16V4H8V2H6V4H5A2,2 0 0,0 3,6V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V6A2,2 0 0,0 19,4M19,20H5V10H19V20M5,8V6H19V8H5M10.56,18.46L16.5,12.53L15.43,11.47L10.56,16.34L8.45,14.23L7.39,15.29L10.56,18.46Z',\n },\n location: {\n id: 'map-marker-outline',\n path: 'M12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5M12,2A7,7 0 0,1 19,9C19,14.25 12,22 12,22C12,22 5,14.25 5,9A7,7 0 0,1 12,2M12,4A5,5 0 0,0 7,9C7,10 7,12 12,18.71C17,12 17,10 17,9A5,5 0 0,0 12,4Z',\n },\n shield: {\n id: 'shield-check-outline',\n path: 'M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9',\n },\n leaf: {\n id: 'leaf',\n path: 'M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z',\n },\n home: {\n id: 'home-outline',\n path: 'M12 5.69L17 10.19V18H15V12H9V18H7V10.19L12 5.69M12 3L2 12H5V20H11V14H13V20H19V12H22L12 3Z',\n },\n tools: {\n id: 'tools',\n path: 'M21.71 20.29L20.29 21.71A1 1 0 0 1 18.88 21.71L7 9.85A3.81 3.81 0 0 1 6 10A4 4 0 0 1 2.22 4.7L4.76 7.24L5.29 6.71L6.71 5.29L7.24 4.76L4.7 2.22A4 4 0 0 1 10 6A3.81 3.81 0 0 1 9.85 7L21.71 18.88A1 1 0 0 1 21.71 20.29M2.29 18.88A1 1 0 0 0 2.29 20.29L3.71 21.71A1 1 0 0 0 5.12 21.71L10.59 16.25L7.76 13.42M20 2L16 4V6L13.83 8.17L15.83 10.17L18 8H20L22 4Z',\n },\n paw: {\n id: 'paw',\n path: 'M8.35,3C9.53,2.83 10.78,4.12 11.14,5.9C11.5,7.67 10.85,9.25 9.67,9.43C8.5,9.61 7.24,8.32 6.87,6.54C6.5,4.77 7.17,3.19 8.35,3M15.5,3C16.69,3.19 17.35,4.77 17,6.54C16.62,8.32 15.37,9.61 14.19,9.43C13,9.25 12.35,7.67 12.72,5.9C13.08,4.12 14.33,2.83 15.5,3M3,7.6C4.14,7.11 5.69,8 6.5,9.55C7.26,11.13 7,12.79 5.87,13.28C4.74,13.77 3.2,12.89 2.41,11.32C1.62,9.75 1.9,8.08 3,7.6M21,7.6C22.1,8.08 22.38,9.75 21.59,11.32C20.8,12.89 19.26,13.77 18.13,13.28C17,12.79 16.74,11.13 17.5,9.55C18.31,8 19.86,7.11 21,7.6M19.33,18.38C19.37,19.32 18.65,20.36 17.79,20.75C16,21.57 13.88,19.87 11.89,19.87C9.9,19.87 7.76,21.64 6,20.75C5,20.26 4.31,18.96 4.44,17.88C4.62,16.39 6.41,15.59 7.47,14.5C8.88,13.09 9.88,10.44 11.89,10.44C13.89,10.44 14.95,13.05 16.3,14.5C17.41,15.72 19.26,16.75 19.33,18.38Z',\n },\n tooth: {\n id: 'tooth-outline',\n path: 'M7,2C4,2 2,5 2,8C2,10.11 3,13 4,14C5,15 6,22 8,22C12.54,22 10,15 12,15C14,15 11.46,22 16,22C18,22 19,15 20,14C21,13 22,10.11 22,8C22,5 20,2 17,2C14,2 14,3 12,3C10,3 10,2 7,2M7,4C9,4 10,5 12,5C14,5 15,4 17,4C18.67,4 20,6 20,8C20,9.75 19.14,12.11 18.19,13.06C17.33,13.92 16.06,19.94 15.5,19.94C15.29,19.94 15,18.88 15,17.59C15,15.55 14.43,13 12,13C9.57,13 9,15.55 9,17.59C9,18.88 8.71,19.94 8.5,19.94C7.94,19.94 6.67,13.92 5.81,13.06C4.86,12.11 4,9.75 4,8C4,6 5.33,4 7,4Z',\n },\n coffee: {\n id: 'coffee-outline',\n path: 'M2,21V19H20V21H2M20,8V5H18V8H20M20,3A2,2 0 0,1 22,5V8A2,2 0 0,1 20,10H18V13A4,4 0 0,1 14,17H8A4,4 0 0,1 4,13V3H20M16,5H6V13A2,2 0 0,0 8,15H14A2,2 0 0,0 16,13V5Z',\n },\n yoga: {\n id: 'meditation',\n path: 'M12 4C13.11 4 14 4.89 14 6S13.11 8 12 8 10 7.11 10 6 10.9 4 12 4M21 16V14C18.76 14 16.84 13.04 15.4 11.32L14.06 9.72C13.68 9.26 13.12 9 12.53 9H11.5C10.89 9 10.33 9.26 9.95 9.72L8.61 11.32C7.16 13.04 5.24 14 3 14V16C5.77 16 8.19 14.83 10 12.75V15L6.12 16.55C5.45 16.82 5 17.5 5 18.21C5 19.2 5.8 20 6.79 20H9V19.5C9 18.12 10.12 17 11.5 17H14.5C14.78 17 15 17.22 15 17.5S14.78 18 14.5 18H11.5C10.67 18 10 18.67 10 19.5V20H17.21C18.2 20 19 19.2 19 18.21C19 17.5 18.55 16.82 17.88 16.55L14 15V12.75C15.81 14.83 18.23 16 21 16Z',\n },\n law: {\n id: 'scale-balance',\n path: 'M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z',\n },\n roof: {\n id: 'home-roof',\n path: 'M19 16H22L12 7L2 16H5L12 9.69L19 16M7 8.81V7H4V11.5L7 8.81Z',\n },\n people: {\n id: 'account-group-outline',\n path: 'M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z',\n },\n chat: {\n id: 'message-text-outline',\n path: 'M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z',\n },\n fast: {\n id: 'lightning-bolt-outline',\n path: 'M11 9.47V11H14.76L13 14.53V13H9.24L11 9.47M13 1L6 15H11V23L18 9H13V1Z',\n },\n sparkle: {\n id: 'creation',\n path: 'M19,1L17.74,3.75L15,5L17.74,6.26L19,9L20.25,6.26L23,5L20.25,3.75M9,4L6.5,9.5L1,12L6.5,14.5L9,20L11.5,14.5L17,12L11.5,9.5M19,15L17.74,17.74L15,19L17.74,20.25L19,23L20.25,20.25L23,19L20.25,17.74',\n },\n care: {\n id: 'hand-heart-outline',\n path: 'M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z',\n },\n award: {\n id: 'medal-outline',\n path: 'M14.94 19.5L12 17.77L9.06 19.5L9.84 16.16L7.25 13.92L10.66 13.63L12 10.5L13.34 13.63L16.75 13.92L14.16 16.16M20 2H4V4L8.86 7.64A8 8 0 1 0 15.14 7.64L20 4M18 15A6 6 0 1 1 10.82 9.12A5.86 5.86 0 0 1 13.18 9.12A6 6 0 0 1 18 15M12.63 7H11.37L7.37 4H16.71Z',\n },\n truck: {\n id: 'truck-outline',\n path: 'M18 18.5C18.83 18.5 19.5 17.83 19.5 17C19.5 16.17 18.83 15.5 18 15.5C17.17 15.5 16.5 16.17 16.5 17C16.5 17.83 17.17 18.5 18 18.5M19.5 9.5H17V12H21.46L19.5 9.5M6 18.5C6.83 18.5 7.5 17.83 7.5 17C7.5 16.17 6.83 15.5 6 15.5C5.17 15.5 4.5 16.17 4.5 17C4.5 17.83 5.17 18.5 6 18.5M20 8L23 12V17H21C21 18.66 19.66 20 18 20C16.34 20 15 18.66 15 17H9C9 18.66 7.66 20 6 20C4.34 20 3 18.66 3 17H1V6C1 4.89 1.89 4 3 4H17V8H20M3 6V15H3.76C4.31 14.39 5.11 14 6 14C6.89 14 7.69 14.39 8.24 15H15V6H3Z',\n },\n learn: {\n id: 'school-outline',\n path: 'M12 3L1 9L5 11.18V17.18L12 21L19 17.18V11.18L21 10.09V17H23V9L12 3M18.82 9L12 12.72L5.18 9L12 5.28L18.82 9M17 16L12 18.72L7 16V12.27L12 15L17 12.27V16Z',\n },\n camera: {\n id: 'camera-outline',\n path: 'M20,4H16.83L15,2H9L7.17,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6A2,2 0 0,0 20,4M20,18H4V6H8.05L9.88,4H14.12L15.95,6H20V18M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15Z',\n },\n design: {\n id: 'palette-outline',\n path: 'M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z',\n },\n spa: {\n id: 'spa-outline',\n path: 'M15.5,9.63C14.21,10.32 13.03,11.2 12,12.26C10.97,11.19 9.79,10.31 8.5,9.63C8.74,6.86 9.92,4.14 12.06,2C14.18,4.12 15.31,6.84 15.5,9.63M12,15.45C14.15,12.17 17.82,10 22,10C22,20 12.68,21.88 12,22C11.32,21.89 2,20 2,10C6.18,10 9.85,12.17 12,15.45M12.05,5.19C11.39,6.23 10.93,7.38 10.68,8.58L12,9.55L13.35,8.57C13.12,7.37 12.68,6.22 12.05,5.19M12,19.97C12,19.97 18,19 19.74,12.25C14,14 12,19.1 12,19.1C12,19.1 9,13 4.26,12.26C6,19 12,19.97 12,19.97Z',\n },\n fitness: {\n id: 'dumbbell',\n path: 'M20.57,14.86L22,13.43L20.57,12L17,15.57L8.43,7L12,3.43L10.57,2L9.14,3.43L7.71,2L5.57,4.14L4.14,2.71L2.71,4.14L4.14,5.57L2,7.71L3.43,9.14L2,10.57L3.43,12L7,8.43L15.57,17L12,20.57L13.43,22L14.86,20.57L16.29,22L18.43,19.86L19.86,21.29L21.29,19.86L19.86,18.43L22,16.29L20.57,14.86Z',\n },\n phone: {\n id: 'phone-outline',\n path: 'M20,15.5C18.8,15.5 17.5,15.3 16.4,14.9C16.3,14.9 16.2,14.9 16.1,14.9C15.8,14.9 15.6,15 15.4,15.2L13.2,17.4C10.4,15.9 8,13.6 6.6,10.8L8.8,8.6C9.1,8.3 9.2,7.9 9,7.6C8.7,6.5 8.5,5.2 8.5,4C8.5,3.5 8,3 7.5,3H4C3.5,3 3,3.5 3,4C3,13.4 10.6,21 20,21C20.5,21 21,20.5 21,20V16.5C21,16 20.5,15.5 20,15.5M5,5H6.5C6.6,5.9 6.8,6.8 7,7.6L5.8,8.8C5.4,7.6 5.1,6.3 5,5M19,19C17.7,18.9 16.4,18.6 15.2,18.2L16.4,17C17.2,17.2 18.1,17.4 19,17.4V19Z',\n },\n mail: {\n id: 'email-outline',\n path: 'M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z',\n },\n money: {\n id: 'cash-multiple',\n path: 'M5,6H23V18H5V6M14,9A3,3 0 0,1 17,12A3,3 0 0,1 14,15A3,3 0 0,1 11,12A3,3 0 0,1 14,9M9,8A2,2 0 0,1 7,10V14A2,2 0 0,1 9,16H19A2,2 0 0,1 21,14V10A2,2 0 0,1 19,8H9M1,10H3V20H19V22H1V10Z',\n },\n chart: {\n id: 'chart-line',\n path: 'M16,11.78L20.24,4.45L21.97,5.45L16.74,14.5L10.23,10.75L5.46,19H22V21H2V3H4V17.54L9.5,8L16,11.78Z',\n },\n lock: {\n id: 'lock-outline',\n path: 'M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z',\n },\n smile: {\n id: 'emoticon-happy-outline',\n path: 'M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z',\n },\n car: {\n id: 'car-outline',\n path: 'M18.9 6C18.7 5.4 18.1 5 17.5 5H6.5C5.8 5 5.3 5.4 5.1 6L3 12V20C3 20.5 3.5 21 4 21H5C5.6 21 6 20.5 6 20V19H18V20C18 20.5 18.5 21 19 21H20C20.5 21 21 20.5 21 20V12L18.9 6M6.8 7H17.1L18.2 10H5.8L6.8 7M19 17H5V12H19V17M7.5 13C8.3 13 9 13.7 9 14.5S8.3 16 7.5 16 6 15.3 6 14.5 6.7 13 7.5 13M16.5 13C17.3 13 18 13.7 18 14.5S17.3 16 16.5 16C15.7 16 15 15.3 15 14.5S15.7 13 16.5 13Z',\n },\n food: {\n id: 'silverware-fork-knife',\n path: 'M11,9H9V2H7V9H5V2H3V9C3,11.12 4.66,12.84 6.75,12.97V22H9.25V12.97C11.34,12.84 13,11.12 13,9V2H11V9M16,6V14H18.5V22H21V2C18.24,2 16,4.24 16,6Z',\n },\n cake: {\n id: 'cake-variant-outline',\n path: 'M12 6C13.11 6 14 5.1 14 4C14 3.62 13.9 3.27 13.71 2.97L12 0L10.29 2.97C10.1 3.27 10 3.62 10 4C10 5.1 10.9 6 12 6M18 9H13V7H11V9H6C4.34 9 3 10.34 3 12V21C3 21.55 3.45 22 4 22H20C20.55 22 21 21.55 21 21V12C21 10.34 19.66 9 18 9M19 20H5V17C5.9 17 6.76 16.63 7.4 16L8.5 14.92L9.56 16C10.87 17.3 13.15 17.29 14.45 16L15.53 14.92L16.6 16C17.24 16.63 18.1 17 19 17V20M19 15.5C18.5 15.5 18 15.3 17.65 14.93L15.5 12.8L13.38 14.93C12.64 15.67 11.35 15.67 10.61 14.93L8.5 12.8L6.34 14.93C6 15.29 5.5 15.5 5 15.5V12C5 11.45 5.45 11 6 11H18C18.55 11 19 11.45 19 12V15.5Z',\n },\n cut: {\n id: 'content-cut',\n path: 'M19,3L13,9L15,11L22,4V3M12,12.5A0.5,0.5 0 0,1 11.5,12A0.5,0.5 0 0,1 12,11.5A0.5,0.5 0 0,1 12.5,12A0.5,0.5 0 0,1 12,12.5M6,20A2,2 0 0,1 4,18C4,16.89 4.9,16 6,16A2,2 0 0,1 8,18C8,19.11 7.1,20 6,20M6,8A2,2 0 0,1 4,6C4,4.89 4.9,4 6,4A2,2 0 0,1 8,6C8,7.11 7.1,8 6,8M9.64,7.64C9.87,7.14 10,6.59 10,6A4,4 0 0,0 6,2A4,4 0 0,0 2,6A4,4 0 0,0 6,10C6.59,10 7.14,9.87 7.64,9.64L10,12L7.64,14.36C7.14,14.13 6.59,14 6,14A4,4 0 0,0 2,18A4,4 0 0,0 6,22A4,4 0 0,0 10,18C10,17.41 9.87,16.86 9.64,16.36L12,14L19,21H22V20L9.64,7.64Z',\n },\n brush: {\n id: 'brush-variant',\n path: 'M8 3C5.79 3 4 4.79 4 7V14C4 15.1 4.9 16 6 16H9V20C9 21.1 9.9 22 11 22H13C14.1 22 15 21.1 15 20V16H18C19.1 16 20 15.1 20 14V3H8M8 5H12V7H14V5H15V9H17V5H18V10H6V7C6 5.9 6.9 5 8 5M6 14V12H18V14H6Z',\n },\n hammer: {\n id: 'hammer-wrench',\n path: 'M13.78 15.3L19.78 21.3L21.89 19.14L15.89 13.14L13.78 15.3M17.5 10.1C17.11 10.1 16.69 10.05 16.36 9.91L4.97 21.25L2.86 19.14L10.27 11.74L8.5 9.96L7.78 10.66L6.33 9.25V12.11L5.63 12.81L2.11 9.25L2.81 8.55H5.62L4.22 7.14L7.78 3.58C8.95 2.41 10.83 2.41 12 3.58L9.89 5.74L11.3 7.14L10.59 7.85L12.38 9.63L14.2 7.75C14.06 7.42 14 7 14 6.63C14 4.66 15.56 3.11 17.5 3.11C18.09 3.11 18.61 3.25 19.08 3.53L16.41 6.2L17.91 7.7L20.58 5.03C20.86 5.5 21 6 21 6.63C21 8.55 19.45 10.1 17.5 10.1Z',\n },\n water: {\n id: 'water-outline',\n path: 'M12,3.77L11.25,4.61C11.25,4.61 9.97,6.06 8.68,7.94C7.39,9.82 6,12.07 6,14.23A6,6 0 0,0 12,20.23A6,6 0 0,0 18,14.23C18,12.07 16.61,9.82 15.32,7.94C14.03,6.06 12.75,4.61 12.75,4.61L12,3.77M12,6.9C12.44,7.42 12.84,7.85 13.68,9.07C14.89,10.83 16,13.07 16,14.23C16,16.45 14.22,18.23 12,18.23C9.78,18.23 8,16.45 8,14.23C8,13.07 9.11,10.83 10.32,9.07C11.16,7.85 11.56,7.42 12,6.9Z',\n },\n sun: {\n id: 'white-balance-sunny',\n path: 'M3.55,18.54L4.96,19.95L6.76,18.16L5.34,16.74M11,22.45C11.32,22.45 13,22.45 13,22.45V19.5H11M12,5.5A6,6 0 0,0 6,11.5A6,6 0 0,0 12,17.5A6,6 0 0,0 18,11.5C18,8.18 15.31,5.5 12,5.5M20,12.5H23V10.5H20M17.24,18.16L19.04,19.95L20.45,18.54L18.66,16.74M20.45,4.46L19.04,3.05L17.24,4.84L18.66,6.26M13,0.55H11V3.5H13M4,10.5H1V12.5H4M6.76,4.84L4.96,3.05L3.55,4.46L5.34,6.26L6.76,4.84Z',\n },\n book: {\n id: 'book-open-page-variant-outline',\n path: 'M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z',\n },\n briefcase: {\n id: 'briefcase-outline',\n path: 'M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z',\n },\n gift: {\n id: 'gift-outline',\n path: 'M22,12V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V12A1,1 0 0,1 1,11V8A2,2 0 0,1 3,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H21A2,2 0 0,1 23,8V11A1,1 0 0,1 22,12M4,20H11V12H4V20M20,20V12H13V20H20M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M3,8V10H11V8H3M13,8V10H21V8H13Z',\n },\n music: {\n id: 'music-note-outline',\n path: 'M12 3V13.55A4 4 0 1 0 14 17V7H18V3M10 19A2 2 0 1 1 12 17A2 2 0 0 1 10 19Z',\n },\n plant: {\n id: 'sprout-outline',\n path: 'M23,4.1V2.3L21.2,2.1C21.1,2.1 20.5,2 19.5,2C15.4,2 12.4,3.2 10.7,5.3C9.4,4.5 7.6,4 5.5,4C4.5,4 3.8,4.1 3.8,4.1L1.9,4.4L2,6.1C2.1,9.1 3.6,14.8 8.8,14.8C8.9,14.8 8.9,14.8 9,14.8V18.2C5.2,18.7 2,20 2,20V22H22V20C22,20 18.8,18.7 15,18.2V15C21.3,14.9 23,7.8 23,4.1M12,18C11.7,18 11.3,18 11,18V12.4C11,12.4 10.8,9 8,9C8,9 9.5,9.8 9.9,12.7C9.5,12.8 9.1,12.8 8.8,12.8C4.2,12.8 4,6.1 4,6.1C4,6.1 4.6,6 5.5,6C7.4,6 10.5,6.4 11.4,9.1C11.9,4.6 17,4 19.5,4C20.4,4 21,4.1 21,4.1C21,4.1 21,13.1 14.7,13.1C14.5,13.1 14.2,13.1 14,13.1C14,11.1 16,8.1 16,8.1C13,9.1 13,13 13,13V18C12.7,18 12.3,18 12,18Z',\n },\n medical: {\n id: 'medical-bag',\n path: 'M10,3L8,5V7H5C3.85,7 3.12,8 3,9L2,19C1.88,20 2.54,21 4,21H20C21.46,21 22.12,20 22,19L21,9C20.88,8 20.06,7 19,7H16V5L14,3H10M10,5H14V7H10V5M11,10H13V13H16V15H13V18H11V15H8V13H11V10Z',\n },\n recycle: {\n id: 'recycle',\n path: 'M21.82,15.42L19.32,19.75C18.83,20.61 17.92,21.06 17,21H15V23L12.5,18.5L15,14V16H17.82L15.6,12.15L19.93,9.65L21.73,12.77C22.25,13.54 22.32,14.57 21.82,15.42M9.21,3.06H14.21C15.19,3.06 16.04,3.63 16.45,4.45L17.45,6.19L19.18,5.19L16.54,9.6L11.39,9.69L13.12,8.69L11.71,6.24L9.5,10.09L5.16,7.59L6.96,4.47C7.37,3.64 8.22,3.06 9.21,3.06M5.05,19.76L2.55,15.43C2.06,14.58 2.13,13.56 2.64,12.79L3.64,11.06L1.91,10.06L7.05,10.14L9.7,14.56L7.97,13.56L6.56,16H11V21H7.4C6.47,21.07 5.55,20.61 5.05,19.76Z',\n },\n globe: {\n id: 'earth',\n path: 'M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z',\n },\n idea: {\n id: 'lightbulb-on-outline',\n path: 'M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z',\n },\n quote: {\n id: 'format-quote-open',\n path: 'M10,7L8,11H11V17H5V11L7,7H10M18,7L16,11H19V17H13V11L15,7H18Z',\n },\n} as const satisfies Record<string, AiIconLibraryEntry>\n\n/** A word the layout language may name an icon by. */\nexport type AiIconWord = keyof typeof AI_ICON_LIBRARY\n\n/** Every icon word, in the order the schema lists them. */\nexport const AI_ICON_WORDS = Object.keys(AI_ICON_LIBRARY) as AiIconWord[]\n\nconst BY_ID: ReadonlyMap<string, AiIconLibraryEntry> = new Map(\n Object.values(AI_ICON_LIBRARY).map((entry) => [entry.id, entry]),\n)\n\n/** The library entry an icon word names; `null` for any other value. */\nexport function aiIconOfWord(word: unknown): AiIconLibraryEntry | null {\n return typeof word === 'string' &&\n Object.prototype.hasOwnProperty.call(AI_ICON_LIBRARY, word)\n ? AI_ICON_LIBRARY[word as AiIconWord]\n : null\n}\n\n/** The library entry a stored icon id names; `null` for an id the library does not hold. */\nexport function aiIconOfId(id: unknown): AiIconLibraryEntry | null {\n return typeof id === 'string' ? (BY_ID.get(id) ?? null) : null\n}\n"],"names":["AI_ICON_COMPONENT_ID","AI_ICON_LIBRARY","check","id","path","star","heart","clock","calendar","location","shield","leaf","home","tools","paw","tooth","coffee","yoga","law","roof","people","chat","fast","sparkle","care","award","truck","learn","camera","design","spa","fitness","phone","mail","money","chart","lock","smile","car","food","cake","cut","brush","hammer","water","sun","book","briefcase","gift","music","plant","medical","recycle","globe","idea","quote","AI_ICON_WORDS","Object","keys","BY_ID","Map","values","map","entry","aiIconOfWord","word","prototype","hasOwnProperty","call","aiIconOfId","get"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;CAcC,GAED,+CAA+C,GAC/C,OAAO,MAAMA,uBAAuB,OAAM;AAS1C,OAAO,MAAMC,kBAAkB;IAC7BC,OAAO;QACLC,IAAI;QACJC,MAAM;IACR;IACAC,MAAM;QACJF,IAAI;QACJC,MAAM;IACR;IACAE,OAAO;QACLH,IAAI;QACJC,MAAM;IACR;IACAG,OAAO;QACLJ,IAAI;QACJC,MAAM;IACR;IACAI,UAAU;QACRL,IAAI;QACJC,MAAM;IACR;IACAK,UAAU;QACRN,IAAI;QACJC,MAAM;IACR;IACAM,QAAQ;QACNP,IAAI;QACJC,MAAM;IACR;IACAO,MAAM;QACJR,IAAI;QACJC,MAAM;IACR;IACAQ,MAAM;QACJT,IAAI;QACJC,MAAM;IACR;IACAS,OAAO;QACLV,IAAI;QACJC,MAAM;IACR;IACAU,KAAK;QACHX,IAAI;QACJC,MAAM;IACR;IACAW,OAAO;QACLZ,IAAI;QACJC,MAAM;IACR;IACAY,QAAQ;QACNb,IAAI;QACJC,MAAM;IACR;IACAa,MAAM;QACJd,IAAI;QACJC,MAAM;IACR;IACAc,KAAK;QACHf,IAAI;QACJC,MAAM;IACR;IACAe,MAAM;QACJhB,IAAI;QACJC,MAAM;IACR;IACAgB,QAAQ;QACNjB,IAAI;QACJC,MAAM;IACR;IACAiB,MAAM;QACJlB,IAAI;QACJC,MAAM;IACR;IACAkB,MAAM;QACJnB,IAAI;QACJC,MAAM;IACR;IACAmB,SAAS;QACPpB,IAAI;QACJC,MAAM;IACR;IACAoB,MAAM;QACJrB,IAAI;QACJC,MAAM;IACR;IACAqB,OAAO;QACLtB,IAAI;QACJC,MAAM;IACR;IACAsB,OAAO;QACLvB,IAAI;QACJC,MAAM;IACR;IACAuB,OAAO;QACLxB,IAAI;QACJC,MAAM;IACR;IACAwB,QAAQ;QACNzB,IAAI;QACJC,MAAM;IACR;IACAyB,QAAQ;QACN1B,IAAI;QACJC,MAAM;IACR;IACA0B,KAAK;QACH3B,IAAI;QACJC,MAAM;IACR;IACA2B,SAAS;QACP5B,IAAI;QACJC,MAAM;IACR;IACA4B,OAAO;QACL7B,IAAI;QACJC,MAAM;IACR;IACA6B,MAAM;QACJ9B,IAAI;QACJC,MAAM;IACR;IACA8B,OAAO;QACL/B,IAAI;QACJC,MAAM;IACR;IACA+B,OAAO;QACLhC,IAAI;QACJC,MAAM;IACR;IACAgC,MAAM;QACJjC,IAAI;QACJC,MAAM;IACR;IACAiC,OAAO;QACLlC,IAAI;QACJC,MAAM;IACR;IACAkC,KAAK;QACHnC,IAAI;QACJC,MAAM;IACR;IACAmC,MAAM;QACJpC,IAAI;QACJC,MAAM;IACR;IACAoC,MAAM;QACJrC,IAAI;QACJC,MAAM;IACR;IACAqC,KAAK;QACHtC,IAAI;QACJC,MAAM;IACR;IACAsC,OAAO;QACLvC,IAAI;QACJC,MAAM;IACR;IACAuC,QAAQ;QACNxC,IAAI;QACJC,MAAM;IACR;IACAwC,OAAO;QACLzC,IAAI;QACJC,MAAM;IACR;IACAyC,KAAK;QACH1C,IAAI;QACJC,MAAM;IACR;IACA0C,MAAM;QACJ3C,IAAI;QACJC,MAAM;IACR;IACA2C,WAAW;QACT5C,IAAI;QACJC,MAAM;IACR;IACA4C,MAAM;QACJ7C,IAAI;QACJC,MAAM;IACR;IACA6C,OAAO;QACL9C,IAAI;QACJC,MAAM;IACR;IACA8C,OAAO;QACL/C,IAAI;QACJC,MAAM;IACR;IACA+C,SAAS;QACPhD,IAAI;QACJC,MAAM;IACR;IACAgD,SAAS;QACPjD,IAAI;QACJC,MAAM;IACR;IACAiD,OAAO;QACLlD,IAAI;QACJC,MAAM;IACR;IACAkD,MAAM;QACJnD,IAAI;QACJC,MAAM;IACR;IACAmD,OAAO;QACLpD,IAAI;QACJC,MAAM;IACR;AACF,EAAuD;AAKvD,yDAAyD,GACzD,OAAO,MAAMoD,gBAAgBC,OAAOC,IAAI,CAACzD,iBAAgC;AAEzE,MAAM0D,QAAiD,IAAIC,IACzDH,OAAOI,MAAM,CAAC5D,iBAAiB6D,GAAG,CAAC,CAACC,QAAU;QAACA,MAAM5D,EAAE;QAAE4D;KAAM;AAGjE,sEAAsE,GACtE,OAAO,SAASC,aAAaC,IAAa;IACxC,OAAO,OAAOA,SAAS,YACrBR,OAAOS,SAAS,CAACC,cAAc,CAACC,IAAI,CAACnE,iBAAiBgE,QACpDhE,eAAe,CAACgE,KAAmB,GACnC;AACN;AAEA,0FAA0F,GAC1F,OAAO,SAASI,WAAWlE,EAAW;QACHwD;IAAjC,OAAO,OAAOxD,OAAO,YAAYwD,aAAAA,MAAMW,GAAG,CAACnE,eAAVwD,aAAiB,OAAQ;AAC5D"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-icon-library.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The icons a page built by the platform's own code may draw (AGL-3660).\n *\n * An icon element draws the path its picker stores beside the id, which a\n * model can never supply, so a tree a model writes keeps no icon\n * (`ai-node-tree.ts` drops the id). The layout language compiler is code: the\n * model names one of these by its word, and the compiler writes the Material\n * Design Icons id and the path copied here, exactly as the icon picker stores\n * them — so the published page draws it without loading the icon catalog.\n *\n * Paths are Material Design Icons 6.5.95 (Apache-2.0), the same set the\n * picker offers (`@aglyn/shared-data-mdi`). The words are the vocabulary the\n * language's schema enumerates; a word added here is a word the model may\n * write.\n */\n\n/** The palette element an icon is drawn with. */\nexport const AI_ICON_COMPONENT_ID = 'icon'\n\nexport interface AiIconLibraryEntry {\n /** The id the icon picker stores. */\n id: string\n /** The SVG path the picker stores beside it. */\n path: string\n}\n\nexport const AI_ICON_LIBRARY = {\n check: {\n id: 'check-circle-outline',\n path: 'M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z',\n },\n star: {\n id: 'star-outline',\n path: 'M12,15.39L8.24,17.66L9.23,13.38L5.91,10.5L10.29,10.13L12,6.09L13.71,10.13L18.09,10.5L14.77,13.38L15.76,17.66M22,9.24L14.81,8.63L12,2L9.19,8.63L2,9.24L7.45,13.97L5.82,21L12,17.27L18.18,21L16.54,13.97L22,9.24Z',\n },\n heart: {\n id: 'heart-outline',\n path: 'M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z',\n },\n clock: {\n id: 'clock-outline',\n path: 'M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z',\n },\n calendar: {\n id: 'calendar-check-outline',\n path: 'M19,4H18V2H16V4H8V2H6V4H5A2,2 0 0,0 3,6V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V6A2,2 0 0,0 19,4M19,20H5V10H19V20M5,8V6H19V8H5M10.56,18.46L16.5,12.53L15.43,11.47L10.56,16.34L8.45,14.23L7.39,15.29L10.56,18.46Z',\n },\n location: {\n id: 'map-marker-outline',\n path: 'M12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5M12,2A7,7 0 0,1 19,9C19,14.25 12,22 12,22C12,22 5,14.25 5,9A7,7 0 0,1 12,2M12,4A5,5 0 0,0 7,9C7,10 7,12 12,18.71C17,12 17,10 17,9A5,5 0 0,0 12,4Z',\n },\n shield: {\n id: 'shield-check-outline',\n path: 'M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9',\n },\n leaf: {\n id: 'leaf',\n path: 'M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z',\n },\n home: {\n id: 'home-outline',\n path: 'M12 5.69L17 10.19V18H15V12H9V18H7V10.19L12 5.69M12 3L2 12H5V20H11V14H13V20H19V12H22L12 3Z',\n },\n tools: {\n id: 'tools',\n path: 'M21.71 20.29L20.29 21.71A1 1 0 0 1 18.88 21.71L7 9.85A3.81 3.81 0 0 1 6 10A4 4 0 0 1 2.22 4.7L4.76 7.24L5.29 6.71L6.71 5.29L7.24 4.76L4.7 2.22A4 4 0 0 1 10 6A3.81 3.81 0 0 1 9.85 7L21.71 18.88A1 1 0 0 1 21.71 20.29M2.29 18.88A1 1 0 0 0 2.29 20.29L3.71 21.71A1 1 0 0 0 5.12 21.71L10.59 16.25L7.76 13.42M20 2L16 4V6L13.83 8.17L15.83 10.17L18 8H20L22 4Z',\n },\n paw: {\n id: 'paw',\n path: 'M8.35,3C9.53,2.83 10.78,4.12 11.14,5.9C11.5,7.67 10.85,9.25 9.67,9.43C8.5,9.61 7.24,8.32 6.87,6.54C6.5,4.77 7.17,3.19 8.35,3M15.5,3C16.69,3.19 17.35,4.77 17,6.54C16.62,8.32 15.37,9.61 14.19,9.43C13,9.25 12.35,7.67 12.72,5.9C13.08,4.12 14.33,2.83 15.5,3M3,7.6C4.14,7.11 5.69,8 6.5,9.55C7.26,11.13 7,12.79 5.87,13.28C4.74,13.77 3.2,12.89 2.41,11.32C1.62,9.75 1.9,8.08 3,7.6M21,7.6C22.1,8.08 22.38,9.75 21.59,11.32C20.8,12.89 19.26,13.77 18.13,13.28C17,12.79 16.74,11.13 17.5,9.55C18.31,8 19.86,7.11 21,7.6M19.33,18.38C19.37,19.32 18.65,20.36 17.79,20.75C16,21.57 13.88,19.87 11.89,19.87C9.9,19.87 7.76,21.64 6,20.75C5,20.26 4.31,18.96 4.44,17.88C4.62,16.39 6.41,15.59 7.47,14.5C8.88,13.09 9.88,10.44 11.89,10.44C13.89,10.44 14.95,13.05 16.3,14.5C17.41,15.72 19.26,16.75 19.33,18.38Z',\n },\n tooth: {\n id: 'tooth-outline',\n path: 'M7,2C4,2 2,5 2,8C2,10.11 3,13 4,14C5,15 6,22 8,22C12.54,22 10,15 12,15C14,15 11.46,22 16,22C18,22 19,15 20,14C21,13 22,10.11 22,8C22,5 20,2 17,2C14,2 14,3 12,3C10,3 10,2 7,2M7,4C9,4 10,5 12,5C14,5 15,4 17,4C18.67,4 20,6 20,8C20,9.75 19.14,12.11 18.19,13.06C17.33,13.92 16.06,19.94 15.5,19.94C15.29,19.94 15,18.88 15,17.59C15,15.55 14.43,13 12,13C9.57,13 9,15.55 9,17.59C9,18.88 8.71,19.94 8.5,19.94C7.94,19.94 6.67,13.92 5.81,13.06C4.86,12.11 4,9.75 4,8C4,6 5.33,4 7,4Z',\n },\n coffee: {\n id: 'coffee-outline',\n path: 'M2,21V19H20V21H2M20,8V5H18V8H20M20,3A2,2 0 0,1 22,5V8A2,2 0 0,1 20,10H18V13A4,4 0 0,1 14,17H8A4,4 0 0,1 4,13V3H20M16,5H6V13A2,2 0 0,0 8,15H14A2,2 0 0,0 16,13V5Z',\n },\n yoga: {\n id: 'meditation',\n path: 'M12 4C13.11 4 14 4.89 14 6S13.11 8 12 8 10 7.11 10 6 10.9 4 12 4M21 16V14C18.76 14 16.84 13.04 15.4 11.32L14.06 9.72C13.68 9.26 13.12 9 12.53 9H11.5C10.89 9 10.33 9.26 9.95 9.72L8.61 11.32C7.16 13.04 5.24 14 3 14V16C5.77 16 8.19 14.83 10 12.75V15L6.12 16.55C5.45 16.82 5 17.5 5 18.21C5 19.2 5.8 20 6.79 20H9V19.5C9 18.12 10.12 17 11.5 17H14.5C14.78 17 15 17.22 15 17.5S14.78 18 14.5 18H11.5C10.67 18 10 18.67 10 19.5V20H17.21C18.2 20 19 19.2 19 18.21C19 17.5 18.55 16.82 17.88 16.55L14 15V12.75C15.81 14.83 18.23 16 21 16Z',\n },\n law: {\n id: 'scale-balance',\n path: 'M12,3C10.73,3 9.6,3.8 9.18,5H3V7H4.95L2,14C1.53,16 3,17 5.5,17C8,17 9.56,16 9,14L6.05,7H9.17C9.5,7.85 10.15,8.5 11,8.83V20H2V22H22V20H13V8.82C13.85,8.5 14.5,7.85 14.82,7H17.95L15,14C14.53,16 16,17 18.5,17C21,17 22.56,16 22,14L19.05,7H21V5H14.83C14.4,3.8 13.27,3 12,3M12,5A1,1 0 0,1 13,6A1,1 0 0,1 12,7A1,1 0 0,1 11,6A1,1 0 0,1 12,5M5.5,10.25L7,14H4L5.5,10.25M18.5,10.25L20,14H17L18.5,10.25Z',\n },\n roof: {\n id: 'home-roof',\n path: 'M19 16H22L12 7L2 16H5L12 9.69L19 16M7 8.81V7H4V11.5L7 8.81Z',\n },\n people: {\n id: 'account-group-outline',\n path: 'M12,5A3.5,3.5 0 0,0 8.5,8.5A3.5,3.5 0 0,0 12,12A3.5,3.5 0 0,0 15.5,8.5A3.5,3.5 0 0,0 12,5M12,7A1.5,1.5 0 0,1 13.5,8.5A1.5,1.5 0 0,1 12,10A1.5,1.5 0 0,1 10.5,8.5A1.5,1.5 0 0,1 12,7M5.5,8A2.5,2.5 0 0,0 3,10.5C3,11.44 3.53,12.25 4.29,12.68C4.65,12.88 5.06,13 5.5,13C5.94,13 6.35,12.88 6.71,12.68C7.08,12.47 7.39,12.17 7.62,11.81C6.89,10.86 6.5,9.7 6.5,8.5C6.5,8.41 6.5,8.31 6.5,8.22C6.2,8.08 5.86,8 5.5,8M18.5,8C18.14,8 17.8,8.08 17.5,8.22C17.5,8.31 17.5,8.41 17.5,8.5C17.5,9.7 17.11,10.86 16.38,11.81C16.5,12 16.63,12.15 16.78,12.3C16.94,12.45 17.1,12.58 17.29,12.68C17.65,12.88 18.06,13 18.5,13C18.94,13 19.35,12.88 19.71,12.68C20.47,12.25 21,11.44 21,10.5A2.5,2.5 0 0,0 18.5,8M12,14C9.66,14 5,15.17 5,17.5V19H19V17.5C19,15.17 14.34,14 12,14M4.71,14.55C2.78,14.78 0,15.76 0,17.5V19H3V17.07C3,16.06 3.69,15.22 4.71,14.55M19.29,14.55C20.31,15.22 21,16.06 21,17.07V19H24V17.5C24,15.76 21.22,14.78 19.29,14.55M12,16C13.53,16 15.24,16.5 16.23,17H7.77C8.76,16.5 10.47,16 12,16Z',\n },\n chat: {\n id: 'message-text-outline',\n path: 'M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z',\n },\n fast: {\n id: 'lightning-bolt-outline',\n path: 'M11 9.47V11H14.76L13 14.53V13H9.24L11 9.47M13 1L6 15H11V23L18 9H13V1Z',\n },\n sparkle: {\n id: 'creation',\n path: 'M19,1L17.74,3.75L15,5L17.74,6.26L19,9L20.25,6.26L23,5L20.25,3.75M9,4L6.5,9.5L1,12L6.5,14.5L9,20L11.5,14.5L17,12L11.5,9.5M19,15L17.74,17.74L15,19L17.74,20.25L19,23L20.25,20.25L23,19L20.25,17.74',\n },\n care: {\n id: 'hand-heart-outline',\n path: 'M16 3.23C16.71 2.41 17.61 2 18.7 2C19.61 2 20.37 2.33 21 3C21.63 3.67 21.96 4.43 22 5.3C22 6 21.67 6.81 21 7.76S19.68 9.5 19.03 10.15C18.38 10.79 17.37 11.74 16 13C14.61 11.74 13.59 10.79 12.94 10.15S11.63 8.71 10.97 7.76C10.31 6.81 10 6 10 5.3C10 4.39 10.32 3.63 10.97 3C11.62 2.37 12.4 2.04 13.31 2C14.38 2 15.27 2.41 16 3.23M22 19V20L14 22.5L7 20.56V22H1V11H8.97L15.13 13.3C16.25 13.72 17 14.8 17 16H19C20.66 16 22 17.34 22 19M5 20V13H3V20H5M19.9 18.57C19.74 18.24 19.39 18 19 18H13.65C13.11 18 12.58 17.92 12.07 17.75L9.69 16.96L10.32 15.06L12.7 15.85C13 15.95 15 16 15 16C15 15.63 14.77 15.3 14.43 15.17L8.61 13H7V18.5L13.97 20.41L19.9 18.57Z',\n },\n award: {\n id: 'medal-outline',\n path: 'M14.94 19.5L12 17.77L9.06 19.5L9.84 16.16L7.25 13.92L10.66 13.63L12 10.5L13.34 13.63L16.75 13.92L14.16 16.16M20 2H4V4L8.86 7.64A8 8 0 1 0 15.14 7.64L20 4M18 15A6 6 0 1 1 10.82 9.12A5.86 5.86 0 0 1 13.18 9.12A6 6 0 0 1 18 15M12.63 7H11.37L7.37 4H16.71Z',\n },\n truck: {\n id: 'truck-outline',\n path: 'M18 18.5C18.83 18.5 19.5 17.83 19.5 17C19.5 16.17 18.83 15.5 18 15.5C17.17 15.5 16.5 16.17 16.5 17C16.5 17.83 17.17 18.5 18 18.5M19.5 9.5H17V12H21.46L19.5 9.5M6 18.5C6.83 18.5 7.5 17.83 7.5 17C7.5 16.17 6.83 15.5 6 15.5C5.17 15.5 4.5 16.17 4.5 17C4.5 17.83 5.17 18.5 6 18.5M20 8L23 12V17H21C21 18.66 19.66 20 18 20C16.34 20 15 18.66 15 17H9C9 18.66 7.66 20 6 20C4.34 20 3 18.66 3 17H1V6C1 4.89 1.89 4 3 4H17V8H20M3 6V15H3.76C4.31 14.39 5.11 14 6 14C6.89 14 7.69 14.39 8.24 15H15V6H3Z',\n },\n learn: {\n id: 'school-outline',\n path: 'M12 3L1 9L5 11.18V17.18L12 21L19 17.18V11.18L21 10.09V17H23V9L12 3M18.82 9L12 12.72L5.18 9L12 5.28L18.82 9M17 16L12 18.72L7 16V12.27L12 15L17 12.27V16Z',\n },\n camera: {\n id: 'camera-outline',\n path: 'M20,4H16.83L15,2H9L7.17,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6A2,2 0 0,0 20,4M20,18H4V6H8.05L9.88,4H14.12L15.95,6H20V18M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15Z',\n },\n design: {\n id: 'palette-outline',\n path: 'M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z',\n },\n spa: {\n id: 'spa-outline',\n path: 'M15.5,9.63C14.21,10.32 13.03,11.2 12,12.26C10.97,11.19 9.79,10.31 8.5,9.63C8.74,6.86 9.92,4.14 12.06,2C14.18,4.12 15.31,6.84 15.5,9.63M12,15.45C14.15,12.17 17.82,10 22,10C22,20 12.68,21.88 12,22C11.32,21.89 2,20 2,10C6.18,10 9.85,12.17 12,15.45M12.05,5.19C11.39,6.23 10.93,7.38 10.68,8.58L12,9.55L13.35,8.57C13.12,7.37 12.68,6.22 12.05,5.19M12,19.97C12,19.97 18,19 19.74,12.25C14,14 12,19.1 12,19.1C12,19.1 9,13 4.26,12.26C6,19 12,19.97 12,19.97Z',\n },\n fitness: {\n id: 'dumbbell',\n path: 'M20.57,14.86L22,13.43L20.57,12L17,15.57L8.43,7L12,3.43L10.57,2L9.14,3.43L7.71,2L5.57,4.14L4.14,2.71L2.71,4.14L4.14,5.57L2,7.71L3.43,9.14L2,10.57L3.43,12L7,8.43L15.57,17L12,20.57L13.43,22L14.86,20.57L16.29,22L18.43,19.86L19.86,21.29L21.29,19.86L19.86,18.43L22,16.29L20.57,14.86Z',\n },\n phone: {\n id: 'phone-outline',\n path: 'M20,15.5C18.8,15.5 17.5,15.3 16.4,14.9C16.3,14.9 16.2,14.9 16.1,14.9C15.8,14.9 15.6,15 15.4,15.2L13.2,17.4C10.4,15.9 8,13.6 6.6,10.8L8.8,8.6C9.1,8.3 9.2,7.9 9,7.6C8.7,6.5 8.5,5.2 8.5,4C8.5,3.5 8,3 7.5,3H4C3.5,3 3,3.5 3,4C3,13.4 10.6,21 20,21C20.5,21 21,20.5 21,20V16.5C21,16 20.5,15.5 20,15.5M5,5H6.5C6.6,5.9 6.8,6.8 7,7.6L5.8,8.8C5.4,7.6 5.1,6.3 5,5M19,19C17.7,18.9 16.4,18.6 15.2,18.2L16.4,17C17.2,17.2 18.1,17.4 19,17.4V19Z',\n },\n mail: {\n id: 'email-outline',\n path: 'M22 6C22 4.9 21.1 4 20 4H4C2.9 4 2 4.9 2 6V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V6M20 6L12 11L4 6H20M20 18H4V8L12 13L20 8V18Z',\n },\n money: {\n id: 'cash-multiple',\n path: 'M5,6H23V18H5V6M14,9A3,3 0 0,1 17,12A3,3 0 0,1 14,15A3,3 0 0,1 11,12A3,3 0 0,1 14,9M9,8A2,2 0 0,1 7,10V14A2,2 0 0,1 9,16H19A2,2 0 0,1 21,14V10A2,2 0 0,1 19,8H9M1,10H3V20H19V22H1V10Z',\n },\n chart: {\n id: 'chart-line',\n path: 'M16,11.78L20.24,4.45L21.97,5.45L16.74,14.5L10.23,10.75L5.46,19H22V21H2V3H4V17.54L9.5,8L16,11.78Z',\n },\n lock: {\n id: 'lock-outline',\n path: 'M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z',\n },\n smile: {\n id: 'emoticon-happy-outline',\n path: 'M20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M10,9.5C10,10.3 9.3,11 8.5,11C7.7,11 7,10.3 7,9.5C7,8.7 7.7,8 8.5,8C9.3,8 10,8.7 10,9.5M17,9.5C17,10.3 16.3,11 15.5,11C14.7,11 14,10.3 14,9.5C14,8.7 14.7,8 15.5,8C16.3,8 17,8.7 17,9.5M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z',\n },\n car: {\n id: 'car-outline',\n path: 'M18.9 6C18.7 5.4 18.1 5 17.5 5H6.5C5.8 5 5.3 5.4 5.1 6L3 12V20C3 20.5 3.5 21 4 21H5C5.6 21 6 20.5 6 20V19H18V20C18 20.5 18.5 21 19 21H20C20.5 21 21 20.5 21 20V12L18.9 6M6.8 7H17.1L18.2 10H5.8L6.8 7M19 17H5V12H19V17M7.5 13C8.3 13 9 13.7 9 14.5S8.3 16 7.5 16 6 15.3 6 14.5 6.7 13 7.5 13M16.5 13C17.3 13 18 13.7 18 14.5S17.3 16 16.5 16C15.7 16 15 15.3 15 14.5S15.7 13 16.5 13Z',\n },\n food: {\n id: 'silverware-fork-knife',\n path: 'M11,9H9V2H7V9H5V2H3V9C3,11.12 4.66,12.84 6.75,12.97V22H9.25V12.97C11.34,12.84 13,11.12 13,9V2H11V9M16,6V14H18.5V22H21V2C18.24,2 16,4.24 16,6Z',\n },\n cake: {\n id: 'cake-variant-outline',\n path: 'M12 6C13.11 6 14 5.1 14 4C14 3.62 13.9 3.27 13.71 2.97L12 0L10.29 2.97C10.1 3.27 10 3.62 10 4C10 5.1 10.9 6 12 6M18 9H13V7H11V9H6C4.34 9 3 10.34 3 12V21C3 21.55 3.45 22 4 22H20C20.55 22 21 21.55 21 21V12C21 10.34 19.66 9 18 9M19 20H5V17C5.9 17 6.76 16.63 7.4 16L8.5 14.92L9.56 16C10.87 17.3 13.15 17.29 14.45 16L15.53 14.92L16.6 16C17.24 16.63 18.1 17 19 17V20M19 15.5C18.5 15.5 18 15.3 17.65 14.93L15.5 12.8L13.38 14.93C12.64 15.67 11.35 15.67 10.61 14.93L8.5 12.8L6.34 14.93C6 15.29 5.5 15.5 5 15.5V12C5 11.45 5.45 11 6 11H18C18.55 11 19 11.45 19 12V15.5Z',\n },\n cut: {\n id: 'content-cut',\n path: 'M19,3L13,9L15,11L22,4V3M12,12.5A0.5,0.5 0 0,1 11.5,12A0.5,0.5 0 0,1 12,11.5A0.5,0.5 0 0,1 12.5,12A0.5,0.5 0 0,1 12,12.5M6,20A2,2 0 0,1 4,18C4,16.89 4.9,16 6,16A2,2 0 0,1 8,18C8,19.11 7.1,20 6,20M6,8A2,2 0 0,1 4,6C4,4.89 4.9,4 6,4A2,2 0 0,1 8,6C8,7.11 7.1,8 6,8M9.64,7.64C9.87,7.14 10,6.59 10,6A4,4 0 0,0 6,2A4,4 0 0,0 2,6A4,4 0 0,0 6,10C6.59,10 7.14,9.87 7.64,9.64L10,12L7.64,14.36C7.14,14.13 6.59,14 6,14A4,4 0 0,0 2,18A4,4 0 0,0 6,22A4,4 0 0,0 10,18C10,17.41 9.87,16.86 9.64,16.36L12,14L19,21H22V20L9.64,7.64Z',\n },\n brush: {\n id: 'brush-variant',\n path: 'M8 3C5.79 3 4 4.79 4 7V14C4 15.1 4.9 16 6 16H9V20C9 21.1 9.9 22 11 22H13C14.1 22 15 21.1 15 20V16H18C19.1 16 20 15.1 20 14V3H8M8 5H12V7H14V5H15V9H17V5H18V10H6V7C6 5.9 6.9 5 8 5M6 14V12H18V14H6Z',\n },\n hammer: {\n id: 'hammer-wrench',\n path: 'M13.78 15.3L19.78 21.3L21.89 19.14L15.89 13.14L13.78 15.3M17.5 10.1C17.11 10.1 16.69 10.05 16.36 9.91L4.97 21.25L2.86 19.14L10.27 11.74L8.5 9.96L7.78 10.66L6.33 9.25V12.11L5.63 12.81L2.11 9.25L2.81 8.55H5.62L4.22 7.14L7.78 3.58C8.95 2.41 10.83 2.41 12 3.58L9.89 5.74L11.3 7.14L10.59 7.85L12.38 9.63L14.2 7.75C14.06 7.42 14 7 14 6.63C14 4.66 15.56 3.11 17.5 3.11C18.09 3.11 18.61 3.25 19.08 3.53L16.41 6.2L17.91 7.7L20.58 5.03C20.86 5.5 21 6 21 6.63C21 8.55 19.45 10.1 17.5 10.1Z',\n },\n water: {\n id: 'water-outline',\n path: 'M12,3.77L11.25,4.61C11.25,4.61 9.97,6.06 8.68,7.94C7.39,9.82 6,12.07 6,14.23A6,6 0 0,0 12,20.23A6,6 0 0,0 18,14.23C18,12.07 16.61,9.82 15.32,7.94C14.03,6.06 12.75,4.61 12.75,4.61L12,3.77M12,6.9C12.44,7.42 12.84,7.85 13.68,9.07C14.89,10.83 16,13.07 16,14.23C16,16.45 14.22,18.23 12,18.23C9.78,18.23 8,16.45 8,14.23C8,13.07 9.11,10.83 10.32,9.07C11.16,7.85 11.56,7.42 12,6.9Z',\n },\n sun: {\n id: 'white-balance-sunny',\n path: 'M3.55,18.54L4.96,19.95L6.76,18.16L5.34,16.74M11,22.45C11.32,22.45 13,22.45 13,22.45V19.5H11M12,5.5A6,6 0 0,0 6,11.5A6,6 0 0,0 12,17.5A6,6 0 0,0 18,11.5C18,8.18 15.31,5.5 12,5.5M20,12.5H23V10.5H20M17.24,18.16L19.04,19.95L20.45,18.54L18.66,16.74M20.45,4.46L19.04,3.05L17.24,4.84L18.66,6.26M13,0.55H11V3.5H13M4,10.5H1V12.5H4M6.76,4.84L4.96,3.05L3.55,4.46L5.34,6.26L6.76,4.84Z',\n },\n book: {\n id: 'book-open-page-variant-outline',\n path: 'M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z',\n },\n briefcase: {\n id: 'briefcase-outline',\n path: 'M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z',\n },\n gift: {\n id: 'gift-outline',\n path: 'M22,12V20A2,2 0 0,1 20,22H4A2,2 0 0,1 2,20V12A1,1 0 0,1 1,11V8A2,2 0 0,1 3,6H6.17C6.06,5.69 6,5.35 6,5A3,3 0 0,1 9,2C10,2 10.88,2.5 11.43,3.24V3.23L12,4L12.57,3.23V3.24C13.12,2.5 14,2 15,2A3,3 0 0,1 18,5C18,5.35 17.94,5.69 17.83,6H21A2,2 0 0,1 23,8V11A1,1 0 0,1 22,12M4,20H11V12H4V20M20,20V12H13V20H20M9,4A1,1 0 0,0 8,5A1,1 0 0,0 9,6A1,1 0 0,0 10,5A1,1 0 0,0 9,4M15,4A1,1 0 0,0 14,5A1,1 0 0,0 15,6A1,1 0 0,0 16,5A1,1 0 0,0 15,4M3,8V10H11V8H3M13,8V10H21V8H13Z',\n },\n music: {\n id: 'music-note-outline',\n path: 'M12 3V13.55A4 4 0 1 0 14 17V7H18V3M10 19A2 2 0 1 1 12 17A2 2 0 0 1 10 19Z',\n },\n plant: {\n id: 'sprout-outline',\n path: 'M23,4.1V2.3L21.2,2.1C21.1,2.1 20.5,2 19.5,2C15.4,2 12.4,3.2 10.7,5.3C9.4,4.5 7.6,4 5.5,4C4.5,4 3.8,4.1 3.8,4.1L1.9,4.4L2,6.1C2.1,9.1 3.6,14.8 8.8,14.8C8.9,14.8 8.9,14.8 9,14.8V18.2C5.2,18.7 2,20 2,20V22H22V20C22,20 18.8,18.7 15,18.2V15C21.3,14.9 23,7.8 23,4.1M12,18C11.7,18 11.3,18 11,18V12.4C11,12.4 10.8,9 8,9C8,9 9.5,9.8 9.9,12.7C9.5,12.8 9.1,12.8 8.8,12.8C4.2,12.8 4,6.1 4,6.1C4,6.1 4.6,6 5.5,6C7.4,6 10.5,6.4 11.4,9.1C11.9,4.6 17,4 19.5,4C20.4,4 21,4.1 21,4.1C21,4.1 21,13.1 14.7,13.1C14.5,13.1 14.2,13.1 14,13.1C14,11.1 16,8.1 16,8.1C13,9.1 13,13 13,13V18C12.7,18 12.3,18 12,18Z',\n },\n medical: {\n id: 'medical-bag',\n path: 'M10,3L8,5V7H5C3.85,7 3.12,8 3,9L2,19C1.88,20 2.54,21 4,21H20C21.46,21 22.12,20 22,19L21,9C20.88,8 20.06,7 19,7H16V5L14,3H10M10,5H14V7H10V5M11,10H13V13H16V15H13V18H11V15H8V13H11V10Z',\n },\n recycle: {\n id: 'recycle',\n path: 'M21.82,15.42L19.32,19.75C18.83,20.61 17.92,21.06 17,21H15V23L12.5,18.5L15,14V16H17.82L15.6,12.15L19.93,9.65L21.73,12.77C22.25,13.54 22.32,14.57 21.82,15.42M9.21,3.06H14.21C15.19,3.06 16.04,3.63 16.45,4.45L17.45,6.19L19.18,5.19L16.54,9.6L11.39,9.69L13.12,8.69L11.71,6.24L9.5,10.09L5.16,7.59L6.96,4.47C7.37,3.64 8.22,3.06 9.21,3.06M5.05,19.76L2.55,15.43C2.06,14.58 2.13,13.56 2.64,12.79L3.64,11.06L1.91,10.06L7.05,10.14L9.7,14.56L7.97,13.56L6.56,16H11V21H7.4C6.47,21.07 5.55,20.61 5.05,19.76Z',\n },\n globe: {\n id: 'earth',\n path: 'M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z',\n },\n idea: {\n id: 'lightbulb-on-outline',\n path: 'M20,11H23V13H20V11M1,11H4V13H1V11M13,1V4H11V1H13M4.92,3.5L7.05,5.64L5.63,7.05L3.5,4.93L4.92,3.5M16.95,5.63L19.07,3.5L20.5,4.93L18.37,7.05L16.95,5.63M12,6A6,6 0 0,1 18,12C18,14.22 16.79,16.16 15,17.2V19A1,1 0 0,1 14,20H10A1,1 0 0,1 9,19V17.2C7.21,16.16 6,14.22 6,12A6,6 0 0,1 12,6M14,21V22A1,1 0 0,1 13,23H11A1,1 0 0,1 10,22V21H14M11,18H13V15.87C14.73,15.43 16,13.86 16,12A4,4 0 0,0 12,8A4,4 0 0,0 8,12C8,13.86 9.27,15.43 11,15.87V18Z',\n },\n quote: {\n id: 'format-quote-open',\n path: 'M10,7L8,11H11V17H5V11L7,7H10M18,7L16,11H19V17H13V11L15,7H18Z',\n },\n} as const satisfies Record<string, AiIconLibraryEntry>\n\n/**\n * Icons the compiler draws from what an item says (AGL-4065) — the languages\n * a business works in, a race, a sponsor or a partner, a price or an\n * estimate — that the language's schema does not list for the model: a word\n * added to the schema is a word in every cached page and frame prompt, and\n * these are the compiler's to pick (`aiStorefrontItemIcons`). A stored id of\n * one is read back like any other (`aiIconOfId`).\n */\nexport const AI_ICON_DRAWN = {\n translate: {\n id: 'translate',\n path: 'M12.87,15.07L10.33,12.56L10.36,12.53C12.1,10.59 13.34,8.36 14.07,6H17V4H10V2H8V4H1V6H12.17C11.5,7.92 10.44,9.75 9,11.35C8.07,10.32 7.3,9.19 6.69,8H4.69C5.42,9.63 6.42,11.17 7.67,12.56L2.58,17.58L4,19L9,14L12.11,17.11L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.88,17L17.5,12.67L19.12,17H15.88Z',\n },\n flag: {\n id: 'flag-checkered',\n path: 'M14.4,6H20V16H13L12.6,14H7V21H5V4H14L14.4,6M14,14H16V12H18V10H16V8H14V10L13,8V6H11V8H9V6H7V8H9V10H7V12H9V10H11V12H13V10L14,12V14M11,10V8H13V10H11M14,10H16V12H14V10Z',\n },\n handshake: {\n id: 'handshake-outline',\n path: 'M21.71 8.71C22.96 7.46 22.39 6 21.71 5.29L18.71 2.29C17.45 1.04 16 1.61 15.29 2.29L13.59 4H11C9.1 4 8 5 7.44 6.15L3 10.59V14.59L2.29 15.29C1.04 16.55 1.61 18 2.29 18.71L5.29 21.71C5.83 22.25 6.41 22.45 6.96 22.45C7.67 22.45 8.32 22.1 8.71 21.71L11.41 19H15C16.7 19 17.56 17.94 17.87 16.9C19 16.6 19.62 15.74 19.87 14.9C21.42 14.5 22 13.03 22 12V9H21.41L21.71 8.71M20 12C20 12.45 19.81 13 19 13L18 13L18 14C18 14.45 17.81 15 17 15L16 15L16 16C16 16.45 15.81 17 15 17H10.59L7.31 20.28C7 20.57 6.82 20.4 6.71 20.29L3.72 17.31C3.43 17 3.6 16.82 3.71 16.71L5 15.41V11.41L7 9.41V11C7 12.21 7.8 14 10 14S13 12.21 13 11H20V12M20.29 7.29L18.59 9H11V11C11 11.45 10.81 12 10 12S9 11.45 9 11V8C9 7.54 9.17 6 11 6H14.41L16.69 3.72C17 3.43 17.18 3.6 17.29 3.71L20.28 6.69C20.57 7 20.4 7.18 20.29 7.29Z',\n },\n receipt: {\n id: 'receipt',\n path: 'M3,22L4.5,20.5L6,22L7.5,20.5L9,22L10.5,20.5L12,22L13.5,20.5L15,22L16.5,20.5L18,22L19.5,20.5L21,22V2L19.5,3.5L18,2L16.5,3.5L15,2L13.5,3.5L12,2L10.5,3.5L9,2L7.5,3.5L6,2L4.5,3.5L3,2M18,9H6V7H18M18,13H6V11H18M18,17H6V15H18V17Z',\n },\n} as const satisfies Record<string, AiIconLibraryEntry>\n\n/** A word the layout language may name an icon by. */\nexport type AiIconWord = keyof typeof AI_ICON_LIBRARY\n\n/** Every icon word, in the order the schema lists them. */\nexport const AI_ICON_WORDS = Object.keys(AI_ICON_LIBRARY) as AiIconWord[]\n\n/** Every icon the platform's code draws: the schema's words and the compiler's own. */\nconst ALL_ICONS: Readonly<Record<string, AiIconLibraryEntry>> = { ...AI_ICON_LIBRARY, ...AI_ICON_DRAWN }\n\nconst BY_ID: ReadonlyMap<string, AiIconLibraryEntry> = new Map(\n Object.values(ALL_ICONS).map((entry) => [entry.id, entry]),\n)\n\n/** The library entry an icon word names, the schema's or the compiler's own; `null` for any other value. */\nexport function aiIconOfWord(word: unknown): AiIconLibraryEntry | null {\n return typeof word === 'string' &&\n Object.prototype.hasOwnProperty.call(ALL_ICONS, word)\n ? (ALL_ICONS[word] as AiIconLibraryEntry)\n : null\n}\n\n/** The library entry a stored icon id names; `null` for an id the library does not hold. */\nexport function aiIconOfId(id: unknown): AiIconLibraryEntry | null {\n return typeof id === 'string' ? (BY_ID.get(id) ?? null) : null\n}\n"],"names":["AI_ICON_COMPONENT_ID","AI_ICON_LIBRARY","check","id","path","star","heart","clock","calendar","location","shield","leaf","home","tools","paw","tooth","coffee","yoga","law","roof","people","chat","fast","sparkle","care","award","truck","learn","camera","design","spa","fitness","phone","mail","money","chart","lock","smile","car","food","cake","cut","brush","hammer","water","sun","book","briefcase","gift","music","plant","medical","recycle","globe","idea","quote","AI_ICON_DRAWN","translate","flag","handshake","receipt","AI_ICON_WORDS","Object","keys","ALL_ICONS","BY_ID","Map","values","map","entry","aiIconOfWord","word","prototype","hasOwnProperty","call","aiIconOfId","get"],"mappings":"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"}
|
|
@@ -18,7 +18,7 @@ import { type AiCatalogProduct, type AiCategoriesProposal, type AiProductCategor
|
|
|
18
18
|
import type { AiStepKind } from '../providers/catalog';
|
|
19
19
|
import type { AiImagePart, AiProvider } from '../providers/contract';
|
|
20
20
|
import { type AiPluginSettings } from '../providers/routing';
|
|
21
|
-
import { type AiValidatedGeneration } from './ai-doctrine';
|
|
21
|
+
import { type AiGenerationCheckResult, type AiValidatedGeneration } from './ai-doctrine';
|
|
22
22
|
import type { AiSystemBlock } from './ai-runtime';
|
|
23
23
|
/**
|
|
24
24
|
* COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each
|
|
@@ -90,6 +90,16 @@ export interface GenerateAiCatalogInput extends AiProductsGenerationBase {
|
|
|
90
90
|
}
|
|
91
91
|
/** The user turn for a catalog: the store and the brief. */
|
|
92
92
|
export declare function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string;
|
|
93
|
+
/**
|
|
94
|
+
* A catalog's check (AGL-4065): the tool's, and on the FIRST answer only, a
|
|
95
|
+
* product named for a kind of product, a make or a model the brief does not
|
|
96
|
+
* give ({@link aiCatalogNameProblem}), which the re-ask names. A second answer
|
|
97
|
+
* is kept as the tool's check holds it; the products step leaves out any
|
|
98
|
+
* product it still names so (`aiCatalogProductsNamed`).
|
|
99
|
+
*/
|
|
100
|
+
export declare function aiCatalogCheck(brief: string): (answer: Record<string, unknown>) => AiGenerationCheckResult<AiCatalogProduct[]>;
|
|
101
|
+
/** A catalog's products less any its re-ask still named for a kind, a make or a model (AGL-4065). Pure. */
|
|
102
|
+
export declare function aiCatalogProductsNamed(products: readonly AiCatalogProduct[], brief: string): AiCatalogProduct[];
|
|
93
103
|
/** Propose a store's first products from a brief. It writes nothing. */
|
|
94
104
|
export declare function generateAiCatalog(input: GenerateAiCatalogInput & {
|
|
95
105
|
maxTokens: number;
|
|
@@ -14,10 +14,11 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { AI_CATALOG_OPTION_VALUES_MAX, AI_PRODUCT_CATEGORY_LIST_MAX, aiProductMerchantWords } from "../model/ai-products.js";
|
|
17
|
+
*/ import { AI_CATALOG_NAME_CODE, AI_CATALOG_NAME_REASK, AI_CATALOG_OPTION_VALUES_MAX, AI_PRODUCT_CATEGORY_LIST_MAX, aiCatalogNameProblem, aiProductMerchantWords } from "../model/ai-products.js";
|
|
18
18
|
import { AI_ROUTING_TABLE } from "../providers/routing.js";
|
|
19
19
|
import { AI_CATALOG_TOOL, AI_CATALOG_TOOL_NAME, AI_CATEGORIES_TOOL, AI_CATEGORIES_TOOL_NAME, AI_PRODUCT_COPY_TOOL, AI_PRODUCT_COPY_TOOL_NAME, checkAiCatalog, checkAiCategories, checkAiProductCopy } from "../tools/ai-products-tool.js";
|
|
20
20
|
import { runValidatedGeneration } from "./ai-doctrine.js";
|
|
21
|
+
import { AI_GENERATION_MAX_ATTEMPTS } from "./ai-generation-bounds.js";
|
|
21
22
|
/**
|
|
22
23
|
* COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each
|
|
23
24
|
* through the building doctrine's generation call (`runValidatedGeneration`):
|
|
@@ -75,6 +76,8 @@ const STOREFRONT_RULES = [
|
|
|
75
76
|
{
|
|
76
77
|
text: 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\n\n' + `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\n\n` + 'Rules:\n' + [
|
|
77
78
|
'Propose 6 to 12 products that fit the brief, unless the brief says how many.',
|
|
79
|
+
// AGL-4065: a parts shop's brief named kinds of part, and its products were "Riding Gear Item" and "OEM ATV Parts Order".
|
|
80
|
+
'Name each product as one thing a shopper buys. A kind of product ("tires", "riding gear") is not a product: where the brief names only kinds, name a plain item of one of them, never the kind with "Item", "Order" or "Part" after it, and never a brand or a model the brief does not name.',
|
|
78
81
|
'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',
|
|
79
82
|
// The price has its own field (Zach, 2026-10-12); the copy states none.
|
|
80
83
|
'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',
|
|
@@ -180,6 +183,52 @@ const STOREFRONT_RULES = [
|
|
|
180
183
|
input.brief.trim()
|
|
181
184
|
].join('\n');
|
|
182
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* A catalog's check (AGL-4065): the tool's, and on the FIRST answer only, a
|
|
188
|
+
* product named for a kind of product, a make or a model the brief does not
|
|
189
|
+
* give ({@link aiCatalogNameProblem}), which the re-ask names. A second answer
|
|
190
|
+
* is kept as the tool's check holds it; the products step leaves out any
|
|
191
|
+
* product it still names so (`aiCatalogProductsNamed`).
|
|
192
|
+
*/ export function aiCatalogCheck(brief) {
|
|
193
|
+
let answers = 0;
|
|
194
|
+
return (answer)=>{
|
|
195
|
+
var _checked_offending;
|
|
196
|
+
answers += 1;
|
|
197
|
+
const checked = checkAiCatalog(answer, {
|
|
198
|
+
merchantWords: brief
|
|
199
|
+
});
|
|
200
|
+
if (answers >= AI_GENERATION_MAX_ATTEMPTS) return checked;
|
|
201
|
+
const raw = Array.isArray(answer['products']) ? answer['products'] : [];
|
|
202
|
+
const named = raw.flatMap((entry, index)=>{
|
|
203
|
+
const name = typeof (entry == null ? void 0 : entry['name']) === 'string' ? entry['name'] : '';
|
|
204
|
+
return aiCatalogNameProblem(name, brief) ? [
|
|
205
|
+
{
|
|
206
|
+
index,
|
|
207
|
+
name
|
|
208
|
+
}
|
|
209
|
+
] : [];
|
|
210
|
+
});
|
|
211
|
+
if (!named.length) return checked;
|
|
212
|
+
const offending = _extends({}, (_checked_offending = checked.offending) != null ? _checked_offending : {});
|
|
213
|
+
for (const { index, name } of named)offending[`products[${index}].name`] = name;
|
|
214
|
+
return {
|
|
215
|
+
value: null,
|
|
216
|
+
violations: [
|
|
217
|
+
...checked.violations,
|
|
218
|
+
{
|
|
219
|
+
rule: null,
|
|
220
|
+
code: AI_CATALOG_NAME_CODE,
|
|
221
|
+
message: AI_CATALOG_NAME_REASK,
|
|
222
|
+
paths: named.map(({ index })=>`products[${index}].name`)
|
|
223
|
+
}
|
|
224
|
+
],
|
|
225
|
+
offending
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/** A catalog's products less any its re-ask still named for a kind, a make or a model (AGL-4065). Pure. */ export function aiCatalogProductsNamed(products, brief) {
|
|
230
|
+
return products.filter((product)=>!aiCatalogNameProblem(product.name, brief));
|
|
231
|
+
}
|
|
183
232
|
/** Propose a store's first products from a brief. It writes nothing. */ export function generateAiCatalog(input) {
|
|
184
233
|
return runValidatedGeneration('catalog', _extends({
|
|
185
234
|
step: AI_PRODUCTS_STEP
|
|
@@ -202,9 +251,7 @@ const STOREFRONT_RULES = [
|
|
|
202
251
|
} : {}, input.provider ? {
|
|
203
252
|
provider: input.provider
|
|
204
253
|
} : {}, {
|
|
205
|
-
check: (
|
|
206
|
-
merchantWords: input.brief
|
|
207
|
-
})
|
|
254
|
+
check: aiCatalogCheck(input.brief)
|
|
208
255
|
}));
|
|
209
256
|
}
|
|
210
257
|
/** The user turn for categories and discounts: the store, what it has, and the brief. */ export function aiCategoriesPrompt(input) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.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 AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiValidatedGeneration } from './ai-doctrine'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price in any text. The rules below say the same, so the model is never\n * refused for a rule it was not told. A catalog's products each carry a\n * suggested price in a field of its own (Zach, 2026-10-12), which code keeps\n * in range (`aiCatalogPrice`); the copy still states none.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_PRICE_RULE =\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.'\nconst STOREFRONT_RULES = [\n STOREFRONT_PRICE_RULE,\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n // The price has its own field (Zach, 2026-10-12); the copy states none.\n 'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',\n ...STOREFRONT_RULES.filter((rule) => rule !== STOREFRONT_PRICE_RULE),\n 'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) => checkAiCatalog(answer, { merchantWords: input.brief }),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_PRICE_RULE","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","filter","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-products-generation.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 AI_CATALOG_NAME_CODE,\n AI_CATALOG_NAME_REASK,\n AI_CATALOG_OPTION_VALUES_MAX,\n AI_PRODUCT_CATEGORY_LIST_MAX,\n aiCatalogNameProblem,\n aiProductMerchantWords,\n type AiCatalogProduct,\n type AiCategoriesProposal,\n type AiProductCategory,\n type AiProductCopy,\n type AiProductFacts,\n} from '../model/ai-products'\nimport type { AiStepKind } from '../providers/catalog'\nimport type { AiImagePart, AiMessagePart, AiProvider } from '../providers/contract'\nimport { AI_ROUTING_TABLE, type AiPluginSettings } from '../providers/routing'\nimport {\n AI_CATALOG_TOOL,\n AI_CATALOG_TOOL_NAME,\n AI_CATEGORIES_TOOL,\n AI_CATEGORIES_TOOL_NAME,\n AI_PRODUCT_COPY_TOOL,\n AI_PRODUCT_COPY_TOOL_NAME,\n checkAiCatalog,\n checkAiCategories,\n checkAiProductCopy,\n} from '../tools/ai-products-tool'\nimport { runValidatedGeneration, type AiGenerationCheckResult, type AiValidatedGeneration } from './ai-doctrine'\nimport { AI_GENERATION_MAX_ATTEMPTS } from './ai-generation-bounds'\nimport type { AiSystemBlock } from './ai-runtime'\n\n/**\n * COMMERCE BY AI (AGL-2916): the three generations a `products` job makes, each\n * through the building doctrine's generation call (`runValidatedGeneration`):\n * the doctrine's cached block, which carries the acceptable-use rules, then the\n * generation's own rules, cached after it, and one strict tool. The store's\n * name, a product's facts, its categories, its photo and the brief ride in the\n * user turn, so no byte of a store sits inside the cached prefix, and no site\n * inventory is sent: product copy is written from the product.\n *\n * Every answer is held by a check in `tools/ai-products-tool.ts`, which is\n * where storefront copy safety is enforced — no health, financial or legal\n * claim, no certification or endorsement the merchant did not state, and no\n * price in any text. The rules below say the same, so the model is never\n * refused for a rule it was not told. A catalog's products each carry a\n * suggested price in a field of its own (Zach, 2026-10-12), which code keeps\n * in range (`aiCatalogPrice`); the copy still states none.\n */\n\n/** The step kind every generation of a `products` job is routed as. */\nexport const AI_PRODUCTS_STEP: AiStepKind = 'job.products'\n\n/**\n * Each generation's answer ceiling. A catalog is the largest answer the step\n * asks for, so it takes the routing table's; one product's copy and a set of\n * categories with discounts are far smaller, and asking them for less keeps a\n * pass's least time short enough for a bulk job to run several a beat.\n * `ai-job-products-step.spec.ts` measures the largest answer each tool accepts\n * against its ceiling.\n */\nexport const AI_CATALOG_MAX_TOKENS = AI_ROUTING_TABLE[AI_PRODUCTS_STEP].maxTokens\nexport const AI_PRODUCT_COPY_MAX_TOKENS = 1_500\nexport const AI_PRODUCT_CATEGORIES_MAX_TOKENS = 2_000\n\n/**\n * Rules every storefront generation states, because its check refuses an\n * answer for each of them.\n */\nconst STOREFRONT_PRICE_RULE =\n 'Never write a price, a discount, a shipping promise or a stock level: the store owner sets those.'\nconst STOREFRONT_RULES = [\n STOREFRONT_PRICE_RULE,\n 'Never say a product cures, treats, heals or prevents anything, and never promise a health, financial or legal result.',\n 'Never claim a certification, an award, a license, an endorsement or a best seller that the owner’s own words do not state.',\n 'Write plain text: no markup, no formatting marks, no emoji, no links.',\n]\n\n/** The rules for one product's copy. Byte-identical on every request. */\nexport const AI_PRODUCT_COPY_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You write the storefront copy for one product of an online store: its description, its search listing, its tags, the store categories it belongs in and clear names for its options.\\n\\n' +\n `Answer by calling ${AI_PRODUCT_COPY_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Write only from what you are given: the product’s name, its current text, its tags, its options and its photo. Never invent a material, a size, a quantity, an origin or a feature. Where a shopper would need a fact you do not have, mark it in square brackets, like [material], in the description only.',\n 'Keep every fact the current text states, in your own words.',\n ...STOREFRONT_RULES,\n 'The description is short paragraphs a shopper reads. The search title names the product plainly; the search description is one or two sentences; neither marks a missing fact.',\n 'Tags are words or short phrases a shopper filters by.',\n 'Choose categories only from the list you are given, by id, and none when none fits.',\n 'Give one name per option the product has, in order, keeping a name that is already clear.',\n 'When a photo is attached, use only what it plainly shows, and prefer the product’s own text where the two differ. What it shows — a color, a finish, a shape, a part — is a fact you have: write it, rather than marking it in brackets.',\n 'Write in the language of the product’s name and text.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for a store's first products. Byte-identical on every request. */\nexport const AI_CATALOG_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose the first products of an online store from its owner’s brief. Each is a draft the owner completes: a name, a type, a description, tags, options, a search listing, what its photo should show and a suggested price.\\n\\n' +\n `Answer by calling ${AI_CATALOG_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Propose 6 to 12 products that fit the brief, unless the brief says how many.',\n // AGL-4065: a parts shop's brief named kinds of part, and its products were \"Riding Gear Item\" and \"OEM ATV Parts Order\".\n 'Name each product as one thing a shopper buys. A kind of product (\"tires\", \"riding gear\") is not a product: where the brief names only kinds, name a plain item of one of them, never the kind with \"Item\", \"Order\" or \"Part\" after it, and never a brand or a model the brief does not name.',\n 'Write only from the brief. Never invent a material, a size, an origin or a feature it does not give; where a shopper would need a fact the brief does not give, mark it in square brackets, like [material], in the description only.',\n // The price has its own field (Zach, 2026-10-12); the copy states none.\n 'Never write a price, a discount, a shipping promise or a stock level in any text: the price goes only in its own field, and the owner sets the rest.',\n ...STOREFRONT_RULES.filter((rule) => rule !== STOREFRONT_PRICE_RULE),\n 'Price each product as a small independent shop would list that kind of product, in US dollars, for the size or quantity the brief gives: for example a bar of handmade soap about 8 to 12, a body lotion 14 to 22, a candle 18 to 32, a ceramic mug 24 to 38, a gift set 30 to 45. Use a whole amount or one ending in .50 or .95. One price covers every option.',\n `Options are only the choices a shopper makes, such as a size or a color, with at most ${AI_CATALOG_OPTION_VALUES_MAX} values each; a product with no choice has none.`,\n 'The photo says in one sentence what a photo of the product should show, so the owner can take or pick it. Never a link.',\n 'The search title names the product plainly, and neither it nor the search description marks a missing fact.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\n/** The rules for categories and discounts. Byte-identical on every request. */\nexport const AI_CATEGORIES_INSTRUCTIONS: AiSystemBlock[] = [\n {\n text:\n 'You propose store categories and a first set of discounts from an online store owner’s brief. The owner creates the ones they accept, and every discount starts switched off.\\n\\n' +\n `Answer by calling ${AI_CATEGORIES_TOOL_NAME} exactly once. A reply in prose cannot be used.\\n\\n` +\n 'Rules:\\n' +\n [\n 'Categories group the store’s products the way a shopper browses them. Never repeat a category the store already has.',\n 'A discount is a percentage off, an amount off in dollars, or free shipping, with an optional minimum order, and a code a shopper types or no code when it applies on its own. Keep them few and modest, and in line with the brief.',\n 'Each category and each discount says in one sentence why it helps this store.',\n 'Never promise savings, earnings or results beyond the discount itself, and never make a health, financial or legal claim.',\n 'Write plain text: no markup, no emoji, no links.',\n 'Propose nothing the brief gives no reason for; an empty list is an answer.',\n 'Write in the language of the brief.',\n ]\n .map((rule) => `- ${rule}`)\n .join('\\n'),\n cacheBreakpoint: true,\n },\n]\n\nexport interface AiProductCopyPromptInput {\n /** The store's name, as its visitors see it. */\n store: string\n product: AiProductFacts\n categories: readonly AiProductCategory[]\n /** Whether a photo rides with the request. */\n photo: boolean\n}\n\n/** The user turn's text: the store, the product as it stands, and the categories to choose from. */\nexport function aiProductCopyPrompt(input: AiProductCopyPromptInput): string {\n const { product } = input\n const lines = [\n `Store: ${input.store || 'untitled store'}`,\n `Product: ${product.name}`,\n `Type: ${product.type}`,\n 'Current text:',\n product.text.trim() || '(none)',\n `Current tags: ${product.tags.length ? product.tags.join(', ') : '(none)'}`,\n ]\n if (product.options.length) {\n lines.push('Options:', ...product.options.map((option) => `- ${option.name}: ${option.values.join(', ')}`))\n } else {\n lines.push('Options: (none)')\n }\n if (product.seoTitle || product.seoDescription) {\n lines.push(\n `Current search title: ${product.seoTitle || '(none)'}`,\n `Current search description: ${product.seoDescription || '(none)'}`,\n )\n }\n const categories = input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n lines.push(\n categories.length ? 'Categories to choose from:' : 'Categories to choose from: (none)',\n ...categories.map((category) => `- ${category.id}: ${category.name}`),\n `Photo: ${input.photo ? 'attached' : 'none'}`,\n )\n return lines.join('\\n')\n}\n\ninterface AiProductsGenerationBase {\n settings?: AiPluginSettings\n model?: string\n maxTokens?: number\n signal?: AbortSignal\n provider?: AiProvider\n}\n\nexport interface GenerateAiProductCopyInput extends AiProductsGenerationBase, AiProductCopyPromptInput {\n /** The product's photo, when the model reads pictures and the photo was read. */\n image?: AiImagePart | null\n}\n\n/**\n * Propose one product's copy. It asks the routing table's `job.products` model\n * through the doctrine's generation call and resolves to `ok` with the copy,\n * `needs_input` when the answer could not be held to the rules after one\n * re-ask, or `refused`. It writes nothing.\n */\nexport function generateAiProductCopy(input: GenerateAiProductCopyInput): Promise<AiValidatedGeneration<AiProductCopy>> {\n const image = input.photo ? (input.image ?? null) : null\n const content: AiMessagePart[] = [\n ...(image ? [image] : []),\n { type: 'text', text: aiProductCopyPrompt({ ...input, photo: Boolean(image) }) },\n ]\n return runValidatedGeneration('product copy', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_PRODUCT_COPY_INSTRUCTIONS,\n messages: [{ role: 'user', content: image ? content : aiProductCopyPrompt({ ...input, photo: false }) }],\n tool: AI_PRODUCT_COPY_TOOL,\n maxTokens: input.maxTokens ?? AI_PRODUCT_COPY_MAX_TOKENS,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiProductCopy(answer, {\n categoryIds: input.categories.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX).map((category) => category.id),\n optionCount: input.product.options.length,\n merchantWords: aiProductMerchantWords(input.product),\n }),\n })\n}\n\nexport interface GenerateAiCatalogInput extends AiProductsGenerationBase {\n store: string\n brief: string\n}\n\n/** The user turn for a catalog: the store and the brief. */\nexport function aiCatalogPrompt(input: Pick<GenerateAiCatalogInput, 'store' | 'brief'>): string {\n return [`Store: ${input.store || 'untitled store'}`, 'Brief:', input.brief.trim()].join('\\n')\n}\n\n/**\n * A catalog's check (AGL-4065): the tool's, and on the FIRST answer only, a\n * product named for a kind of product, a make or a model the brief does not\n * give ({@link aiCatalogNameProblem}), which the re-ask names. A second answer\n * is kept as the tool's check holds it; the products step leaves out any\n * product it still names so (`aiCatalogProductsNamed`).\n */\nexport function aiCatalogCheck(brief: string): (answer: Record<string, unknown>) => AiGenerationCheckResult<AiCatalogProduct[]> {\n let answers = 0\n return (answer) => {\n answers += 1\n const checked = checkAiCatalog(answer, { merchantWords: brief })\n if (answers >= AI_GENERATION_MAX_ATTEMPTS) return checked\n const raw = Array.isArray(answer['products']) ? (answer['products'] as unknown[]) : []\n const named = raw.flatMap((entry, index) => {\n const name = typeof (entry as Record<string, unknown> | null)?.['name'] === 'string' ? ((entry as Record<string, unknown>)['name'] as string) : ''\n return aiCatalogNameProblem(name, brief) ? [{ index, name }] : []\n })\n if (!named.length) return checked\n const offending: Record<string, unknown> = { ...(checked.offending ?? {}) }\n for (const { index, name } of named) offending[`products[${index}].name`] = name\n return {\n value: null,\n violations: [\n ...checked.violations,\n { rule: null, code: AI_CATALOG_NAME_CODE, message: AI_CATALOG_NAME_REASK, paths: named.map(({ index }) => `products[${index}].name`) },\n ],\n offending,\n }\n }\n}\n\n/** A catalog's products less any its re-ask still named for a kind, a make or a model (AGL-4065). Pure. */\nexport function aiCatalogProductsNamed(products: readonly AiCatalogProduct[], brief: string): AiCatalogProduct[] {\n return products.filter((product) => !aiCatalogNameProblem(product.name, brief))\n}\n\n/** Propose a store's first products from a brief. It writes nothing. */\nexport function generateAiCatalog(input: GenerateAiCatalogInput & { maxTokens: number }): Promise<AiValidatedGeneration<AiCatalogProduct[]>> {\n return runValidatedGeneration('catalog', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATALOG_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCatalogPrompt(input) }],\n tool: AI_CATALOG_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: aiCatalogCheck(input.brief),\n })\n}\n\nexport interface GenerateAiCategoriesInput extends AiProductsGenerationBase {\n store: string\n brief: string\n /** The names of the categories the store already has. */\n existingCategoryNames: readonly string[]\n}\n\n/** The user turn for categories and discounts: the store, what it has, and the brief. */\nexport function aiCategoriesPrompt(input: Pick<GenerateAiCategoriesInput, 'store' | 'brief' | 'existingCategoryNames'>): string {\n const existing = input.existingCategoryNames.slice(0, AI_PRODUCT_CATEGORY_LIST_MAX)\n return [\n `Store: ${input.store || 'untitled store'}`,\n `Categories the store already has: ${existing.length ? existing.join(', ') : '(none)'}`,\n 'Brief:',\n input.brief.trim(),\n ].join('\\n')\n}\n\n/** Propose categories and discounts from a brief. It writes nothing. */\nexport function generateAiCategories(\n input: GenerateAiCategoriesInput & { maxTokens: number },\n): Promise<AiValidatedGeneration<Pick<AiCategoriesProposal, 'categories' | 'discounts'>>> {\n return runValidatedGeneration('store categories and discounts', {\n step: AI_PRODUCTS_STEP,\n ...(input.settings ? { settings: input.settings } : {}),\n ...(input.model ? { model: input.model } : {}),\n instructions: AI_CATEGORIES_INSTRUCTIONS,\n messages: [{ role: 'user', content: aiCategoriesPrompt(input) }],\n tool: AI_CATEGORIES_TOOL,\n maxTokens: input.maxTokens,\n ...thinkingOf(),\n ...(input.signal ? { signal: input.signal } : {}),\n ...(input.provider ? { provider: input.provider } : {}),\n check: (answer) =>\n checkAiCategories(answer, {\n existingCategoryNames: input.existingCategoryNames,\n merchantWords: input.brief,\n }),\n })\n}\n\n/** The routing row's thinking and effort, sent only where the row names one. */\nfunction thinkingOf() {\n const row = AI_ROUTING_TABLE[AI_PRODUCTS_STEP]\n return {\n ...(row.thinking ? { thinking: row.thinking } : {}),\n ...(row.effort ? { effort: row.effort } : {}),\n }\n}\n"],"names":["AI_CATALOG_NAME_CODE","AI_CATALOG_NAME_REASK","AI_CATALOG_OPTION_VALUES_MAX","AI_PRODUCT_CATEGORY_LIST_MAX","aiCatalogNameProblem","aiProductMerchantWords","AI_ROUTING_TABLE","AI_CATALOG_TOOL","AI_CATALOG_TOOL_NAME","AI_CATEGORIES_TOOL","AI_CATEGORIES_TOOL_NAME","AI_PRODUCT_COPY_TOOL","AI_PRODUCT_COPY_TOOL_NAME","checkAiCatalog","checkAiCategories","checkAiProductCopy","runValidatedGeneration","AI_GENERATION_MAX_ATTEMPTS","AI_PRODUCTS_STEP","AI_CATALOG_MAX_TOKENS","maxTokens","AI_PRODUCT_COPY_MAX_TOKENS","AI_PRODUCT_CATEGORIES_MAX_TOKENS","STOREFRONT_PRICE_RULE","STOREFRONT_RULES","AI_PRODUCT_COPY_INSTRUCTIONS","text","map","rule","join","cacheBreakpoint","AI_CATALOG_INSTRUCTIONS","filter","AI_CATEGORIES_INSTRUCTIONS","aiProductCopyPrompt","input","product","lines","store","name","type","trim","tags","length","options","push","option","values","seoTitle","seoDescription","categories","slice","category","id","photo","generateAiProductCopy","image","content","Boolean","step","settings","model","instructions","messages","role","tool","thinkingOf","signal","provider","check","answer","categoryIds","optionCount","merchantWords","aiCatalogPrompt","brief","aiCatalogCheck","answers","checked","raw","Array","isArray","named","flatMap","entry","index","offending","value","violations","code","message","paths","aiCatalogProductsNamed","products","generateAiCatalog","aiCategoriesPrompt","existing","existingCategoryNames","generateAiCategories","row","thinking","effort"],"mappings":"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"}
|
|
@@ -33,6 +33,8 @@ export interface AssistAnswerCacheInputs {
|
|
|
33
33
|
model: string;
|
|
34
34
|
/** The product name the assistant answers under (white-label aware). */
|
|
35
35
|
productName: string;
|
|
36
|
+
/** The workspace's plan by name (AGL-4075), which every answer is told. */
|
|
37
|
+
plan?: string;
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Normalise a question so trivially different phrasings share an entry.
|
|
@@ -108,13 +108,16 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
108
108
|
*
|
|
109
109
|
* Joined on {@link CACHE_KEY_DELIMITER}, which no part can contain.
|
|
110
110
|
*/ export function assistAnswerCacheKey(inputs) {
|
|
111
|
-
var _inputs_tier, _inputs_route, _inputs_model, _inputs_productName;
|
|
111
|
+
var _inputs_tier, _inputs_route, _inputs_model, _inputs_productName, _inputs_plan;
|
|
112
112
|
const parts = [
|
|
113
113
|
normaliseAssistQuestion(inputs.question),
|
|
114
114
|
String((_inputs_tier = inputs.tier) != null ? _inputs_tier : ''),
|
|
115
115
|
String((_inputs_route = inputs.route) != null ? _inputs_route : ''),
|
|
116
116
|
String((_inputs_model = inputs.model) != null ? _inputs_model : ''),
|
|
117
|
-
String((_inputs_productName = inputs.productName) != null ? _inputs_productName : '')
|
|
117
|
+
String((_inputs_productName = inputs.productName) != null ? _inputs_productName : ''),
|
|
118
|
+
// The plan's name rides every prompt since AGL-4075, so "what plan am I
|
|
119
|
+
// on" must not be answered from before an upgrade.
|
|
120
|
+
String((_inputs_plan = inputs.plan) != null ? _inputs_plan : '')
|
|
118
121
|
];
|
|
119
122
|
return createHash('sha256').update(parts.join(CACHE_KEY_DELIMITER)).digest('hex').slice(0, 32);
|
|
120
123
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/assist-answer-cache.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 { createHash } from 'node:crypto'\n\n/**\n * The answer cache for Aglyn Assist escalations (AGL-2486).\n *\n * An identical question asked twice must not buy two completions. Retrieval\n * deflection already takes most of the repeats — the questions people ask over\n * and over are exactly the ones the documentation answers — so what reaches\n * here is the residue: the docs-shaped questions retrieval was not confident\n * enough about, asked again a week later in the same words. The yield is\n * therefore modest by construction, and saying so is the honest framing: this\n * is the second lever, not the first.\n *\n * ## Scoped to ONE workspace, deliberately\n *\n * A cross-tenant cache would hit far more often, and it is the wrong trade.\n * An entitled answer is composed against that workspace's own name, plan and\n * current screen, and every deployment can carry white-label orgs whose\n * assistant answers under their own product name. Sharing an entry between\n * two workspaces means one workspace's facts reaching the other's panel the\n * first time a key collides, and a cache is the last place anyone looks for\n * that. `orgs/{orgId}/…` makes it impossible rather than unlikely.\n *\n * The key still carries every input that varies the answer — see\n * {@link assistAnswerCacheKey}. Scoping alone is not enough: within one\n * workspace a free member and a Pro member get different capability levels,\n * and two people on different screens get different guidance.\n *\n * ## One document, self-pruning — and why not a TTL\n *\n * Entries live in a single `counters/assistAnswerCache` document as a map,\n * pruned to {@link MAX_CACHE_ENTRIES} newest on every write. That shape was\n * chosen against a per-document collection with a TTL field, which is the\n * more obvious design and the worse one here:\n *\n * - A Firestore TTL policy is MANUAL gcloud configuration, not repo state\n * (see `docs/FIRESTORE_MANUAL_CONFIG.md`). A new collection that assumes one\n * grows without bound on every deployment where nobody ran the command —\n * including every self-hoster, none of whom will know it exists.\n * - Pruning on write needs no policy, no index and no operator step, and it\n * bounds the document on the deployment that is actually using it.\n *\n * Write contention is the cost, and it is affordable: escalations are bounded\n * by the message caps (ten a day free, a thousand a month entitled), which is\n * orders of magnitude under the per-document write ceiling.\n *\n * ## Staleness\n *\n * An entry expires after {@link CACHE_TTL_MS}. The bound exists because the\n * product moves: an answer describing a screen that has since been rebuilt is\n * worse than no cache, and unlike a docs deflection there is no live citation\n * for the user to check it against. Expiry is evaluated on READ as well as\n * pruned on write, so a stale entry cannot be served by a deployment that has\n * stopped writing.\n */\n\n/** Cached answers kept per workspace. */\nconst MAX_CACHE_ENTRIES = 60\n\n/**\n * How long a cached answer may be served — seven days.\n *\n * Long enough that a question asked on Monday and again the following Monday\n * hits, which is the recurrence pattern this exists for; short enough that an\n * answer never outlives a release cycle by much. The failure it bounds is an\n * answer that was right when it was written and is now confidently describing\n * a screen that changed.\n */\nconst CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000\n\n/** One doc per org, beside the other `counters/*` documents. */\nconst CACHE_DOC = 'assistAnswerCache'\n\nexport interface AssistCachedAnswer {\n answer: string\n docs: { title: string; url: string }[]\n /** Epoch ms the entry was written. */\n at: number\n}\n\nexport interface AssistAnswerCacheInputs {\n question: string\n /** 'free' or 'entitled' — different capability levels, different answers. */\n tier: string\n /** The console route the question was asked from ('' when none). */\n route: string\n /** The serving model id. */\n model: string\n /** The product name the assistant answers under (white-label aware). */\n productName: string\n}\n\n/**\n * Normalise a question so trivially different phrasings share an entry.\n *\n * Case, surrounding whitespace, internal runs of whitespace and trailing\n * punctuation only. Deliberately NOT stemming, stop-word removal or token\n * sorting: those would make \"how do I turn OFF maintenance mode\" and \"how do\n * I turn ON maintenance mode\" the same key, and serving one for the other is\n * a wrong answer rather than a missed saving. A cache key may only ever\n * collapse differences that cannot change the answer.\n */\nexport function normaliseAssistQuestion(question: string): string {\n return String(question ?? '')\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .replace(/[\\s?!.,;:]+$/g, '')\n .trim()\n}\n\n/**\n * The delimiter the key parts are joined on: NUL, written as an ESCAPE rather\n * than as a literal byte.\n *\n * NUL because it is the one character that cannot appear in a question, a\n * route, a model id or a product name, so no pair of different inputs can\n * concatenate into the same string — the classic way a hashed composite key\n * develops collisions that read as cache bugs.\n *\n * As an escape because a literal NUL in the source makes git classify the\n * whole file as BINARY: no diff, no review, no blame. It shipped that way once\n * and the commit carried `Bin 0 -> 9601 bytes` where the file should have been.\n */\nconst CACHE_KEY_DELIMITER = '\\u0000'\n\n/**\n * The cache key: a hash over EVERY input that can change the answer.\n *\n * Joined on {@link CACHE_KEY_DELIMITER}, which no part can contain.\n */\nexport function assistAnswerCacheKey(inputs: AssistAnswerCacheInputs): string {\n const parts = [\n normaliseAssistQuestion(inputs.question),\n String(inputs.tier ?? ''),\n String(inputs.route ?? ''),\n String(inputs.model ?? ''),\n String(inputs.productName ?? ''),\n ]\n return createHash('sha256')\n .update(parts.join(CACHE_KEY_DELIMITER))\n .digest('hex')\n .slice(0, 32)\n}\n\n/** Whether an entry is still inside the freshness window. */\nexport function assistCacheEntryIsFresh(\n entry: AssistCachedAnswer | null | undefined,\n now = Date.now(),\n): boolean {\n if (!entry || typeof entry.answer !== 'string' || !entry.answer) return false\n const at = Number(entry.at)\n // `strictNullChecks` is off repo-wide, so a missing `at` reads as NaN rather\n // than failing a null check. NaN fails every comparison, which is the right\n // way round: an entry that cannot say when it was written is not fresh.\n if (!Number.isFinite(at)) return false\n return now - at < CACHE_TTL_MS\n}\n\n/**\n * Read one cached answer, or `null`.\n *\n * Never throws: a cache is an optimisation, and an unavailable one must cost\n * a model call rather than the user's answer. The caller therefore cannot\n * tell a miss from an outage, which is correct — both mean \"call the model\".\n */\nexport async function readAssistAnswerCache(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n key: string,\n now = Date.now(),\n): Promise<AssistCachedAnswer | null> {\n try {\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(CACHE_DOC)\n .get()\n const entry = snapshot.get(key) as AssistCachedAnswer | undefined\n return assistCacheEntryIsFresh(entry, now) ? entry : null\n } catch (error) {\n console.error('assist answer cache read failed', error)\n return null\n }\n}\n\n/**\n * Store one answer, pruning the document back to {@link MAX_CACHE_ENTRIES}.\n *\n * Read-then-write rather than a blind merging set, because pruning needs to\n * see what is already there. Not a transaction: two concurrent writers can\n * lose one entry to each other, and losing a cache entry costs one model call\n * later — a transaction would cost a contended write on every escalation to\n * prevent that. The counters this module must never race are in\n * `reserveAssistMessage`, and none of them are here.\n *\n * Never throws, for the same reason the read does not.\n */\nexport async function writeAssistAnswerCache(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n key: string,\n value: Omit<AssistCachedAnswer, 'at'>,\n now = Date.now(),\n): Promise<void> {\n try {\n const ref = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(CACHE_DOC)\n const snapshot = await ref.get()\n const existing = (snapshot.data() ?? {}) as Record<\n string,\n AssistCachedAnswer\n >\n const next: Record<string, AssistCachedAnswer> = {\n ...existing,\n [key]: { answer: value.answer, docs: value.docs ?? [], at: now },\n }\n const fresh = Object.entries(next).filter(([, entry]) =>\n assistCacheEntryIsFresh(entry, now),\n )\n fresh.sort((a, b) => Number(b[1].at) - Number(a[1].at))\n // A full `set` without merge, so pruning actually REMOVES entries. A\n // merging set can only ever add: the document would keep every key it had\n // and the cap would be a comment. This is the one place a non-merging\n // write is correct, and it is safe only because nothing else writes here.\n await ref.set(Object.fromEntries(fresh.slice(0, MAX_CACHE_ENTRIES)))\n } catch (error) {\n console.error('assist answer cache write failed', error)\n }\n}\n\n/** Exported for the spec — the bound the pruning enforces. */\nexport const ASSIST_ANSWER_CACHE_LIMIT = MAX_CACHE_ENTRIES\n/** Exported for the spec — the freshness window in ms. */\nexport const ASSIST_ANSWER_CACHE_TTL_MS = CACHE_TTL_MS\n"],"names":["createHash","MAX_CACHE_ENTRIES","CACHE_TTL_MS","CACHE_DOC","normaliseAssistQuestion","question","String","toLowerCase","replace","trim","CACHE_KEY_DELIMITER","assistAnswerCacheKey","inputs","parts","tier","route","model","productName","update","join","digest","slice","assistCacheEntryIsFresh","entry","now","Date","answer","at","Number","isFinite","readAssistAnswerCache","firestore","orgId","key","snapshot","collection","doc","get","error","console","writeAssistAnswerCache","value","ref","existing","data","next","docs","fresh","Object","entries","filter","sort","a","b","set","fromEntries","ASSIST_ANSWER_CACHE_LIMIT","ASSIST_ANSWER_CACHE_TTL_MS"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/assist-answer-cache.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 { createHash } from 'node:crypto'\n\n/**\n * The answer cache for Aglyn Assist escalations (AGL-2486).\n *\n * An identical question asked twice must not buy two completions. Retrieval\n * deflection already takes most of the repeats — the questions people ask over\n * and over are exactly the ones the documentation answers — so what reaches\n * here is the residue: the docs-shaped questions retrieval was not confident\n * enough about, asked again a week later in the same words. The yield is\n * therefore modest by construction, and saying so is the honest framing: this\n * is the second lever, not the first.\n *\n * ## Scoped to ONE workspace, deliberately\n *\n * A cross-tenant cache would hit far more often, and it is the wrong trade.\n * An entitled answer is composed against that workspace's own name, plan and\n * current screen, and every deployment can carry white-label orgs whose\n * assistant answers under their own product name. Sharing an entry between\n * two workspaces means one workspace's facts reaching the other's panel the\n * first time a key collides, and a cache is the last place anyone looks for\n * that. `orgs/{orgId}/…` makes it impossible rather than unlikely.\n *\n * The key still carries every input that varies the answer — see\n * {@link assistAnswerCacheKey}. Scoping alone is not enough: within one\n * workspace a free member and a Pro member get different capability levels,\n * and two people on different screens get different guidance.\n *\n * ## One document, self-pruning — and why not a TTL\n *\n * Entries live in a single `counters/assistAnswerCache` document as a map,\n * pruned to {@link MAX_CACHE_ENTRIES} newest on every write. That shape was\n * chosen against a per-document collection with a TTL field, which is the\n * more obvious design and the worse one here:\n *\n * - A Firestore TTL policy is MANUAL gcloud configuration, not repo state\n * (see `docs/FIRESTORE_MANUAL_CONFIG.md`). A new collection that assumes one\n * grows without bound on every deployment where nobody ran the command —\n * including every self-hoster, none of whom will know it exists.\n * - Pruning on write needs no policy, no index and no operator step, and it\n * bounds the document on the deployment that is actually using it.\n *\n * Write contention is the cost, and it is affordable: escalations are bounded\n * by the message caps (ten a day free, a thousand a month entitled), which is\n * orders of magnitude under the per-document write ceiling.\n *\n * ## Staleness\n *\n * An entry expires after {@link CACHE_TTL_MS}. The bound exists because the\n * product moves: an answer describing a screen that has since been rebuilt is\n * worse than no cache, and unlike a docs deflection there is no live citation\n * for the user to check it against. Expiry is evaluated on READ as well as\n * pruned on write, so a stale entry cannot be served by a deployment that has\n * stopped writing.\n */\n\n/** Cached answers kept per workspace. */\nconst MAX_CACHE_ENTRIES = 60\n\n/**\n * How long a cached answer may be served — seven days.\n *\n * Long enough that a question asked on Monday and again the following Monday\n * hits, which is the recurrence pattern this exists for; short enough that an\n * answer never outlives a release cycle by much. The failure it bounds is an\n * answer that was right when it was written and is now confidently describing\n * a screen that changed.\n */\nconst CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000\n\n/** One doc per org, beside the other `counters/*` documents. */\nconst CACHE_DOC = 'assistAnswerCache'\n\nexport interface AssistCachedAnswer {\n answer: string\n docs: { title: string; url: string }[]\n /** Epoch ms the entry was written. */\n at: number\n}\n\nexport interface AssistAnswerCacheInputs {\n question: string\n /** 'free' or 'entitled' — different capability levels, different answers. */\n tier: string\n /** The console route the question was asked from ('' when none). */\n route: string\n /** The serving model id. */\n model: string\n /** The product name the assistant answers under (white-label aware). */\n productName: string\n /** The workspace's plan by name (AGL-4075), which every answer is told. */\n plan?: string\n}\n\n/**\n * Normalise a question so trivially different phrasings share an entry.\n *\n * Case, surrounding whitespace, internal runs of whitespace and trailing\n * punctuation only. Deliberately NOT stemming, stop-word removal or token\n * sorting: those would make \"how do I turn OFF maintenance mode\" and \"how do\n * I turn ON maintenance mode\" the same key, and serving one for the other is\n * a wrong answer rather than a missed saving. A cache key may only ever\n * collapse differences that cannot change the answer.\n */\nexport function normaliseAssistQuestion(question: string): string {\n return String(question ?? '')\n .toLowerCase()\n .replace(/\\s+/g, ' ')\n .replace(/[\\s?!.,;:]+$/g, '')\n .trim()\n}\n\n/**\n * The delimiter the key parts are joined on: NUL, written as an ESCAPE rather\n * than as a literal byte.\n *\n * NUL because it is the one character that cannot appear in a question, a\n * route, a model id or a product name, so no pair of different inputs can\n * concatenate into the same string — the classic way a hashed composite key\n * develops collisions that read as cache bugs.\n *\n * As an escape because a literal NUL in the source makes git classify the\n * whole file as BINARY: no diff, no review, no blame. It shipped that way once\n * and the commit carried `Bin 0 -> 9601 bytes` where the file should have been.\n */\nconst CACHE_KEY_DELIMITER = '\\u0000'\n\n/**\n * The cache key: a hash over EVERY input that can change the answer.\n *\n * Joined on {@link CACHE_KEY_DELIMITER}, which no part can contain.\n */\nexport function assistAnswerCacheKey(inputs: AssistAnswerCacheInputs): string {\n const parts = [\n normaliseAssistQuestion(inputs.question),\n String(inputs.tier ?? ''),\n String(inputs.route ?? ''),\n String(inputs.model ?? ''),\n String(inputs.productName ?? ''),\n // The plan's name rides every prompt since AGL-4075, so \"what plan am I\n // on\" must not be answered from before an upgrade.\n String(inputs.plan ?? ''),\n ]\n return createHash('sha256')\n .update(parts.join(CACHE_KEY_DELIMITER))\n .digest('hex')\n .slice(0, 32)\n}\n\n/** Whether an entry is still inside the freshness window. */\nexport function assistCacheEntryIsFresh(\n entry: AssistCachedAnswer | null | undefined,\n now = Date.now(),\n): boolean {\n if (!entry || typeof entry.answer !== 'string' || !entry.answer) return false\n const at = Number(entry.at)\n // `strictNullChecks` is off repo-wide, so a missing `at` reads as NaN rather\n // than failing a null check. NaN fails every comparison, which is the right\n // way round: an entry that cannot say when it was written is not fresh.\n if (!Number.isFinite(at)) return false\n return now - at < CACHE_TTL_MS\n}\n\n/**\n * Read one cached answer, or `null`.\n *\n * Never throws: a cache is an optimisation, and an unavailable one must cost\n * a model call rather than the user's answer. The caller therefore cannot\n * tell a miss from an outage, which is correct — both mean \"call the model\".\n */\nexport async function readAssistAnswerCache(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n key: string,\n now = Date.now(),\n): Promise<AssistCachedAnswer | null> {\n try {\n const snapshot = await firestore\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(CACHE_DOC)\n .get()\n const entry = snapshot.get(key) as AssistCachedAnswer | undefined\n return assistCacheEntryIsFresh(entry, now) ? entry : null\n } catch (error) {\n console.error('assist answer cache read failed', error)\n return null\n }\n}\n\n/**\n * Store one answer, pruning the document back to {@link MAX_CACHE_ENTRIES}.\n *\n * Read-then-write rather than a blind merging set, because pruning needs to\n * see what is already there. Not a transaction: two concurrent writers can\n * lose one entry to each other, and losing a cache entry costs one model call\n * later — a transaction would cost a contended write on every escalation to\n * prevent that. The counters this module must never race are in\n * `reserveAssistMessage`, and none of them are here.\n *\n * Never throws, for the same reason the read does not.\n */\nexport async function writeAssistAnswerCache(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n key: string,\n value: Omit<AssistCachedAnswer, 'at'>,\n now = Date.now(),\n): Promise<void> {\n try {\n const ref = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('counters')\n .doc(CACHE_DOC)\n const snapshot = await ref.get()\n const existing = (snapshot.data() ?? {}) as Record<\n string,\n AssistCachedAnswer\n >\n const next: Record<string, AssistCachedAnswer> = {\n ...existing,\n [key]: { answer: value.answer, docs: value.docs ?? [], at: now },\n }\n const fresh = Object.entries(next).filter(([, entry]) =>\n assistCacheEntryIsFresh(entry, now),\n )\n fresh.sort((a, b) => Number(b[1].at) - Number(a[1].at))\n // A full `set` without merge, so pruning actually REMOVES entries. A\n // merging set can only ever add: the document would keep every key it had\n // and the cap would be a comment. This is the one place a non-merging\n // write is correct, and it is safe only because nothing else writes here.\n await ref.set(Object.fromEntries(fresh.slice(0, MAX_CACHE_ENTRIES)))\n } catch (error) {\n console.error('assist answer cache write failed', error)\n }\n}\n\n/** Exported for the spec — the bound the pruning enforces. */\nexport const ASSIST_ANSWER_CACHE_LIMIT = MAX_CACHE_ENTRIES\n/** Exported for the spec — the freshness window in ms. */\nexport const ASSIST_ANSWER_CACHE_TTL_MS = CACHE_TTL_MS\n"],"names":["createHash","MAX_CACHE_ENTRIES","CACHE_TTL_MS","CACHE_DOC","normaliseAssistQuestion","question","String","toLowerCase","replace","trim","CACHE_KEY_DELIMITER","assistAnswerCacheKey","inputs","parts","tier","route","model","productName","plan","update","join","digest","slice","assistCacheEntryIsFresh","entry","now","Date","answer","at","Number","isFinite","readAssistAnswerCache","firestore","orgId","key","snapshot","collection","doc","get","error","console","writeAssistAnswerCache","value","ref","existing","data","next","docs","fresh","Object","entries","filter","sort","a","b","set","fromEntries","ASSIST_ANSWER_CACHE_LIMIT","ASSIST_ANSWER_CACHE_TTL_MS"],"mappings":"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"}
|
|
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { pluginRequestFromWeb } from "@aglyn/aglyn/server";
|
|
18
18
|
import { assistCreditsFromUsd, resolveAssistCreditBudget } from "../usage/assist-credits.js";
|
|
19
19
|
import { hostRoleFor, isOrgWideMember } from "@aglyn/aglyn/app-utils/organizations";
|
|
20
|
+
import { resolveSiteRolePresetPermissions } from "@aglyn/aglyn/app-utils/host-permissions";
|
|
20
21
|
import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
21
22
|
import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin";
|
|
22
23
|
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
@@ -90,9 +91,13 @@ async function authenticate(headers) {
|
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* A collaborator scoped to the site who holds "Manage site settings" there
|
|
96
|
+
* (`site.manage`, AGL-4047): their site role's preset (Admin), then the
|
|
97
|
+
* site's toggle for them.
|
|
98
|
+
*/ function isSiteAdminCollaborator(member, hostId) {
|
|
94
99
|
if (!member || !hostId || isOrgWideMember(member)) return false;
|
|
95
|
-
return
|
|
100
|
+
return resolveSiteRolePresetPermissions(member, hostId)['site.manage'] === true;
|
|
96
101
|
}
|
|
97
102
|
/** The roster and each member's month, by path — no index, one getAll a hundred. */ async function readRosterMonths(firestore, orgRef, month) {
|
|
98
103
|
const roster = await orgRef.collection('members').get();
|