acinguiux-preact-components 0.0.1
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 +56 -0
- package/src/content/themes/theme-acinguiux-amg/theme-acinguiux-amg.css +23 -0
- package/src/content/themes/theme-acinguiux-cafe/theme-acinguiux-cafe.css +47 -0
- package/src/content/themes/theme-acinguiux-energy/theme-acinguiux-energy.css +45 -0
- package/src/content/themes/theme-acinguiux-livewire/theme-acinguiux-livewire.css +22 -0
- package/src/content/themes/theme-acinguiux-livewire-italy/theme-acinguiux-livewire-italy.css +22 -0
- package/src/content/themes/theme-acinguiux-recharge/theme-acinguiux-recharge.css +49 -0
- package/src/content/themes/theme-allon/theme-allon.css +25 -0
- package/src/content/themes/theme-atlas/theme-atlas.css +31 -0
- package/src/content/themes/theme-aurvana/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/favicon.svg +13 -0
- package/src/content/themes/theme-aurvana/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-aurvana/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-aurvana/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-aurvana/theme-aurvana.css +49 -0
- package/src/content/themes/theme-base/theme-base.css +49 -0
- package/src/content/themes/theme-base2/resources/favicon/android-chrome-192x192.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/android-chrome-512x512.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-16x16.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-32x32.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/favicon.svg +9 -0
- package/src/content/themes/theme-base2/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-base2/resources/favicon/site.webmanifest +1 -0
- package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-base2/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-base2/resources/fonts/acinguiux-typeface-la-heavy-221208.woff2 +0 -0
- package/src/content/themes/theme-base2/theme-base2.css +47 -0
- package/src/content/themes/theme-eco-marathon/theme-eco-marathon.css +22 -0
- package/src/content/themes/theme-energy-transition-campus-amsterdam/theme-energy-transition-campus-amsterdam.css +26 -0
- package/src/content/themes/theme-evpass/theme-evpass.css +46 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/favicon.svg +9 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-nam-2025/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-nam-2025/theme-nam-2025.css +47 -0
- package/src/content/themes/theme-pennzoil/theme-pennzoil.css +36 -0
- package/src/content/themes/theme-quaker-state/theme-quaker-state.css +63 -0
- package/src/content/themes/theme-tafawoq/theme-tafawoq.css +26 -0
- package/src/content/themes/theme-vegetable/resources/favicon/apple-touch-icon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favico.ico +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon-96x96.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.ico +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/favicon.svg +13 -0
- package/src/content/themes/theme-vegetable/resources/favicon/google-touch-icon.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/manifest.json +14 -0
- package/src/content/themes/theme-vegetable/resources/favicon/site.webmanifest +21 -0
- package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-192x192.png +0 -0
- package/src/content/themes/theme-vegetable/resources/favicon/web-app-manifest-512x512.png +0 -0
- package/src/content/themes/theme-vegetable/theme-vegetable.css +49 -0
- package/src/content/themes/theme-zeolyst/resources/fonts/type-ar-medium.woff2 +0 -0
- package/src/content/themes/theme-zeolyst/theme-zeolyst.css +29 -0
- package/src/main/atoms/audio.js +16 -0
- package/src/main/atoms/box.js +5 -0
- package/src/main/atoms/button.js +40 -0
- package/src/main/atoms/card.js +22 -0
- package/src/main/atoms/form.js +30 -0
- package/src/main/atoms/heading.js +17 -0
- package/src/main/atoms/icon.js +24 -0
- package/src/main/atoms/img.js +131 -0
- package/src/main/atoms/input.js +55 -0
- package/src/main/atoms/link-text.js +21 -0
- package/src/main/atoms/link.js +60 -0
- package/src/main/atoms/list.js +12 -0
- package/src/main/atoms/logo.js +9 -0
- package/src/main/atoms/menu.js +10 -0
- package/src/main/atoms/message.js +5 -0
- package/src/main/atoms/nav-link.js +49 -0
- package/src/main/atoms/popup.js +47 -0
- package/src/main/atoms/rich-text.js +128 -0
- package/src/main/atoms/scroller.js +224 -0
- package/src/main/atoms/svg.js +65 -0
- package/src/main/atoms/table.js +32 -0
- package/src/main/atoms/textarea.js +10 -0
- package/src/main/atoms/time.js +12 -0
- package/src/main/atoms/video.js +100 -0
- package/src/main/export-main.js +12 -0
- package/src/main/export-matter.js +86 -0
- package/src/main/export-preact-hooks.js +1 -0
- package/src/main/export-preact.js +1 -0
- package/src/main/index.js +13 -0
- package/src/main/molecules/asset.js +23 -0
- package/src/main/molecules/glossary.js +44 -0
- package/src/main/molecules/links.js +23 -0
- package/src/main/molecules/promo-text.js +27 -0
- package/src/main/molecules/tags.js +15 -0
- package/src/main/molecules/tree.js +51 -0
- package/src/main/organisms/accordion-item.js +106 -0
- package/src/main/organisms/author.js +29 -0
- package/src/main/organisms/breadcrumb.js +69 -0
- package/src/main/organisms/call-to-action.js +24 -0
- package/src/main/organisms/carousel.js +178 -0
- package/src/main/organisms/cart-item.js +156 -0
- package/src/main/organisms/cart.js +162 -0
- package/src/main/organisms/contact-form.js +141 -0
- package/src/main/organisms/container/ab-test.js +47 -0
- package/src/main/organisms/container/default.js +6 -0
- package/src/main/organisms/container/filtered-section.js +293 -0
- package/src/main/organisms/container/footer.js +12 -0
- package/src/main/organisms/container/grid.js +44 -0
- package/src/main/organisms/container/header.js +13 -0
- package/src/main/organisms/container/list.js +7 -0
- package/src/main/organisms/container/main.js +6 -0
- package/src/main/organisms/container/raw.js +7 -0
- package/src/main/organisms/container/section.js +28 -0
- package/src/main/organisms/container.js +29 -0
- package/src/main/organisms/content-owner.js +15 -0
- package/src/main/organisms/date-entry.js +56 -0
- package/src/main/organisms/external-search.js +73 -0
- package/src/main/organisms/filtered-item.js +163 -0
- package/src/main/organisms/footer-item.js +17 -0
- package/src/main/organisms/image-gallery.js +164 -0
- package/src/main/organisms/last-modified.js +20 -0
- package/src/main/organisms/legal-footer.js +16 -0
- package/src/main/organisms/list-item.js +48 -0
- package/src/main/organisms/metadata.js +11 -0
- package/src/main/organisms/navigation.js +232 -0
- package/src/main/organisms/notification.js +87 -0
- package/src/main/organisms/order-tracker.js +203 -0
- package/src/main/organisms/page-header-banner.js +26 -0
- package/src/main/organisms/page-header.js +33 -0
- package/src/main/organisms/page-tags.js +14 -0
- package/src/main/organisms/page.js +260 -0
- package/src/main/organisms/press-release.js +24 -0
- package/src/main/organisms/product-admin.js +204 -0
- package/src/main/organisms/promo-banner.js +28 -0
- package/src/main/organisms/promo-bottom.js +23 -0
- package/src/main/organisms/promo-button.js +8 -0
- package/src/main/organisms/promo-card-cover.js +35 -0
- package/src/main/organisms/promo-card.js +33 -0
- package/src/main/organisms/promo-full.js +20 -0
- package/src/main/organisms/promo-image.js +22 -0
- package/src/main/organisms/promo-lure.js +22 -0
- package/src/main/organisms/promo-product-card.js +187 -0
- package/src/main/organisms/promo-product-full.js +293 -0
- package/src/main/organisms/promo-simple.js +23 -0
- package/src/main/organisms/quote.js +21 -0
- package/src/main/organisms/search-form.js +42 -0
- package/src/main/organisms/search-nav.js +66 -0
- package/src/main/organisms/search-result.js +53 -0
- package/src/main/organisms/slider.js +26 -0
- package/src/main/organisms/standalone-asset.js +22 -0
- package/src/main/organisms/tabs.js +277 -0
- package/src/main/organisms/topbar.js +83 -0
- package/src/main/organisms/web-component.js +53 -0
- package/src/main/routing/annotation.js +9 -0
- package/src/main/routing/component.js +138 -0
- package/src/main/routing/empty.js +5 -0
- package/src/main/routing/error-handler.js +64 -0
- package/src/main/routing/placeholder-image.svg +5 -0
- package/src/main/routing/router.js +219 -0
- package/src/main/shared/analytics.js +677 -0
- package/src/main/shared/bubble-event.js +11 -0
- package/src/main/shared/custom-element.js +21 -0
- package/src/main/shared/deep-selector.js +28 -0
- package/src/main/shared/disable-transparency.js +10 -0
- package/src/main/shared/format-time.js +8 -0
- package/src/main/shared/get-id.js +5 -0
- package/src/main/shared/get-meta.js +3 -0
- package/src/main/shared/get-size-class.js +3 -0
- package/src/main/shared/get-size.js +11 -0
- package/src/main/shared/h.js +88 -0
- package/src/main/shared/hash-jump.js +33 -0
- package/src/main/shared/icons/arrow-back.svg +1 -0
- package/src/main/shared/icons/arrow-down.svg +1 -0
- package/src/main/shared/icons/arrow-next.svg +1 -0
- package/src/main/shared/icons/arrow-tail-right.svg +1 -0
- package/src/main/shared/icons/arrow-tail-up.svg +1 -0
- package/src/main/shared/icons/arrow-up.svg +1 -0
- package/src/main/shared/icons/asset-download.svg +1 -0
- package/src/main/shared/icons/logo.svg +5 -0
- package/src/main/shared/icons/low-carbon-placeholder.svg +9 -0
- package/src/main/shared/icons/media-pause.svg +1 -0
- package/src/main/shared/icons/media-play.svg +1 -0
- package/src/main/shared/icons/navigation-burger.svg +1 -0
- package/src/main/shared/icons/navigation-close.svg +1 -0
- package/src/main/shared/icons/navigation-link.svg +1 -0
- package/src/main/shared/icons/navigation-refresh.svg +1 -0
- package/src/main/shared/icons/navigation-search.svg +1 -0
- package/src/main/shared/icons/navigation-share.svg +1 -0
- package/src/main/shared/icons/toggle-newwindow.svg +1 -0
- package/src/main/shared/icons.js +18 -0
- package/src/main/shared/id-from-string.js +5 -0
- package/src/main/shared/mark-selection.js +19 -0
- package/src/main/shared/register.js +26 -0
- package/src/main/shared/renderer.js +43 -0
- package/src/main/shared/simple-consent-api.js +70 -0
- package/src/main/shared/split-links.js +11 -0
- package/src/main/shared/t.js +60 -0
- package/src/main/shared/twind.js +837 -0
- package/src/main/shared/update-head.js +34 -0
- package/src/main/shared/update-scrollbar-width.js +30 -0
- package/src/main/shared/use-link.js +151 -0
- package/src/main/shared/use-persistent-state.js +42 -0
- package/src/main/shared/wait-for-dom-ready.js +6 -0
- package/src/main/shared/wcm-mode.js +4 -0
- package/src/wcs/components/acinguiux-preact-doc/acinguiux-preact-doc.js +207 -0
- package/src/wcs/components/admin-dashboard/admin-dashboard.js +487 -0
- package/src/wcs/components/admin-login/admin-login.js +91 -0
- package/src/wcs/components/bazaar-voice/bazaar-voice.js +56 -0
- package/src/wcs/components/chatbot-koreai/chatbot-koreai.js +176 -0
- package/src/wcs/components/chatbot-koreai/koreai-transport.js +217 -0
- package/src/wcs/components/chatbot-ms/chatbot-ms.js +210 -0
- package/src/wcs/components/chatbot-test/chatbot-test.js +44 -0
- package/src/wcs/components/comparison-chart/comparison-chart.js +111 -0
- package/src/wcs/components/consent-banner/consent-banner.js +248 -0
- package/src/wcs/components/consent-banner/icons/ccpa.svg +6 -0
- package/src/wcs/components/consent-banner/icons/info.svg +1 -0
- package/src/wcs/components/consent-banner/provider-onetrust.js +131 -0
- package/src/wcs/components/decision-tree/arrow-back.svg +3 -0
- package/src/wcs/components/decision-tree/badges.js +37 -0
- package/src/wcs/components/decision-tree/decision-tree.js +162 -0
- package/src/wcs/components/dynamic-contact-details/dynamic-contact-details.js +111 -0
- package/src/wcs/components/example-accordion/example-accordion.js +10 -0
- package/src/wcs/components/example-asset/example-asset.js +12 -0
- package/src/wcs/components/example-form/example-form.js +59 -0
- package/src/wcs/components/example-nested/example-nested.js +10 -0
- package/src/wcs/components/example-routing/example-routing.js +51 -0
- package/src/wcs/components/example-rtl/example-rtl.js +28 -0
- package/src/wcs/components/example-tabs/example-tabs.js +12 -0
- package/src/wcs/components/example-web-component/example-web-component.js +34 -0
- package/src/wcs/components/floating-button/floating-button.js +17 -0
- package/src/wcs/components/formstack-form/fields/address.js +38 -0
- package/src/wcs/components/formstack-form/fields/checkbox.js +42 -0
- package/src/wcs/components/formstack-form/fields/date.js +22 -0
- package/src/wcs/components/formstack-form/fields/description.js +8 -0
- package/src/wcs/components/formstack-form/fields/input.js +8 -0
- package/src/wcs/components/formstack-form/fields/name.js +39 -0
- package/src/wcs/components/formstack-form/fields/radio.js +24 -0
- package/src/wcs/components/formstack-form/fields/rating.js +53 -0
- package/src/wcs/components/formstack-form/fields/section.js +8 -0
- package/src/wcs/components/formstack-form/fields/select.js +10 -0
- package/src/wcs/components/formstack-form/fields/textarea.js +8 -0
- package/src/wcs/components/formstack-form/fields/wrapper.js +11 -0
- package/src/wcs/components/formstack-form/formstack-form.js +280 -0
- package/src/wcs/components/fuel-prices/fuel-prices.js +45 -0
- package/src/wcs/components/furniture-overview/furniture-overview.js +115 -0
- package/src/wcs/components/gauge-value/gauge-value.js +65 -0
- package/src/wcs/components/help-centre/api.js +150 -0
- package/src/wcs/components/help-centre/help-centre.js +400 -0
- package/src/wcs/components/help-centre/icon-search.svg +1 -0
- package/src/wcs/components/image-gen/admin-panel.js +248 -0
- package/src/wcs/components/image-gen/image-gen.js +385 -0
- package/src/wcs/components/image-gen/labels.js +37 -0
- package/src/wcs/components/image-gen/use-api.js +392 -0
- package/src/wcs/components/inspired-gallery/inspired-gallery.js +118 -0
- package/src/wcs/components/launch-container/launch-container.js +95 -0
- package/src/wcs/components/launch-container/ledger.js +140 -0
- package/src/wcs/components/lng-map/lng-map.js +44 -0
- package/src/wcs/components/mouseflow-analytics/mouseflow-analytics.js +39 -0
- package/src/wcs/components/msds-search/msds-search.js +127 -0
- package/src/wcs/components/msds-search/navigation-search.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-back.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-cart.svg +3 -0
- package/src/wcs/components/product-catalogue/icon-close.svg +3 -0
- package/src/wcs/components/product-catalogue/product-catalogue.js +215 -0
- package/src/wcs/components/product-links/icon-cart.svg +3 -0
- package/src/wcs/components/product-links/product-links.js +43 -0
- package/src/wcs/components/rio-iframe/rio-iframe.js +137 -0
- package/src/wcs/components/salsify-products/filter-tools.js +60 -0
- package/src/wcs/components/salsify-products/icon-cart.svg +3 -0
- package/src/wcs/components/salsify-products/process-products.js +53 -0
- package/src/wcs/components/salsify-products/route-tools.js +54 -0
- package/src/wcs/components/salsify-products/salsify-products.js +281 -0
- package/src/wcs/components/shout-out/shout-out.js +51 -0
- package/src/wcs/components/simple-chart/simple-chart.js +53 -0
- package/src/wcs/components/single-stat/single-stat.js +85 -0
- package/src/wcs/components/site-feedback/site-feedback.js +56 -0
- package/src/wcs/components/skds-search/navigation-search.svg +3 -0
- package/src/wcs/components/skds-search/skds-search.js +103 -0
- package/src/wcs/components/smart-banner/smart-banner.js +104 -0
- package/src/wcs/components/standalone-table/arrow-up-down.svg +3 -0
- package/src/wcs/components/standalone-table/arrow-up.svg +3 -0
- package/src/wcs/components/standalone-table/standalone-table.js +440 -0
- package/src/wcs/components/station-locator/station-locator.js +49 -0
- package/src/wcs/components/store-badges/badges.js +60 -0
- package/src/wcs/components/store-badges/store-badges.js +93 -0
- package/src/wcs/components/topbar-button/person.svg +1 -0
- package/src/wcs/components/topbar-button/topbar-button.js +22 -0
- package/src/wcs/components/universal-gallery/universal-gallery.js +308 -0
- package/src/wcs/components/zendesk-chat/zendesk-chat.js +133 -0
- package/src/wcs/shared/chat-bot/README.md +61 -0
- package/src/wcs/shared/chat-bot/chat-bot.js +216 -0
- package/src/wcs/shared/chat-bot/resources/arrow-next.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/navigation-close.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/person.svg +1 -0
- package/src/wcs/shared/chat-bot/resources/upload.svg +1 -0
- package/src/wcs/shared/filtered-data/README.md +52 -0
- package/src/wcs/shared/filtered-data/fetch-data.js +33 -0
- package/src/wcs/shared/filtered-data/filtered-data.js +337 -0
- package/src/wcs/shared/promo-with-popup/icon-close.svg +3 -0
- package/src/wcs/shared/promo-with-popup/icon-next.svg +3 -0
- package/src/wcs/shared/promo-with-popup/icon-prev.svg +3 -0
- package/src/wcs/shared/promo-with-popup/promo-with-popup.js +93 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import formatTime from '#acinguiux-preact/main/shared/format-time.js'
|
|
2
|
+
import LABELS from './labels.js'
|
|
3
|
+
import { ARROW_NEXT } from '#acinguiux-preact/main/shared/icons.js'
|
|
4
|
+
|
|
5
|
+
const { h, matter, preactHooks } = globalThis.ami
|
|
6
|
+
const { Box, Button, Heading, Input, Link, List, Table, Tabs } = matter
|
|
7
|
+
const { useEffect, useRef, useState } = preactHooks
|
|
8
|
+
|
|
9
|
+
const TEXT_CENTER = 'text-center'
|
|
10
|
+
|
|
11
|
+
function FormInput ({ label, title, ...props }) {
|
|
12
|
+
return h('label',
|
|
13
|
+
h('div', { className: 'block font-bold', title }, label),
|
|
14
|
+
h(Input, {
|
|
15
|
+
type: 'select',
|
|
16
|
+
...props
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function AdminUserForm ({ adminTools }) {
|
|
22
|
+
const adminFormRef = useRef(null)
|
|
23
|
+
const [state, setState] = useState({ isInProgress: false, hasSucceeded: false })
|
|
24
|
+
|
|
25
|
+
const onSubmit = event => {
|
|
26
|
+
event.preventDefault()
|
|
27
|
+
|
|
28
|
+
const form = adminFormRef.current
|
|
29
|
+
const formData = Object.fromEntries(new FormData(form))
|
|
30
|
+
|
|
31
|
+
const toggleForm = setDisabled => {
|
|
32
|
+
for (const element of form.elements) {
|
|
33
|
+
element.disabled = setDisabled
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
toggleForm(true)
|
|
38
|
+
setState({ isInProgress: true, hasSucceeded: false })
|
|
39
|
+
|
|
40
|
+
adminTools.request('user', formData).then(result => {
|
|
41
|
+
setState({ isInProgress: false, hasSucceeded: Boolean(result) })
|
|
42
|
+
toggleForm(false)
|
|
43
|
+
if (result) {
|
|
44
|
+
// Clear email value as it will be typically used only once.
|
|
45
|
+
form.elements['email'].value = ''
|
|
46
|
+
}
|
|
47
|
+
}).catch(error => {
|
|
48
|
+
toggleForm(false)
|
|
49
|
+
console.error(error)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
h('form', {
|
|
55
|
+
ref: adminFormRef,
|
|
56
|
+
onSubmit
|
|
57
|
+
},
|
|
58
|
+
h('div', { className: 'flex sm:flex-col md:flex-col lg:items-end gap-5' },
|
|
59
|
+
h('div', { className: 'grow min-w-1/3' },
|
|
60
|
+
h(FormInput, {
|
|
61
|
+
label: 'Email',
|
|
62
|
+
name: 'email',
|
|
63
|
+
required: true,
|
|
64
|
+
type: 'email'
|
|
65
|
+
})
|
|
66
|
+
),
|
|
67
|
+
h('div', { className: 'grow' },
|
|
68
|
+
h(FormInput, {
|
|
69
|
+
label: 'Action',
|
|
70
|
+
name: 'action',
|
|
71
|
+
children: [
|
|
72
|
+
h('option', { value: 'update' }, 'Update'),
|
|
73
|
+
h('option', { value: 'add' }, 'Add/reactivate'),
|
|
74
|
+
h('option', { value: 'remove' }, 'Remove/deactivate')
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
),
|
|
78
|
+
h('div', { className: 'grow' },
|
|
79
|
+
h(FormInput, {
|
|
80
|
+
label: 'Quota',
|
|
81
|
+
name: 'quota',
|
|
82
|
+
type: 'number',
|
|
83
|
+
min: '0'
|
|
84
|
+
})
|
|
85
|
+
),
|
|
86
|
+
h('div', { className: 'grow' },
|
|
87
|
+
h(FormInput, {
|
|
88
|
+
label: 'Admin status',
|
|
89
|
+
name: 'isAdmin',
|
|
90
|
+
children: [
|
|
91
|
+
h('option', { value: '' }, '(no change)'),
|
|
92
|
+
h('option', { value: '1' }, 'Yes'),
|
|
93
|
+
h('option', { value: '0' }, 'No')
|
|
94
|
+
]
|
|
95
|
+
})
|
|
96
|
+
),
|
|
97
|
+
h('div', { className: 'grow-0' },
|
|
98
|
+
h(Button, {
|
|
99
|
+
type: 'submit',
|
|
100
|
+
_textless: true,
|
|
101
|
+
disabled: state.isInProgress,
|
|
102
|
+
_model: { name: LABELS.GENERATE_BUTTON, icon: ARROW_NEXT }
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
)),
|
|
106
|
+
state.hasSucceeded && h('div', { className: TEXT_CENTER }, 'Action completed successfully.')
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function AdminData ({ adminTools, endpoint, dataPanelCache, headings, excludeFields, fieldHandler }) {
|
|
111
|
+
const [state, setState] = useState({ isLoading: true, data: dataPanelCache?.current[endpoint] || null })
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (state.data) {
|
|
115
|
+
setState(prev => ({ ...prev, isLoading: false }))
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
adminTools.request(endpoint)
|
|
119
|
+
.then(data => {
|
|
120
|
+
setState(prev => ({ ...prev, isLoading: false, data }))
|
|
121
|
+
// Cache data if enabled.
|
|
122
|
+
if (dataPanelCache) {
|
|
123
|
+
dataPanelCache.current[endpoint] = data
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.catch(console.error)
|
|
127
|
+
}, []) // Only a single call.
|
|
128
|
+
|
|
129
|
+
if (state.isLoading) {
|
|
130
|
+
return h('div', { className: TEXT_CENTER }, LABELS.LOADING)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Boolean(state.data) && h(Table, {
|
|
134
|
+
children: [
|
|
135
|
+
h('tr', headings.map((heading, index) =>
|
|
136
|
+
h('th', { key: index }, heading)
|
|
137
|
+
)),
|
|
138
|
+
state.data?.items?.map((item, index) =>
|
|
139
|
+
h('tr', { key: index },
|
|
140
|
+
Object.entries(item).filter(([field]) => !excludeFields?.includes(field)).map(([field, value], cellIndex) =>
|
|
141
|
+
h('td', { key: cellIndex }, fieldHandler(value, field, item))
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function AdminReportLink ({ name, value }) {
|
|
150
|
+
return h(Link, {
|
|
151
|
+
className: 'clickable cursor-pointer hover:underline',
|
|
152
|
+
_model: {
|
|
153
|
+
name,
|
|
154
|
+
value
|
|
155
|
+
},
|
|
156
|
+
onClick: e => {
|
|
157
|
+
e.preventDefault()
|
|
158
|
+
// Bypass external disclaimer.
|
|
159
|
+
window.open(value, '_blank', 'noopener')
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default function AdminPanel ({ adminTools }) {
|
|
165
|
+
const [state, setState] = useState({ userList: false, models: false })
|
|
166
|
+
const dataPanelCache = useRef({})
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const onHashChange = () => {
|
|
170
|
+
const tabStateMap = {
|
|
171
|
+
'tab-user-list': 'userList',
|
|
172
|
+
'tab-models': 'models'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const stateProp = tabStateMap[globalThis.location.hash.substring(1)]
|
|
176
|
+
if (stateProp) {
|
|
177
|
+
setState(prev => ({
|
|
178
|
+
...prev,
|
|
179
|
+
[stateProp]: true
|
|
180
|
+
}))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
globalThis.addEventListener('hashchange', onHashChange)
|
|
184
|
+
globalThis.location.hash && onHashChange()
|
|
185
|
+
|
|
186
|
+
return () => globalThis.removeEventListener('hashchange', onHashChange)
|
|
187
|
+
}, [])
|
|
188
|
+
|
|
189
|
+
return h(Box, { className: 'rounded-2xl bg-bga text-txa' },
|
|
190
|
+
h(Heading, { _level: 2, _size: 4 }, 'Administrator panel'),
|
|
191
|
+
h(Tabs, {
|
|
192
|
+
_model: {
|
|
193
|
+
links: [{ name: 'User management' }, { name: 'User list' }, { name: 'Models' }, { name: 'Reports' }]
|
|
194
|
+
},
|
|
195
|
+
children: [
|
|
196
|
+
h('div', { className: 'space-y-5' }, h(AdminUserForm, { adminTools })),
|
|
197
|
+
h('div', {
|
|
198
|
+
children: state.userList &&
|
|
199
|
+
h(AdminData, {
|
|
200
|
+
adminTools,
|
|
201
|
+
endpoint: 'users',
|
|
202
|
+
dataPanelCache,
|
|
203
|
+
headings: ['Email', 'Quota', 'Usage', 'Prompts', 'Admin'],
|
|
204
|
+
excludeFields: ['isInactive'], // Handled by line-through.
|
|
205
|
+
fieldHandler: (value, field, item) => {
|
|
206
|
+
if (['isInactive', 'isAdmin'].includes(field)) {
|
|
207
|
+
return h('div', { className: TEXT_CENTER }, value ? '+' : '-')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (field === 'email' && item['isInactive']) {
|
|
211
|
+
return h('span', { className: 'line-through' }, value)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return value
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
}),
|
|
218
|
+
h('div', {
|
|
219
|
+
children: state.models &&
|
|
220
|
+
h(AdminData, {
|
|
221
|
+
adminTools,
|
|
222
|
+
endpoint: 'models',
|
|
223
|
+
dataPanelCache,
|
|
224
|
+
headings: ['Name', 'ID', 'Created', 'Modified'],
|
|
225
|
+
fieldHandler: (value, field) => {
|
|
226
|
+
if (['createdDate', 'modifiedDate'].includes(field)) {
|
|
227
|
+
const date = new Date(value)
|
|
228
|
+
return date.getTime() ? formatTime(date, true) : '-'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return value
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}),
|
|
235
|
+
h('div',
|
|
236
|
+
h(List,
|
|
237
|
+
h('li',
|
|
238
|
+
h('div', h(AdminReportLink, { name: 'Quota usage', value: adminTools.getUrl('reports?id=quota-usage') }))
|
|
239
|
+
),
|
|
240
|
+
h('li',
|
|
241
|
+
h('div', h(AdminReportLink, { name: 'User prompts', value: adminTools.getUrl('reports?id=user-prompts') }))
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
]
|
|
246
|
+
})
|
|
247
|
+
)
|
|
248
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
import { ARROW_DOWN, ARROW_NEXT, ASSET_DOWNLOAD } from '#acinguiux-preact/main/shared/icons.js'
|
|
3
|
+
import formatTime from '#acinguiux-preact/main/shared/format-time.js'
|
|
4
|
+
import useApi, { STATUS, LOAD_MORE_STATUS, PROMPT_STATUS } from './use-api.js'
|
|
5
|
+
import AdminPanel from './admin-panel.js'
|
|
6
|
+
import LABELS from './labels.js'
|
|
7
|
+
import { isEdit } from '#acinguiux-preact/main/shared/wcm-mode.js'
|
|
8
|
+
|
|
9
|
+
const { h, register, matter, preactHooks } = globalThis.ami
|
|
10
|
+
const { Box, Button, Icon, Input, Link } = matter
|
|
11
|
+
const { useEffect, useMemo, useRef, useState } = preactHooks
|
|
12
|
+
|
|
13
|
+
const IMAGE_GEN_API_URL = process.env.IMAGE_GEN_API_URL || ''
|
|
14
|
+
const LG_COL_SPAN_4 = 'lg:col-span-4'
|
|
15
|
+
|
|
16
|
+
// Form configuration.
|
|
17
|
+
const PROMPT_MIN_LENGTH = 5
|
|
18
|
+
const PROMPT_MAX_LENGTH = 1024
|
|
19
|
+
const NUM_VARIATIONS = 2
|
|
20
|
+
const VARIATIONS = Array.from({ length: NUM_VARIATIONS }, (_, i) => ({ label: i + 1, value: i + 1 }))
|
|
21
|
+
const SIZES = [
|
|
22
|
+
{ label: '1:1 - 2048x2048 (Square)', value: '2048x2048' },
|
|
23
|
+
{ label: '4:3 - 2304x1792 (Landscape)', value: '2304x1792' },
|
|
24
|
+
{ label: '3:4 - 1792x2304 (Portrait)', value: '1792x2304' },
|
|
25
|
+
{ label: '16:9 - 2688x1536 (Widescreen)', value: '2688x1536' },
|
|
26
|
+
{ label: '9:16 - 1440x2560 (Tall portrait)', value: '1440x2560' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
// Common Link-like button props for accessibility.
|
|
30
|
+
const accessibleLinkButtonProps = {
|
|
31
|
+
role: 'button',
|
|
32
|
+
tabindex: 0,
|
|
33
|
+
onKeyDown: e => {
|
|
34
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
e.stopPropagation()
|
|
37
|
+
e.currentTarget.click()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function PromptForm ({ formRef, submitPrompt, isInProgress, customModels, quotaUsage }) {
|
|
43
|
+
const [state, setState] = useState({ isExpanded: false })
|
|
44
|
+
const formOptionsRef = useRef(null)
|
|
45
|
+
const quotaData = useMemo(() => {
|
|
46
|
+
if (!quotaUsage?.total) {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
const imagesLeft = Math.max(quotaUsage.total - quotaUsage.usage, 0)
|
|
50
|
+
const percentageLeft = Math.min(Math.floor((imagesLeft / quotaUsage.total) * 100), 100)
|
|
51
|
+
return { ...quotaUsage, imagesLeft, percentageLeft }
|
|
52
|
+
}, [quotaUsage])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
// Set default values.
|
|
56
|
+
resetOptions()
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
const onSubmit = event => {
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
|
|
62
|
+
const form = formRef.current
|
|
63
|
+
const prompt = form.text.value.trim() // We internally use "text" to avoid "prompt.prompt" confusion.
|
|
64
|
+
if (!prompt?.length) {
|
|
65
|
+
form.text.value = ''
|
|
66
|
+
form.reportValidity()
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const formData =
|
|
70
|
+
// Filter out empty values to skip these fields entirely, remove text as it will be renamed to prompt.
|
|
71
|
+
Object.fromEntries(
|
|
72
|
+
[...new FormData(form)].filter(([k, v]) => k !== 'text' && Boolean(v?.trim?.()))
|
|
73
|
+
)
|
|
74
|
+
const numVariations = Number(formData.numVariations)
|
|
75
|
+
const [width, height] = formData.size.split('x')
|
|
76
|
+
|
|
77
|
+
form.text.value = ''
|
|
78
|
+
submitPrompt({
|
|
79
|
+
...formData,
|
|
80
|
+
prompt,
|
|
81
|
+
numVariations,
|
|
82
|
+
size: { width, height }
|
|
83
|
+
}, () => {
|
|
84
|
+
if (!form.text.value) {
|
|
85
|
+
// Restore prompt text on error.
|
|
86
|
+
form.text.value = prompt
|
|
87
|
+
}
|
|
88
|
+
}).catch(console.error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const resetOptions = () => {
|
|
92
|
+
const inputs = formOptionsRef.current.querySelectorAll('select, input, textarea')
|
|
93
|
+
for (const input of inputs) {
|
|
94
|
+
if (input.dataset.default) {
|
|
95
|
+
input.value = input.dataset.default
|
|
96
|
+
} else if (input.options) {
|
|
97
|
+
input.selectedIndex = 0
|
|
98
|
+
} else {
|
|
99
|
+
input.value = ''
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return h('form', {
|
|
105
|
+
ref: formRef,
|
|
106
|
+
onSubmit
|
|
107
|
+
},
|
|
108
|
+
h(Box, { className: 'bg-bga rounded-2xl' },
|
|
109
|
+
// Main input.
|
|
110
|
+
h('div', { className: 'flex gap-5' },
|
|
111
|
+
h('div', { className: 'grow' },
|
|
112
|
+
h(Input, {
|
|
113
|
+
type: 'textarea',
|
|
114
|
+
minlength: PROMPT_MIN_LENGTH,
|
|
115
|
+
maxlength: PROMPT_MAX_LENGTH,
|
|
116
|
+
required: true,
|
|
117
|
+
name: 'text',
|
|
118
|
+
placeholder: LABELS.PROMPT_PLACEHOLDER,
|
|
119
|
+
'aria-label': LABELS.PROMPT_PLACEHOLDER,
|
|
120
|
+
onKeyDown: e => {
|
|
121
|
+
if (e.key === 'Enter' && e.ctrlKey) {
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
formRef.current.requestSubmit()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
),
|
|
128
|
+
h('div', { className: 'grow-0' },
|
|
129
|
+
h(Button, {
|
|
130
|
+
type: 'submit',
|
|
131
|
+
_textless: true,
|
|
132
|
+
disabled: isInProgress,
|
|
133
|
+
_model: { name: LABELS.GENERATE_BUTTON, icon: ARROW_NEXT }
|
|
134
|
+
})
|
|
135
|
+
)
|
|
136
|
+
),
|
|
137
|
+
h('div',
|
|
138
|
+
h('div', { className: 'flex gap-5' },
|
|
139
|
+
h('div', { className: 'w-1/2' },
|
|
140
|
+
// Settings toggle.
|
|
141
|
+
h(Link, {
|
|
142
|
+
_variant: 'underline',
|
|
143
|
+
'aria-controls': 'image-gen-prompt-settings',
|
|
144
|
+
'aria-expanded': state.isExpanded,
|
|
145
|
+
className: 'clickable inline-flex items-center gap-2 cursor-pointer',
|
|
146
|
+
onClick: () => setState(prev => ({ ...prev, isExpanded: !prev.isExpanded })),
|
|
147
|
+
...accessibleLinkButtonProps
|
|
148
|
+
},
|
|
149
|
+
h('span', LABELS.NEW_PROMPT_SETTINGS),
|
|
150
|
+
h('span', { className: ` transition-all ${state.isExpanded ? 'rotate-180' : ''}` }, h(Icon, { _size: 'sm' }, ARROW_DOWN)))
|
|
151
|
+
),
|
|
152
|
+
// Quota/usage meter.
|
|
153
|
+
Boolean(quotaData) && h('div', { className: 'w-1/2' },
|
|
154
|
+
h('div', { className: 'text-right', title: `${quotaData.percentageLeft}%` }, `${LABELS.QUOTA_USAGE}: ${quotaData.imagesLeft} / ${quotaData.total}`)
|
|
155
|
+
)
|
|
156
|
+
),
|
|
157
|
+
h('div', { className: `grid lg:grid-cols-12 gap-5 mt-5 animate-expand ${state.isExpanded ? '' : 'hidden'}`, ref: formOptionsRef, id: 'image-gen-prompt-settings' },
|
|
158
|
+
// Settings.
|
|
159
|
+
h('div', { className: LG_COL_SPAN_4 },
|
|
160
|
+
h(PromptFormInput, {
|
|
161
|
+
label: LABELS.VARIATIONS_LABEL,
|
|
162
|
+
name: 'numVariations',
|
|
163
|
+
'data-default': '1',
|
|
164
|
+
children: VARIATIONS.map((option, index) => h('option', { key: index, value: option.value }, option.label))
|
|
165
|
+
})
|
|
166
|
+
),
|
|
167
|
+
h('div', { className: LG_COL_SPAN_4 },
|
|
168
|
+
h(PromptFormInput, {
|
|
169
|
+
label: LABELS.SIZE_LABEL,
|
|
170
|
+
name: 'size',
|
|
171
|
+
children: SIZES.map((option, index) => h('option', { key: index, value: option.value }, option.label))
|
|
172
|
+
})
|
|
173
|
+
),
|
|
174
|
+
(customModels?.length > 0) && h('div', { className: LG_COL_SPAN_4 },
|
|
175
|
+
h(PromptFormInput, {
|
|
176
|
+
label: LABELS.CUSTOM_MODEL_LABEL,
|
|
177
|
+
name: 'customModelId',
|
|
178
|
+
children: customModels.map(({ name, id }, index) => h('option', { key: index, value: id }, name))
|
|
179
|
+
})
|
|
180
|
+
),
|
|
181
|
+
h('div', { className: 'lg:row-start-2 lg:col-span-12' },
|
|
182
|
+
h(PromptFormInput, {
|
|
183
|
+
label: LABELS.NEGATIVE_PROMPT_LABEL,
|
|
184
|
+
type: 'textarea',
|
|
185
|
+
maxlength: PROMPT_MAX_LENGTH,
|
|
186
|
+
name: 'negativePrompt',
|
|
187
|
+
placeholder: LABELS.NEGATIVE_PROMPT_PLACEHOLDER
|
|
188
|
+
})
|
|
189
|
+
),
|
|
190
|
+
h('div', { className: 'lg:row-start-3 lg:col-span-12' },
|
|
191
|
+
h(Link, {
|
|
192
|
+
_model: { name: LABELS.RESTORE_DEFAULTS, value: '#' },
|
|
193
|
+
_variant: 'underline',
|
|
194
|
+
onClick: e => {
|
|
195
|
+
e.preventDefault()
|
|
196
|
+
resetOptions()
|
|
197
|
+
},
|
|
198
|
+
...accessibleLinkButtonProps
|
|
199
|
+
})
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function PromptFormInput ({ label, title, ...props }) {
|
|
207
|
+
return h('label',
|
|
208
|
+
h('div', { className: 'block font-bold', title }, label),
|
|
209
|
+
h(Input, {
|
|
210
|
+
type: 'select',
|
|
211
|
+
...props
|
|
212
|
+
})
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function PromptItem ({ prompt, formRef }) {
|
|
217
|
+
const layoutImageCount = prompt.images?.length || prompt.numVariations || 1
|
|
218
|
+
const getMessage = messages =>
|
|
219
|
+
h('div', { className: `${layoutImageCount < 2 ? LG_COL_SPAN_4 : 'lg:col-span-8'} text-center pt-5 pb-5 space-y-5 self-center` }, messages)
|
|
220
|
+
const text = prompt.text?.trim()
|
|
221
|
+
|
|
222
|
+
const handlers = {
|
|
223
|
+
[PROMPT_STATUS.PENDING]: () => getMessage(LABELS.STATUS_SENDING_REQUEST),
|
|
224
|
+
[PROMPT_STATUS.RUNNING]: () => getMessage(LABELS.STATUS_GENERATING_IMAGES),
|
|
225
|
+
[PROMPT_STATUS.SUCCEEDED]: () =>
|
|
226
|
+
prompt.images.map((image, imageIndex) =>
|
|
227
|
+
image.url && h('div', { className: LG_COL_SPAN_4, key: image.seed },
|
|
228
|
+
h('div', { className: 'relative' },
|
|
229
|
+
// Image.
|
|
230
|
+
h('a', {
|
|
231
|
+
title: LABELS.VIEW_IMAGE_TOOLTIP,
|
|
232
|
+
'aria-label': LABELS.VIEW_IMAGE_TOOLTIP,
|
|
233
|
+
href: image.url,
|
|
234
|
+
target: '_blank',
|
|
235
|
+
rel: 'noopener noreferrer',
|
|
236
|
+
className: 'clickable cursor-pointer block aspect-square',
|
|
237
|
+
}, h('img', {
|
|
238
|
+
className: 'block cursor-pointer w-full h-full object-cover object-center rounded-2xl',
|
|
239
|
+
src: image.url,
|
|
240
|
+
alt: `${text} (${imageIndex + 1})`
|
|
241
|
+
})),
|
|
242
|
+
// Tools.
|
|
243
|
+
image.downloadUrl && h('div', { className: 'inline-flex absolute bottom-4 right-4' },
|
|
244
|
+
h(Button, {
|
|
245
|
+
_variant: 'plain',
|
|
246
|
+
_size: 'xs',
|
|
247
|
+
_textless: true,
|
|
248
|
+
_model: {
|
|
249
|
+
name: LABELS.DOWNLOAD_IMAGE_BUTTON,
|
|
250
|
+
value: image.downloadUrl,
|
|
251
|
+
icon: ASSET_DOWNLOAD
|
|
252
|
+
},
|
|
253
|
+
onClick: e => {
|
|
254
|
+
e.preventDefault()
|
|
255
|
+
if (image.downloadUrl) {
|
|
256
|
+
// Bypass external disclaimer.
|
|
257
|
+
window.open(image.downloadUrl, '_blank', 'noopener')
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
),
|
|
265
|
+
[PROMPT_STATUS.CANCELLED]: () => getMessage(LABELS.STATUS_CANCELLED),
|
|
266
|
+
[PROMPT_STATUS.FAILED]: () => getMessage(LABELS.STATUS_FAILED)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// No images means failed (service download/upload issues).
|
|
270
|
+
const status = ((prompt.status === PROMPT_STATUS.SUCCEEDED) && !prompt.images?.length) ? PROMPT_STATUS.FAILED : prompt.status
|
|
271
|
+
|
|
272
|
+
return handlers[status] &&
|
|
273
|
+
h(Box, { key: prompt.id, className: 'bg-bga rounded-2xl' },
|
|
274
|
+
h('article', { className: 'grid lg:grid-cols-12 grid-flow-row gap-5', tabindex: 0, 'data-prompt-id': prompt.id },
|
|
275
|
+
handlers[status](),
|
|
276
|
+
h('div', { className: `${layoutImageCount < 2 ? 'lg:col-span-8 lg:col-start-5' : 'lg:col-span-4 lg:col-start-9'} break-words space-y-5` },
|
|
277
|
+
// Prompt text - break into lines.
|
|
278
|
+
h('div', `${text || LABELS.PROMPT_TEXT_NOT_PROVIDED}`
|
|
279
|
+
.split('\n')
|
|
280
|
+
.map((line, index) => h('div', { className: 'whitespace-pre-wrap mb-1', key: index }, line))),
|
|
281
|
+
// Timestamp (show invisible if not ready to prevent layout shift).
|
|
282
|
+
h('time', {
|
|
283
|
+
className: `block text-txa/80 ${prompt.initDateTime ? '' : 'invisible'}`,
|
|
284
|
+
datetime: prompt.initDateTime?.toISOString()
|
|
285
|
+
}, `${prompt.initDateTime ? formatTime(prompt.initDateTime, true) : '-'}`),
|
|
286
|
+
// Reuse the prompt.
|
|
287
|
+
(text?.length > 0) && h(Link, {
|
|
288
|
+
_size: 'xs',
|
|
289
|
+
_model: { name: LABELS.REUSE_BUTTON },
|
|
290
|
+
_variant: 'underline',
|
|
291
|
+
onClick: e => {
|
|
292
|
+
e.preventDefault()
|
|
293
|
+
formRef.current.text.value = text
|
|
294
|
+
formRef.current.text.focus()
|
|
295
|
+
},
|
|
296
|
+
...accessibleLinkButtonProps
|
|
297
|
+
})
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function ImageGen ({ 'api-url': apiUrl = IMAGE_GEN_API_URL, models, anonymous: isAnonymous }) {
|
|
304
|
+
// Get models from the attributes, skip invalid entries.
|
|
305
|
+
const customModels = useMemo(() =>
|
|
306
|
+
models?.split(';')
|
|
307
|
+
.map(model => model.split('|').map(s => s?.trim()))
|
|
308
|
+
.map(([name, id = '']) => ({ name, id }))
|
|
309
|
+
, [models])
|
|
310
|
+
|
|
311
|
+
// The main state.
|
|
312
|
+
const [state, setState] = useState({
|
|
313
|
+
status: isEdit ? STATUS.ERROR : STATUS.LOADING,
|
|
314
|
+
statusMessage: isEdit ? LABELS.ERROR_EDIT_MODE : null,
|
|
315
|
+
loadMore: LOAD_MORE_STATUS.NONE,
|
|
316
|
+
lastLoadPromptId: null,
|
|
317
|
+
isAdmin: false
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Get API-related handlers.
|
|
321
|
+
const [prompts, loadPrompts, statusPoll, submitPrompt, quotaUsage, adminTools] = useApi({
|
|
322
|
+
apiUrl,
|
|
323
|
+
isAnonymous,
|
|
324
|
+
setState
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Form element reference.
|
|
328
|
+
const formRef = useRef(null)
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
// Load initial prompts.
|
|
332
|
+
state.status === STATUS.LOADING && loadPrompts().then(async () => {
|
|
333
|
+
// Determine if admin user.
|
|
334
|
+
const userStatus = await adminTools.request('user-status')
|
|
335
|
+
if (userStatus?.isAdmin) {
|
|
336
|
+
setState(prev => ({ ...prev, isAdmin: true }))
|
|
337
|
+
}
|
|
338
|
+
}).catch(console.error)
|
|
339
|
+
}, [state.status, apiUrl, adminTools])
|
|
340
|
+
|
|
341
|
+
useEffect(() => {
|
|
342
|
+
// Start status polling on prompt list update.
|
|
343
|
+
prompts?.length && statusPoll().catch(console.error)
|
|
344
|
+
|
|
345
|
+
// Focus on the newly loaded prompt after load more.
|
|
346
|
+
if (state.lastLoadPromptId) {
|
|
347
|
+
formRef.current.getRootNode().querySelectorAll(`[data-prompt-id="${state.lastLoadPromptId}"]`)[0]?.focus({ preventScroll: true })
|
|
348
|
+
setState(prev => ({ ...prev, lastLoadPromptId: null }))
|
|
349
|
+
}
|
|
350
|
+
}, [prompts])
|
|
351
|
+
|
|
352
|
+
// General component status.
|
|
353
|
+
if ([STATUS.LOADING, STATUS.ERROR].includes(state.status)) {
|
|
354
|
+
return h(Box, { className: 'rounded-2xl bg-bga text-txa text-center' },
|
|
355
|
+
state.status === STATUS.ERROR ? state.statusMessage : LABELS.LOADING
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return h('div', { className: 'text-txa space-y-5' },
|
|
360
|
+
state.isAdmin && h(AdminPanel, { adminTools }),
|
|
361
|
+
// Prompt form.
|
|
362
|
+
h(PromptForm, { formRef, submitPrompt, isInProgress: state.status === STATUS.PROCESSING, customModels, quotaUsage }),
|
|
363
|
+
|
|
364
|
+
// Prompt list.
|
|
365
|
+
(prompts?.length > 0) && h('div', { className: 'space-y-5' },
|
|
366
|
+
prompts.map(
|
|
367
|
+
prompt => h(PromptItem, { key: prompt.id || `new-${prompt.newItemId}`, prompt, formRef })
|
|
368
|
+
),
|
|
369
|
+
// Load more button.
|
|
370
|
+
([LOAD_MORE_STATUS.ACTIVE, LOAD_MORE_STATUS.IN_PROGRESS].includes(state.loadMore)) &&
|
|
371
|
+
h('div', { className: 'flex items-center justify-center min-h-12' },
|
|
372
|
+
state.loadMore === LOAD_MORE_STATUS.IN_PROGRESS
|
|
373
|
+
? h('span', LABELS.LOADING)
|
|
374
|
+
: h(Button, {
|
|
375
|
+
onClick: () => {
|
|
376
|
+
loadPrompts().catch(console.error)
|
|
377
|
+
},
|
|
378
|
+
_model: { name: LABELS.LOAD_MORE_BUTTON }
|
|
379
|
+
})
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
register(ImageGen, 'image-gen')
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
// Component general state.
|
|
3
|
+
LOADING: 'Loading...',
|
|
4
|
+
ERROR_EDIT_MODE: 'This web component is not available in Edit mode.',
|
|
5
|
+
ERROR_UNAUTHORISED: 'Access restricted: this tool is currently available to pilot users only. Want access? Request via ServiceNow with your business justification and line of business.',
|
|
6
|
+
ERROR_LOGGED_OFF: 'Authentication failed. Please reload the page, you have probably been logged off.',
|
|
7
|
+
ERROR: 'Error connecting to the service. Please try to reload the page.',
|
|
8
|
+
|
|
9
|
+
// Form validation, labels and placeholders.
|
|
10
|
+
PROMPT_PLACEHOLDER: 'Describe your image idea in detail - include all key elements for best results (max 1024 characters)',
|
|
11
|
+
GENERATE_BUTTON: 'Generate (Ctrl+Enter)',
|
|
12
|
+
NEW_PROMPT_SETTINGS: 'New prompt settings',
|
|
13
|
+
QUOTA_USAGE: 'Images left',
|
|
14
|
+
|
|
15
|
+
// Form field labels.
|
|
16
|
+
VARIATIONS_LABEL: 'Variations',
|
|
17
|
+
SIZE_LABEL: 'Size',
|
|
18
|
+
NEGATIVE_PROMPT_LABEL: 'Negative prompt',
|
|
19
|
+
NEGATIVE_PROMPT_PLACEHOLDER: 'Specify elements to exclude from the image (max 1024 characters)',
|
|
20
|
+
CUSTOM_MODEL_LABEL: 'Model',
|
|
21
|
+
CUSTOM_MODEL_DEFAULT: 'Standard',
|
|
22
|
+
RESTORE_DEFAULTS: 'Restore defaults',
|
|
23
|
+
|
|
24
|
+
// Prompt status messages.
|
|
25
|
+
STATUS_SENDING_REQUEST: 'Sending request...',
|
|
26
|
+
STATUS_GENERATING_IMAGES: 'Generating images (it may take up to ~1 minute)...',
|
|
27
|
+
STATUS_CANCELLED: 'Cancelled',
|
|
28
|
+
STATUS_FAILED: 'Failed',
|
|
29
|
+
PROMPT_TEXT_NOT_PROVIDED: '[ Error: failed to retrieve prompt text ]',
|
|
30
|
+
|
|
31
|
+
// Action labels.
|
|
32
|
+
DISMISS_BUTTON: 'Dismiss',
|
|
33
|
+
VIEW_IMAGE_TOOLTIP: 'View image in a new tab',
|
|
34
|
+
DOWNLOAD_IMAGE_BUTTON: 'Download the image',
|
|
35
|
+
REUSE_BUTTON: 'Reuse',
|
|
36
|
+
LOAD_MORE_BUTTON: 'Load more'
|
|
37
|
+
}
|