diff --git a/express/blocks/gen-ai-cards/gen-ai-cards.css b/express/blocks/gen-ai-cards/gen-ai-cards.css
new file mode 100644
index 0000000..8b1204e
--- /dev/null
+++ b/express/blocks/gen-ai-cards/gen-ai-cards.css
@@ -0,0 +1,213 @@
+.section .gen-ai-cards-wrapper {
+ max-width: none;
+}
+
+.gen-ai-cards {
+ padding: 0 28px;
+ max-width: 1440px;
+ margin: auto;
+}
+
+.gen-ai-cards .gen-ai-cards-heading-section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: space-between;
+ margin: auto auto 32px;
+}
+
+.gen-ai-cards .gen-ai-cards-heading-section h2 {
+ text-align: left;
+ max-width: 800px;
+ margin-bottom: 8px;
+ font-size: var(--heading-font-size-m);
+}
+.gen-ai-cards .gen-ai-cards-heading-section p {
+ text-align: left;
+ margin: 0;
+ max-width: 800px;
+}
+
+.gen-ai-cards .gen-ai-cards-heading-section .gen-ai-cards-link {
+ font-size: var(--body-font-size-s);
+ line-height: 22px;
+ color: var(--body-color);
+ text-decoration: underline;
+}
+
+.gen-ai-cards .carousel-container .carousel-platform {
+ align-items: unset;
+}
+
+.gen-ai-cards .card {
+ position: relative;
+ height: 347px;
+ width: 406px;
+ border-radius: 8px;
+ overflow: hidden;
+ margin: 0 8px;
+ background-color: #f4f4fd;
+}
+
+.gen-ai-cards .carousel-left-trigger ~ .card {
+ margin-left: 0;
+}
+
+.gen-ai-cards .card .text-wrapper {
+ text-align: left;
+ margin: 24px 24px 16px 24px;
+}
+
+.gen-ai-cards .card .text-wrapper p {
+ margin: 0;
+}
+
+.gen-ai-cards .card .text-wrapper p.cta-card-title {
+ font-size: var(--body-font-size-m);
+ line-height: var(--body-font-size-m);
+ font-weight: 700;
+}
+
+.gen-ai-cards .card .text-wrapper .tag {
+ font-size: 10px;
+ padding: 0 4px;
+ margin-left: 6px;
+ font-weight: 700;
+ background-color: #DEDEF9;
+ color: var(--color-info-accent);
+ border-radius: 4px;
+}
+
+.gen-ai-cards .card .text-wrapper p.cta-card-desc {
+ font-size: var(--body-font-size-s);
+ line-height: var(--body-font-size-l);
+}
+
+.gen-ai-cards .card .media-wrapper {
+ position: absolute;
+ border-radius: 16px;
+ left: 50%;
+ bottom: 16px;
+ transform: translateX(-50%);
+ width: 374px;
+ height: 246px;
+ overflow: hidden;
+}
+
+.gen-ai-cards .card .media-wrapper picture img {
+ display: block;
+ height: 100%;
+ object-fit: cover;
+}
+
+.gen-ai-cards .card .gen-ai-input-form {
+ position: absolute;
+ width: 352px;
+ height: 55px;
+ padding: 8px;
+ background-color: var(--color-white);
+ border-radius: 8px;
+ box-sizing: border-box;
+ border: 2px solid var(--color-info-accent);
+ left: 50%;
+ top: 100%;
+ transform: translate(-50%, calc(-100% - 30px));
+ display: flex;
+ justify-content: space-between;
+}
+
+.gen-ai-cards .card .gen-ai-input-form input {
+ box-sizing: border-box;
+ border-radius: 8px;
+ border: none;
+ font-family: var(--body-font-family);
+ font-size: var(--body-font-size-s);
+ resize: none;
+ width: 230px;
+}
+
+.gen-ai-cards .gen-ai-input:active,
+.gen-ai-cards .gen-ai-input:focus-visible {
+ border: none;
+ outline: none;
+}
+
+.gen-ai-cards .card .gen-ai-input-form input::placeholder {
+ font-style: italic;
+ font-family: var(--body-font-family);
+ font-size: var(--body-font-size-s);
+}
+
+.gen-ai-cards .card .gen-ai-input-form input:placeholder-shown {
+ text-overflow: ellipsis;
+}
+
+.gen-ai-cards .card .gen-ai-input-form button:disabled {
+ pointer-events: none;
+}
+
+.gen-ai-cards .card .gen-ai-input-form .gen-ai-submit {
+ color: var(--color-white);
+ background-color: var(--color-info-accent);
+ border-style: none;
+ font-family: var(--body-font-family);
+ font-size: var(--body-font-size-s);
+ line-height: var(--body-font-size-s);
+ font-weight: 700;
+ padding: 10px 1.5em 10px 1.5em;
+ border-radius: 22px;
+ cursor: pointer;
+ transition: background-color .2s;
+}
+
+.gen-ai-cards .card.gen-ai-action .gen-ai-input-form .gen-ai-submit {
+ left: 56px;
+ bottom: 24px;
+}
+
+.gen-ai-cards .gen-ai-input-form .gen-ai-submit:not(:disabled):hover {
+ background-color: var(--color-info-accent-hover);
+}
+
+.gen-ai-cards .card .gen-ai-input-form .gen-ai-submit:disabled {
+ background-color: var(--color-gray-200);
+ color: var(--color-gray-500);
+}
+
+.gen-ai-cards .card .links-wrapper {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ left: 50%;
+ bottom: 40px;
+ transform: translate(-50%, 0);
+}
+
+.gen-ai-cards .card .links-wrapper a {
+ margin: 4px auto;
+ width: max-content;
+ color: var(--body-color);
+ border: 0;
+}
+
+.gen-ai-cards .card .links-wrapper a:hover {
+ /* 90% white to align with products */
+ background-color: #e6e6e6;
+}
+
+.gen-ai-cards .card .links-wrapper a:not(:hover) {
+ background-color: var(--color-white);
+}
+
+@media (min-width: 900px) {
+ .gen-ai-cards .gen-ai-cards-heading-section {
+ flex-direction: row;
+ align-items: flex-end;
+ }
+
+ .gen-ai-cards .carousel-container .carousel-fader-left,
+ .gen-ai-cards .carousel-container .carousel-fader-right{
+ background: unset;
+ }
+}
diff --git a/express/blocks/gen-ai-cards/gen-ai-cards.js b/express/blocks/gen-ai-cards/gen-ai-cards.js
new file mode 100644
index 0000000..4267184
--- /dev/null
+++ b/express/blocks/gen-ai-cards/gen-ai-cards.js
@@ -0,0 +1,226 @@
+import { getLibs } from '../../scripts/utils.js';
+import { addTempWrapperDeprecated } from '../../scripts/utils/decorate.js';
+import buildCarousel from '../../scripts/widgets/carousel.js';
+
+const { createTag, getConfig } = await import(`${getLibs()}/utils/utils.js`);
+const promptTokenRegex = /(?:\{\{|%7B%7B)?prompt(?:-|\+|%20|\s)text(?:\}\}|%7D%7D)?/;
+
+export function decorateTextWithTag(textSource, options = {}) {
+ const {
+ baseT,
+ tagT,
+ baseClass,
+ tagClass,
+ } = options;
+ const text = createTag(baseT || 'p', { class: baseClass || '' });
+ const tagText = textSource.match(/\[(.*?)]/);
+
+ if (tagText) {
+ const [fullText, tagTextContent] = tagText;
+ const $tag = createTag(tagT || 'span', { class: tagClass || 'tag' });
+ text.textContent = textSource.replace(fullText, '').trim();
+ text.dataset.text = text.textContent.toLowerCase();
+ $tag.textContent = tagTextContent;
+ text.append($tag);
+ } else {
+ text.textContent = textSource;
+ text.dataset.text = text.textContent.toLowerCase();
+ }
+ return text;
+}
+
+export function decorateHeading(block, payload) {
+ const headingSection = createTag('div', { class: 'gen-ai-cards-heading-section' });
+ const headingTextWrapper = createTag('div', { class: 'text-wrapper' });
+ const heading = createTag('h2', { class: 'gen-ai-cards-heading' });
+
+ heading.textContent = payload.heading;
+ headingSection.append(headingTextWrapper);
+ headingTextWrapper.append(heading);
+
+ if (payload.subHeadings.length > 0) {
+ payload.subHeadings.forEach((p) => {
+ headingTextWrapper.append(p);
+ });
+ }
+
+ if (payload.legalLink.href !== '') {
+ const legalButton = createTag('a', {
+ class: 'gen-ai-cards-link',
+ href: payload.legalLink.href,
+ });
+ legalButton.textContent = payload.legalLink.text;
+ headingSection.append(legalButton);
+ }
+
+ block.append(headingSection);
+}
+
+export const windowHelper = {
+ redirect: (url) => {
+ window.location.assign(url);
+ },
+};
+
+function handleGenAISubmit(form, link) {
+ const input = form.querySelector('input');
+ if (input.value.trim() === '') return;
+ const genAILink = link.replace(promptTokenRegex, encodeURI(input.value).replaceAll(' ', '+'));
+ if (genAILink) windowHelper.redirect(genAILink);
+}
+
+function buildGenAIForm({ ctaLinks, subtext }) {
+ const genAIForm = createTag('form', { class: 'gen-ai-input-form' });
+ const genAIInput = createTag('input', {
+ placeholder: subtext || '',
+ type: 'text',
+ enterKeyhint: 'enter',
+ });
+ const genAISubmit = createTag('button', {
+ class: 'gen-ai-submit',
+ type: 'submit',
+ disabled: true,
+ });
+
+ genAIForm.append(genAIInput, genAISubmit);
+
+ genAISubmit.textContent = ctaLinks[0].textContent;
+ genAISubmit.disabled = genAIInput.value === '';
+
+ genAIInput.addEventListener('input', () => {
+ genAISubmit.disabled = genAIInput.value.trim() === '';
+ });
+
+ genAIInput.addEventListener('keyup', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleGenAISubmit(genAIForm, ctaLinks[0].href);
+ }
+ });
+
+ genAIForm.addEventListener('submit', (e) => {
+ e.preventDefault();
+ handleGenAISubmit(genAIForm, ctaLinks[0].href);
+ });
+
+ return genAIForm;
+}
+
+function removeLazyAfterNeighborLoaded(image, lastImage) {
+ if (!image || !lastImage) return;
+ lastImage.onload = (e) => {
+ if (e.eventPhase >= Event.AT_TARGET) {
+ image.querySelector('img').removeAttribute('loading');
+ }
+ };
+}
+
+async function decorateCards(block, { actions }) {
+ const cards = createTag('div', { class: 'gen-ai-cards-cards' });
+ let searchBranchLinks;
+
+ await import(`${getLibs()}/features/placeholders.js`).then(async (mod) => {
+ searchBranchLinks = await mod.replaceKey('search-branch-links', getConfig());
+ return mod.replaceKey();
+ });
+
+ actions.forEach((cta, i) => {
+ const {
+ image,
+ ctaLinks,
+ text,
+ title,
+ } = cta;
+ const card = createTag('div', { class: 'card' });
+ const linksWrapper = createTag('div', { class: 'links-wrapper' });
+ const mediaWrapper = createTag('div', { class: 'media-wrapper' });
+ const textWrapper = createTag('div', { class: 'text-wrapper' });
+
+ card.append(textWrapper, mediaWrapper, linksWrapper);
+ if (image) {
+ mediaWrapper.append(image);
+ if (i > 0) {
+ const lastImage = actions[i - 1].image?.querySelector('img');
+ removeLazyAfterNeighborLoaded(image, lastImage);
+ }
+ }
+
+ const hasGenAIForm = promptTokenRegex.test(ctaLinks?.[0]?.href);
+
+ if (ctaLinks.length > 0) {
+ if (hasGenAIForm) {
+ const genAIForm = buildGenAIForm(cta);
+ card.classList.add('gen-ai-action');
+ card.append(genAIForm);
+ linksWrapper.remove();
+ } else {
+ const a = ctaLinks[0];
+ const btnUrl = new URL(a.href);
+ if (searchBranchLinks?.replace(/\s/g, '').split(',').includes(`${btnUrl.origin}${btnUrl.pathname}`)) {
+ btnUrl.searchParams.set('q', cta.text);
+ btnUrl.searchParams.set('category', 'templates');
+ a.href = decodeURIComponent(btnUrl.toString());
+ }
+ a.classList.add('con-button');
+ a.removeAttribute('title');
+ linksWrapper.append(a);
+ }
+ }
+
+ const titleText = decorateTextWithTag(title, { tagT: 'sup', baseClass: 'cta-card-title' });
+ textWrapper.append(titleText);
+ const desc = createTag('p', { class: 'cta-card-desc' });
+ desc.textContent = text;
+ textWrapper.append(desc);
+
+ cards.append(card);
+ });
+
+ block.append(cards);
+}
+
+function constructPayload(block) {
+ const rows = Array.from(block.children);
+ block.innerHTML = '';
+ const headingDiv = rows.shift();
+
+ const payload = {
+ heading: headingDiv.querySelector('h2, h3, h4, h5, h6')?.textContent?.trim(),
+ subHeadings: headingDiv.querySelectorAll('p:not(.button-container, :has(a.con-button, a[href*="legal"]))'),
+ legalLink: {
+ text: headingDiv.querySelector('a[href*="legal"]')?.textContent?.trim(),
+ href: headingDiv.querySelector('a[href*="legal"]')?.href,
+ },
+ actions: [],
+ };
+
+ rows.forEach((row) => {
+ const ctaObj = {
+ image: row.querySelector(':scope > div:nth-of-type(1) picture'),
+ videoLink: row.querySelector(':scope > div:nth-of-type(1) a'),
+ title: row.querySelector(':scope > div:nth-of-type(2) p:nth-of-type(2):not(.button-container) strong')?.textContent.trim(),
+ text: row.querySelector(':scope > div:nth-of-type(2) p:not(.button-container):not(:has(strong)):not(:has(em)):not(:empty)')?.textContent.trim(),
+ subtext: row.querySelector(':scope > div:nth-of-type(2) p:not(.button-container) em')?.textContent.trim(),
+ ctaLinks: row.querySelectorAll(':scope > div:nth-of-type(2) a'),
+ };
+
+ payload.actions.push(ctaObj);
+ });
+
+ return payload;
+}
+
+export default async function decorate(block) {
+ addTempWrapperDeprecated(block, 'gen-ai-cards');
+ const links = block.querySelectorAll(':scope a[href*="adobesparkpost"]');
+
+ if (links) {
+ const linksPopulated = new CustomEvent('linkspopulated', { detail: links });
+ document.dispatchEvent(linksPopulated);
+ }
+
+ const payload = constructPayload(block);
+ decorateHeading(block, payload);
+ await decorateCards(block, payload);
+ await buildCarousel('', block.querySelector('.gen-ai-cards-cards'));
+}
diff --git a/express/scripts/branchlinks.js b/express/scripts/branchlinks.js
index 9020c00..1307a34 100644
--- a/express/scripts/branchlinks.js
+++ b/express/scripts/branchlinks.js
@@ -56,6 +56,7 @@ export default async function trackBranchParameters(links) {
const listBranchMetadataNodes = [...document.head.querySelectorAll('meta[name^=branch-]')];
const listAdditionalBranchMetadataNodes = listBranchMetadataNodes.filter((e) => !setBasicBranchMetadata.has(e.name.replace(/^branch-/, '')));
+ const searchBranchLinks = await placeholderMod.replaceKey('search-branch-links', getConfig());
const [
searchTerm,
@@ -103,7 +104,6 @@ export default async function trackBranchParameters(links) {
params.get('cgen'),
];
- const promises = [];
links.forEach((a) => {
if (a.href && a.href.match(/adobesparkpost(-web)?\.app\.link/)) {
a.rel = 'nofollow';
@@ -120,29 +120,26 @@ export default async function trackBranchParameters(links) {
}
const placement = getPlacement(a);
- const prom = placeholderMod.replaceKey('search-branch-links', getConfig()).then((searchBranchLink) => {
- const isSearchBranchLink = searchBranchLink?.replace(/\s/g, '').split(',').includes(`${btnUrl.origin}${btnUrl.pathname}`);
- if (isSearchBranchLink) {
- setParams('category', category || 'templates');
- setParams('taskID', taskID);
- setParams('assetCollection', assetCollection);
- setParams('height', canvasHeight);
- setParams('width', canvasWidth);
- setParams('unit', canvasUnit);
- setParams('sceneline', sceneline);
-
- if (searchCategory) {
- setParams('searchCategory', searchCategory);
- } else if (searchTerm) {
- setParams('q', searchTerm);
- }
- if (loadPrintAddon) setParams('loadPrintAddon', loadPrintAddon);
- setParams('tab', tab);
- setParams('action', action);
- setParams('prompt', prompt);
+ const isSearchBranchLink = searchBranchLinks?.replace(/\s/g, '').split(',').includes(`${btnUrl.origin}${btnUrl.pathname}`);
+ if (isSearchBranchLink) {
+ setParams('category', category || 'templates');
+ setParams('taskID', taskID);
+ setParams('assetCollection', assetCollection);
+ setParams('height', canvasHeight);
+ setParams('width', canvasWidth);
+ setParams('unit', canvasUnit);
+ setParams('sceneline', sceneline);
+
+ if (searchCategory) {
+ setParams('searchCategory', searchCategory);
+ } else if (searchTerm) {
+ setParams('q', searchTerm);
}
- });
- promises.push(prom);
+ if (loadPrintAddon) setParams('loadPrintAddon', loadPrintAddon);
+ setParams('tab', tab);
+ setParams('action', action);
+ setParams('prompt', prompt);
+ }
for (const { name, content } of listAdditionalBranchMetadataNodes) {
const paramName = toCamelCase(name.replace(/^branch-/, ''));
@@ -179,5 +176,4 @@ export default async function trackBranchParameters(links) {
a.href = decodeURIComponent(btnUrl.toString());
}
});
- await Promise.all(promises);
}
diff --git a/express/scripts/utils/content-replace.js b/express/scripts/utils/content-replace.js
index ef51285..1d8a6b5 100644
--- a/express/scripts/utils/content-replace.js
+++ b/express/scripts/utils/content-replace.js
@@ -289,7 +289,7 @@ async function sanitizeMeta(meta) {
// metadata -> dom blades
async function autoUpdatePage(main) {
- const wl = ['{{heading_placeholder}}', '{{type}}', '{{quantity}}'];
+ const wl = ['{{heading_placeholder}}', '{{type}}', '{{quantity}}', '{{prompt-text}}'];
// FIXME: deprecate wl
if (!main) return;
diff --git a/test/blocks/gen-ai-cards/gen-ai-cards.test.js b/test/blocks/gen-ai-cards/gen-ai-cards.test.js
new file mode 100644
index 0000000..025953b
--- /dev/null
+++ b/test/blocks/gen-ai-cards/gen-ai-cards.test.js
@@ -0,0 +1,114 @@
+import { readFile } from '@web/test-runner-commands';
+import sinon from 'sinon';
+import { expect } from '@esm-bundle/chai';
+
+const imports = await Promise.all([
+ import('../../../express/scripts/scripts.js'),
+ import('../../../express/blocks/gen-ai-cards/gen-ai-cards.js'),
+]);
+
+const { default: decorate, windowHelper } = imports[1];
+const testBody = await readFile({ path: './mocks/body.html' });
+
+describe('Gen AI Cards', () => {
+ let blocks;
+ before(async () => {
+ window.isTestEnv = true;
+ document.body.innerHTML = testBody;
+ window.placeholders = { 'search-branch-links': 'https://adobesparkpost.app.link/c4bWARQhWAb' };
+ blocks = [...document.querySelectorAll('.gen-ai-cards')];
+ await Promise.all(blocks.map((bl) => decorate(bl)));
+ });
+ afterEach(() => {
+ window.placeholders = undefined;
+ });
+
+ it('should have all things', async () => {
+ for (const block of blocks) {
+ expect(block).to.exist;
+ expect(block.querySelector('.gen-ai-cards-heading-section')).to.exist;
+ const cards = block.querySelector('.carousel-container .carousel-platform');
+ expect(cards).to.exist;
+ expect(cards.querySelectorAll('.card').length).to.equal(5);
+ expect(cards.querySelector('.card').classList.contains('gen-ai-action')).to.be.true;
+ expect(cards.querySelectorAll('.card')[1].classList.contains('gen-ai-action')).to.be.false;
+ expect(cards.querySelectorAll('.card')[2].classList.contains('gen-ai-action')).to.be.true;
+ expect(cards.querySelectorAll('.card')[3].classList.contains('gen-ai-action')).to.be.true;
+ }
+ });
+
+ it('should have all cards with proper children', async () => {
+ for (const block of blocks) {
+ const cards = block.querySelector('.carousel-container .carousel-platform');
+ for (const card of cards.querySelectorAll('.card')) {
+ expect(card.querySelector('.text-wrapper .cta-card-desc')).to.exist;
+ expect(card.querySelector('.text-wrapper .cta-card-title')).to.exist;
+ expect(card.querySelector('.media-wrapper picture')).to.exist;
+ }
+ for (const card of cards.querySelectorAll('.card:not(.gen-ai-action)')) {
+ const cta = card.querySelector('.links-wrapper a');
+ expect(cta).to.exist;
+ expect(cta.textContent).to.exist;
+ expect(cta.href).to.exist;
+ }
+ for (const card of cards.querySelectorAll('.card.gen-ai-action')) {
+ const form = card.querySelector('.gen-ai-input-form');
+ expect(form).to.exist;
+ const input = form.querySelector('input');
+ const button = form.querySelector('button');
+ expect(input).to.exist;
+ expect(button).to.exist;
+ expect(input.placeholder).to.exist;
+ expect(button.textContent).to.exist;
+ expect(button.classList.contains('gen-ai-submit')).to.be.true;
+ }
+ }
+ });
+
+ function decodeHTMLEntities(text) {
+ const textArea = document.createElement('textarea');
+ textArea.innerHTML = text;
+ return textArea.value;
+ }
+
+ it('should toggle submit button disabled based on input content', async () => {
+ for (const block of blocks) {
+ const cards = block.querySelector('.carousel-container .carousel-platform');
+ const card = cards.querySelector('.card.gen-ai-action');
+ const form = card.querySelector('.gen-ai-input-form');
+ const input = form.querySelector('input');
+ const button = form.querySelector('button');
+ const stub = sinon.stub(windowHelper, 'redirect');
+ expect(button.disabled).to.be.true;
+ const enterEvent = new KeyboardEvent('keyup', {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ which: 13,
+ bubbles: true,
+ cancelable: true,
+ });
+ input.dispatchEvent(enterEvent);
+ expect(button.disabled).to.be.true;
+ expect(stub.called).to.be.false;
+ form.dispatchEvent(new Event('submit'));
+ expect(stub.called).to.be.false;
+
+ input.value = 'test';
+ input.dispatchEvent(new Event('input'));
+ expect(button.disabled).to.be.false;
+
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(button.disabled).to.be.true;
+ input.value = 'fakeInput';
+ input.dispatchEvent(enterEvent);
+ expect(stub.called).to.be.true;
+ expect(decodeHTMLEntities(stub.firstCall.args[0])).to.equal(
+ decodeHTMLEntities('https://new.express.adobe.com/new?category=media&prompt=fakeInput&action=text+to+image&width=1080&height=1080'),
+ );
+
+ stub.restore();
+ }
+ });
+});
diff --git a/test/blocks/gen-ai-cards/mocks/body.html b/test/blocks/gen-ai-cards/mocks/body.html
new file mode 100644
index 0000000..88c8d07
--- /dev/null
+++ b/test/blocks/gen-ai-cards/mocks/body.html
@@ -0,0 +1,191 @@
+
+
+
+
+ This is a good one.
+
+
+
+
+
+
+
+
+
This is a good one.
+
Unlock your imagination with generative AI. Experiment, imagine, and create an infinite range of content in Adobe Express with generative AI features.
+
Adobe Generative AI Terms
+
+
+
+
+
+
+
+
Generate
+
Text to image
+
Generate images from a detailed text description.
+
Try describing people, places, and things
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Generate
+
Text to template
+
Add or remove elements with a text prompt.
+
Describe the template you want to make
+
+
+
+
+
+
+
+
Generate
+
Text effects
+
Apply styles or textures to text with a text prompt.
+
Describe the text effect you want to make
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This is a good one.
+
Unlock your imagination with generative AI. Experiment, imagine, and create an infinite range of content in Adobe Express with generative AI features.
+
Adobe Generative AI Terms
+
+
+
+
+
+
+
+
Generate
+
Text to image
+
Generate images from a detailed text description.
+
Try describing people, places, and things
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Generate
+
Text to template
+
Add or remove elements with a text prompt.
+
Describe the template you want to make
+
+
+
+
+
+
+
+
Generate
+
Text effects
+
Apply styles or textures to text with a text prompt.
+
Describe the text effect you want to make
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+