Skip to content

Commit

Permalink
feat:Experience Shell integration (#43)
Browse files Browse the repository at this point in the history
* Initial integration with Experience Shell.
* integration w Shell Auth
* Code cleanup for ShellAuthProvider.js
* Minor changes to comments.
* Implemented Unified Shell 'Settings' API for consent.
* Show 'Access Denied' dialog if user doesn't consent to the legal terms.
* Uncommented getWebsiteUrlFromReferrer().
* Code cleanup.
* Renamed 'config' to 'shellConfig' to avoid confusion with app config.
* Fixed ApplicationProvider.js to include revised logic from 'main'.
* chore: generate package-lock.json and code cleanup.
* chore: code cleanup.
* Changes to pass the imsOrg from the client and revise authorization logic.

---------
Co-authored-by: Dragos Dascalita Haut <[email protected]>
  • Loading branch information
askayastha22 authored Nov 21, 2023
1 parent fb6e6fc commit e50e785
Show file tree
Hide file tree
Showing 13 changed files with 3,360 additions and 4,296 deletions.
56 changes: 10 additions & 46 deletions actions/AuthAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function isValidToken(endpoint, clientId, token, logger) {
});
}

async function getImsOrgForProductContext(endpoint, clientId, token, productContext, logger) {
async function checkForProductContext(endpoint, clientId, org, token, productContext, logger) {
return wretchRetry(`${endpoint}/ims/profile/v1`)
.addon(QueryStringAddon).query({
client_id: clientId,
Expand All @@ -51,51 +51,16 @@ async function getImsOrgForProductContext(endpoint, clientId, token, productCont
const filteredProductContext = json.projectedProductContext
.filter((obj) => obj.prodCtx.serviceCode === productContext);

// Case 1: No product context found
if (filteredProductContext.length === 0) {
return '';
}

// Case 2: Exactly one product context found
if (filteredProductContext.length === 1) {
return filteredProductContext[0].prodCtx.owningEntity;
}

// Case 3: Multiple product contexts found
if (filteredProductContext.length > 1) {
return wretchRetry(`${endpoint}/ims/organizations/v6`)
.headers({
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
})
.get()
.json()
// eslint-disable-next-line consistent-return
.then((imsOrgsList) => {
if (Array.isArray(imsOrgsList)) {
// Case 3a: Exactly one IMS Org found in the profile
if (imsOrgsList.length === 1) {
const { ident: orgIdent, authSrc: orgAuthSrc } = imsOrgsList[0].orgRef;
return `${orgIdent}@${orgAuthSrc}`;

// Case 3b: More than one IMS Org found in the profile
} else if (imsOrgsList.length > 1) {
logger.warn(`Multiple IMS Orgs found in the profile with ${productContext}. Returning the first one.`);
return filteredProductContext[0].prodCtx.owningEntity;
}
}
})
.catch((error) => {
logger.error(error);
return '';
});
}
// For each entry in filteredProductContext check that
// there is at least one entry where imsOrg matches the owningEntity property
// otherwise, if no match, the user is not authorized
return filteredProductContext.some((obj) => obj.prodCtx.owningEntity === org);
}
return '';
return false;
})
.catch((error) => {
logger.error(error);
return '';
return false;
});
}

Expand All @@ -110,16 +75,15 @@ function asAuthAction(action) {
const productContext = params.IMS_PRODUCT_CONTEXT;

// Extract the token from the params
const { accessToken } = params;
const { imsOrg, accessToken } = params;

// Validate the access token
if (!await isValidToken(imsEndpoint, clientId, accessToken, logger)) {
throw new Error('Access token is invalid');
}

// Check that the profile has expected product context and retrieve the IMS Org
const imsOrg = await getImsOrgForProductContext(imsEndpoint, clientId, accessToken, productContext, logger);
if (imsOrg === '') {
// Check that the profile has the expected product context
if (!await checkForProductContext(imsEndpoint, clientId, imsOrg, accessToken, productContext, logger)) {
throw new Error('Profile does not have the required product context');
}

Expand Down
7,317 changes: 3,134 additions & 4,183 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
},
"dependencies": {
"@adobe/aio-sdk": "3.0.0",
"@adobe/exc-app": "^1.2.10",
"@adobe/generator-add-action-generic": "0.2.9",
"@adobe/react-spectrum": "3.32.0",
"@emotion/css": "11.11.2",
"@emotion/react": "11.11.1",
"@identity/imslib": "0.40.0",
"@react-spectrum/toast": "3.0.0-beta.6",
"@spectrum-icons/illustrations": "3.6.7",
"@spectrum-icons/workflow": "4.2.6",
Expand Down
8 changes: 8 additions & 0 deletions web-src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
<meta name="theme-color" content="#333333">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Generate Variations</title>
<script async>
!function(e,t){const o="exc-module-runtime";t.config&&(t.config.mrlv="mrl:c836db09");const n=t.parent;if(t.location===n.location)throw new Error("Module Runtime: Needs to be within an iframe!");if(!t[o]||!t[o].Runtime){try{if(!t[o]&&n[o]&&n[o].bootstrapRuntime)return n[o].bootstrapRuntime(t,((e,t)=>n.postMessage(e,t)))}catch{}var r=function(e){var t=new URL(e.location.href).searchParams.get("_mr");return t||!e.EXC_US_HMR?t:e.sessionStorage.getItem("unifiedShellMRScript")}(t);if(!r)throw new Error("Module Runtime: Missing script!");if("https:"!==(r=new URL(decodeURIComponent(r))).protocol)throw new Error("Module Runtime: Must be HTTPS!");if(!/^(exc-unifiedcontent\.|cdn\.)?experience(-qa|-stage|-cdn|-cdn-stage)?\.adobe\.(com|net)$/.test(r.hostname)&&!/localhost\.corp\.adobe\.com$/.test(r.hostname))throw new Error("Module Runtime: Invalid domain!");if(!/\.js$/.test(r.pathname))throw new Error("Module Runtime: Must be a JavaScript file!");t.EXC_US_HMR&&t.sessionStorage.setItem("unifiedShellMRScript",r.toString());var a=e.createElement("script");a.async=1,a.src=r.toString(),a.onload=a.onreadystatechange=function(){a.readyState&&!/loaded|complete/.test(a.readyState)||(a.onload=a.onreadystatechange=null,a=void 0,"EXC_MR_READY"in t&&t.EXC_MR_READY())},t._srp?.handleError&&(a.onerror=t._srp.handleError),e.head.appendChild(a)}}(document,window);

// This avoids the React Spectrum Provider loading typekit js by default.
// We load fonts (from Typekit) in the application's css file instead.
// This avoids a few extra requests.
window.Typekit = 1;
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
30 changes: 20 additions & 10 deletions web-src/src/components/ApplicationProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import React, { Fragment, useContext, useEffect } from 'react';
import React, { Fragment, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { Content, Heading, InlineAlert } from '@adobe/react-spectrum';
import { useSetRecoilState } from 'recoil';
import { FirefallService } from '../services/FirefallService.js';
import { ImsAuthClient } from '../ims/ImsAuthClient.js';
import excApp from '@adobe/exc-app';
import page from '@adobe/exc-app/page';

import { FirefallService } from '../services/FirefallService.js';
import actions from '../config.json';
import { configurationState } from '../state/ConfigurationState.js';

Expand Down Expand Up @@ -49,35 +50,44 @@ function getConfiguration() {
};
}

function createApplication(configuration) {
function createApplication(configuration, shellConfig) {
return {
...configuration,
firefallService: new FirefallService({
completeEndpoint: actions[COMPLETE_ACTION],
feedbackEndpoint: actions[FEEDBACK_ACTION],
}),
imsAuthClient: new ImsAuthClient(),
shellConfig: shellConfig
};
}

export const ApplicationContext = React.createContext(undefined);

export const ApplicationProvider = ({ children }) => {
const setConfiguration = useSetRecoilState(configurationState);
const [application, setApplication] = React.useState(undefined);
const [application, setApplication] = useState(undefined);
const [error, setError] = React.useState(undefined);

useEffect(() => {
const shellEventsHandler = useCallback((config) => {
try {
const config = getConfiguration();
setConfiguration(config);
setApplication(createApplication(config));
const appConfig = getConfiguration();
setConfiguration(appConfig);
setApplication(() => createApplication(appConfig, config));
console.log(config);
} catch (e) {
console.error(e, 'Failed to create application');
setError(e);
}
}, [setConfiguration, setError]);

useEffect(() => {
const runtime = excApp();
runtime.on('ready', shellEventsHandler);
runtime.on('configuration', shellEventsHandler);
}, []);

page.done();

if (error) {
return (
<InlineAlert margin={'50px'}>
Expand Down
74 changes: 43 additions & 31 deletions web-src/src/components/ConsentDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,68 @@ import {
Divider,
Heading,
} from '@adobe/react-spectrum';
import Cookies from 'js-cookie';
import React, { useEffect } from 'react';
import { LegalTermsLink } from './LegalTermsLink.js';
import { sampleRUM } from '../rum.js';
import { NoAccessDialog } from './NoAccessDialog.js';

const CONSENT_COOKIE_NAME = 'genai-assistant-consent';
const CONSENT_COOKIE_EXPIRATION_DAYS = 365 * 10;
const CONSENT_COOKIE_VALUE = 'yes';
const REDIRECT_URL = 'https://adobe.com';
import settingsApi, { SettingsLevel } from '@adobe/exc-app/settings';

const CONSENT_KEY = 'genai-assistant-consent';
const REDIRECT_URL = 'https://experience.adobe.com';

export function ConsentDialog() {
const [isOpen, setOpen] = React.useState(true);
const [isOpen, setOpen] = React.useState(false);
const [isAccess, setAccess] = React.useState(true);

useEffect(() => {
const consent = Cookies.get(CONSENT_COOKIE_NAME);
setOpen(!consent);
useEffect(async () => {
const { settings } = await settingsApi.get({
groupId: 'test-aem-genai-assistant',
level: SettingsLevel.USERORG,
settings: {[CONSENT_KEY]: false}
});
setOpen(!settings[CONSENT_KEY]);
}, []);

const handleAgree = () => {
sampleRUM('genai:consent:agree', { source: 'ConsentDialog#handleAgree' });
Cookies.set(CONSENT_COOKIE_NAME, CONSENT_COOKIE_VALUE, { expires: CONSENT_COOKIE_EXPIRATION_DAYS });
settingsApi.set({
groupId: 'test-aem-genai-assistant',
level: SettingsLevel.USERORG,
settings: {[CONSENT_KEY]: true}
});
setOpen(false);
};

const handleCancel = () => {
sampleRUM('genai:consent:cancel', { source: 'ConsentDialog#handleCancel' });
setOpen(false);
window.location.href = REDIRECT_URL;
// window.location.href = REDIRECT_URL;
setAccess(false);
};

return (
<DialogContainer onDismiss={handleCancel}>
{isOpen
&& <Dialog onDismiss={handleCancel}>
<Heading>Generative AI in Adobe apps</Heading>
<Divider />
<Content>
<p>
You can create in new ways with generative AI technology.
</p>
<p>
By clicking &quot;Agree&quot;, you agree to our <LegalTermsLink />.
</p>
</Content>
<ButtonGroup>
<Button variant="secondary" onPress={handleCancel}>Cancel</Button>
<Button variant="accent" onPress={handleAgree}>Agree</Button>
</ButtonGroup>
</Dialog>
}
</DialogContainer>
isAccess ? (
<DialogContainer onDismiss={handleCancel}>
{isOpen
&& <Dialog onDismiss={handleCancel}>
<Heading>Generative AI in Adobe apps</Heading>
<Divider />
<Content>
<p>
You can create in new ways with generative AI technology.
</p>
<p>
By clicking &quot;Agree&quot;, you agree to our <LegalTermsLink />.
</p>
</Content>
<ButtonGroup>
<Button variant="secondary" onPress={handleCancel}>Cancel</Button>
<Button variant="accent" onPress={handleAgree}>Agree</Button>
</ButtonGroup>
</Dialog>
}
</DialogContainer>
) : <NoAccessDialog />
);
}
8 changes: 4 additions & 4 deletions web-src/src/components/GenerateButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { v4 as uuid } from 'uuid';
import SenseiGenAIIcon from '../icons/GenAIIcon.js';
import { renderPrompt } from '../helpers/PromptRenderer.js';
import { useApplicationContext } from './ApplicationProvider.js';
import { useAuthContext } from './AuthProvider.js';
import { useShellAuthContext } from './ShellAuthProvider.js';
import { promptState } from '../state/PromptState.js';
import { temperatureState } from '../state/TemperatureState.js';
import { resultsState } from '../state/ResultsState.js';
Expand All @@ -32,7 +32,7 @@ import { sampleRUM } from '../rum.js';

export function GenerateButton() {
const { firefallService } = useApplicationContext();
const { imsToken } = useAuthContext();
const { user } = useShellAuthContext();
const prompt = useRecoilValue(promptState);
const parameters = useRecoilValue(parametersState);
const temperature = useRecoilValue(temperatureState);
Expand All @@ -42,7 +42,7 @@ export function GenerateButton() {

const generateResults = useCallback(async () => {
const finalPrompt = renderPrompt(prompt, parameters);
const { queryId, response } = await firefallService.complete(finalPrompt, temperature, imsToken);
const { queryId, response } = await firefallService.complete(finalPrompt, temperature, user.imsOrg, user.imsToken);
setResults((results) => [...results, {
resultId: queryId,
variants: createVariants(uuid, response),
Expand Down Expand Up @@ -81,7 +81,7 @@ export function GenerateButton() {
style="fill"
onPress={handleGenerate}
isDisabled={generationInProgress}>
{generationInProgress ? <ProgressCircle size="S" aria-label="Generate" isIndeterminate right="10px"/> : <SenseiGenAIIcon />}
{generationInProgress ? <ProgressCircle size="S" aria-label="Generate" isIndeterminate right="10px" /> : <SenseiGenAIIcon />}
Generate
</Button>
<ContextualHelp variant="info">
Expand Down
1 change: 0 additions & 1 deletion web-src/src/components/HomePanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ export function HomePanel({ props }) {
<h3 style={{ padding: 0, margin: 0 }}>
Create high quality content quickly then measure it with experimentation or publish it to your site.
</h3>
<SignOutButton right={-150} top={50}/>
</Flex>

<Heading level={4} alignSelf={'start'}>Prompts</Heading>
Expand Down
38 changes: 38 additions & 0 deletions web-src/src/components/NoAccessDialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import React from 'react';

import {
Content,
Dialog,
DialogContainer,
Heading,
IllustratedMessage,
} from '@adobe/react-spectrum';

import AccessDeniedIcon from '@spectrum-icons/workflow/LockClosed';

export function NoAccessDialog() {
return (
<DialogContainer onDismiss={() => 0}>
<Dialog>
<Content>
<IllustratedMessage>
<AccessDeniedIcon size={'XL'} />
<Heading>Access Denied</Heading>
<Content>You have no access to this product</Content>
</IllustratedMessage>
</Content>
</Dialog>
</DialogContainer>
);
}
6 changes: 3 additions & 3 deletions web-src/src/components/ResultCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { useSetRecoilState } from 'recoil';
import { useIsFavorite } from '../state/IsFavoriteHook.js';
import { useToggleFavorite } from '../state/ToggleFavoriteHook.js';
import { useApplicationContext } from './ApplicationProvider.js';
import { useAuthContext } from './AuthProvider.js';
import { useShellAuthContext } from './ShellAuthProvider.js';
import ReusePromptIcon from '../assets/reuse-prompt.svg';
import { promptState } from '../state/PromptState.js';
import { parametersState } from '../state/ParametersState.js';
Expand Down Expand Up @@ -119,7 +119,7 @@ const styles = {

export function ResultCard({ result, ...props }) {
const { firefallService } = useApplicationContext();
const { imsToken } = useAuthContext();
const { user } = useShellAuthContext();
const [selectedVariant, setSelectedVariant] = useState(result.variants[0]);
const setPrompt = useSetRecoilState(promptState);
const setParameters = useSetRecoilState(parametersState);
Expand All @@ -129,7 +129,7 @@ export function ResultCard({ result, ...props }) {
const saveSession = useSaveSession();

const sendFeedback = useCallback((sentiment) => {
firefallService.feedback(result.resultId, sentiment, imsToken)
firefallService.feedback(result.resultId, sentiment, user.imsOrg, user.imsToken)
.then((id) => {
ToastQueue.positive('Feedback sent', { timeout: 1000 });
})
Expand Down
Loading

0 comments on commit e50e785

Please sign in to comment.