diff --git a/catalog-explorer/lambda/catalog-provider.ts b/catalog-explorer/lambda/catalog-provider.ts index 4b3775e..8ce03e7 100644 --- a/catalog-explorer/lambda/catalog-provider.ts +++ b/catalog-explorer/lambda/catalog-provider.ts @@ -4,7 +4,7 @@ // representation of a page token; simply a raw string as the real representation // will differ with each catalog provider instance -export type PageToken = string; +export type PageToken = any; // a single page of items, contains next/previous tokens to allow for easy navigation export interface Page{ @@ -58,7 +58,7 @@ export interface CatalogProvider { performSearch( searchConditions: SearchConditions, pageSize: number - ): RecommendationResult; + ): RecommendationResult | Promise>; // get a page of items for the given search conditions, page token, // page size and paging direction @@ -67,7 +67,7 @@ export interface CatalogProvider { pageSize: number, pageToken: PageToken | undefined, pagingDirection: PagingDirection - ): RecommendationResult; + ): RecommendationResult | Promise>; // selects the item at the given index from the provided page selectItem( diff --git a/catalog-explorer/lambda/handlers/base-api-handler.ts b/catalog-explorer/lambda/handlers/base-api-handler.ts index effbbc3..0b1aa7a 100644 --- a/catalog-explorer/lambda/handlers/base-api-handler.ts +++ b/catalog-explorer/lambda/handlers/base-api-handler.ts @@ -23,7 +23,7 @@ export abstract class BaseApiHandler implements RequestHandler { return util.isApiRequest(handlerInput, this.apiName); } - abstract handle(handlerInput : HandlerInput): Response; + abstract handle(handlerInput : HandlerInput): Response| Promise; // returns activeCatalog from session, when session is used instead of focus or // when there is no active catalog reference provided in the arguments diff --git a/catalog-explorer/lambda/handlers/get-page-handler.ts b/catalog-explorer/lambda/handlers/get-page-handler.ts index 2482bbc..7539d3a 100644 --- a/catalog-explorer/lambda/handlers/get-page-handler.ts +++ b/catalog-explorer/lambda/handlers/get-page-handler.ts @@ -35,10 +35,10 @@ export class GetPageHandler extends BaseApiHandler{ super(apiName); } - handle(handlerInput: HandlerInput): Response { + async handle(handlerInput: HandlerInput): Promise { const args = util.getApiArguments(handlerInput) as Arguments; - let recommendationResult: RecommendationResult; + let recommendationResult: RecommendationResult | Promise< RecommendationResult>; const catalogRef = super.getActiveCatalog(handlerInput, args.catalogRef); if (CatalogExplorer.useSessionArgs){ @@ -51,7 +51,9 @@ export class GetPageHandler extends BaseApiHandler{ // needs to be refactored for pagingDirection and current page size, once support for generics on actions is added const pagingDirection = PagingDirection.NEXT; recommendationResult = catalogProvider.getRecommendationsPage(args.searchConditions, catalogRef.pageSize, args.pageToken, pagingDirection); + } + recommendationResult = await Promise.resolve(recommendationResult) //converting rescoped boolean to number, as ACDL does not support boolean datatype const modifiedRecommendationResult = util.modifyRecommendationResultRescoped(recommendationResult); @@ -61,12 +63,11 @@ export class GetPageHandler extends BaseApiHandler{ .getResponse(); } - getNewRecommendationResultFromSession(handlerInput: HandlerInput, args: Arguments, catalogRef: CatalogReference): RecommendationResult{ + async getNewRecommendationResultFromSession(handlerInput: HandlerInput, args: Arguments, catalogRef: CatalogReference): Promise>{ const sessionState = CatalogExplorerSessionState.load(handlerInput); const providerState = sessionState.providerState; const catalogProvider: CatalogProvider = CatalogExplorer.getProvider(catalogRef, providerState); - // get arguments from session state const pageToken = sessionState.argsState?.upcomingPageToken; const searchConditions = sessionState.argsState?.searchConditions; @@ -76,8 +77,8 @@ export class GetPageHandler extends BaseApiHandler{ if (pagingDirection === undefined) { throw new Error("Paging Direction not present in session") } - const recommendationResult = catalogProvider.getRecommendationsPage(searchConditions,currPageSize!, pageToken, pagingDirection); - + let recommendationResult = catalogProvider.getRecommendationsPage(searchConditions,currPageSize!, pageToken, pagingDirection); + recommendationResult = await Promise.resolve(recommendationResult) // updating session state arguments sessionState.providerState = catalogProvider.serialize(); sessionState.argsState!.currentPageTokens = { diff --git a/catalog-explorer/lambda/handlers/search-handler.ts b/catalog-explorer/lambda/handlers/search-handler.ts index 30d8320..f8ff96a 100644 --- a/catalog-explorer/lambda/handlers/search-handler.ts +++ b/catalog-explorer/lambda/handlers/search-handler.ts @@ -26,12 +26,12 @@ export class SearchHandler extends BaseApiHandler { return util.isApiRequestPrefix(handlerInput, this.apiName); } - handle(handlerInput : HandlerInput): Response { + async handle(handlerInput : HandlerInput): Promise{ const args = util.getApiArguments(handlerInput) as any; const searchType = this.getSearchTypeFromAPI(handlerInput); - let recommendationResult : RecommendationResult; + let recommendationResult : RecommendationResult | Promise>; if (searchType === "new" || searchType == "refine"){ - recommendationResult = this.newSearchSession(handlerInput,searchType); + recommendationResult = await Promise.resolve(this.newSearchSession(handlerInput,searchType)); } else { throw new Error("Search Type fetched from API is incorrect"); @@ -45,9 +45,9 @@ export class SearchHandler extends BaseApiHandler { .getResponse(); } - newSearchSession(handlerInput: HandlerInput, searchType : string): RecommendationResult{ + async newSearchSession(handlerInput: HandlerInput, searchType : string): Promise>{ const sessionState = CatalogExplorerSessionState.load(handlerInput); - + // get catalog reference out of session, instead of arguments const catalogRef = sessionState.activeCatalog; const providerState = sessionState.providerState; @@ -64,7 +64,7 @@ export class SearchHandler extends BaseApiHandler { const refinedSearchConditions = { ...searchConditionsFromSession, ...newSearchConditions }; newSearchConditions = refinedSearchConditions; } - const recommendationResult = catalogProvider.performSearch(newSearchConditions, catalogRef.pageSize); + const recommendationResult = await Promise.resolve(catalogProvider.performSearch(newSearchConditions, catalogRef.pageSize)); // updating session state arguments sessionState.providerState = catalogProvider.serialize(); diff --git a/catalog-explorer/lambda/package.json b/catalog-explorer/lambda/package.json deleted file mode 100644 index 4bca8b1..0000000 --- a/catalog-explorer/lambda/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@alexa-skill-components/catalog-explorer", - "version": "0.0.1", - "publisher": "Amazon", - "description": "Alexa skill component for searching, refining and navigating through a catalog", - "license": "AmznSL-1.0", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/*", - "interactionModels/*", - "response/*", - "build" - ], - "scripts": { - "clean": "rm -rf dist && rm -rf build && rm -rf node_modules && rm -rf *.tgz", - "compile-ts": "tsc", - "compile-acdl": "acc", - "compile": "npm run compile-ts && npm run compile-acdl", - "lint": "eslint --fix --max-warnings 0 -c .eslintrc.js 'lambda/**/*.{ts,tsx}'", - "test": "jest --testTimeout=10000 --coverage", - "build": "npm run compile && npm pack", - "clean-build": "npm run clean && npm install && npm run build", - "release": "npm run build && npm run lint && npm run test", - "clean-release": "npm run clean && npm install && npm run release" - }, - "dependencies": { - "@aws-sdk/client-dynamodb": "^3.186.0", - "@aws-sdk/util-dynamodb": "^3.186.0", - "@types/node": "^18.8.4", - "ask-sdk-core": "^2.10.1", - "ask-sdk-model": "^1.39.0", - "lodash": "^4.17.15", - "uuid": "^8.3.2" - }, - "devDependencies": { - "@alexa/acdl": "^0.1.18", - "@types/jest": "^26.0.24", - "@types/lodash": "^4.14.181", - "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.3.0", - "ask-sdk-local-debug": "^1.1.0", - "eslint": "^7.2.0", - "eslint-plugin-import": "^2.22.1", - "jest": "^26.6.3", - "jest-html-reporter": "3.2.0", - "ts-jest": "^26.5.6", - "ts-loader": "^9.2.0", - "ts-mockito": "2.5.0", - "typescript": "^3.9.9", - "webpack": "^5.38.1", - "webpack-cli": "^4.7.0" - }, - "ask": { - "srcDir": "src" - } -} diff --git a/catalog-explorer/lambda/providers/ddb-provider.ts b/catalog-explorer/lambda/providers/ddb-provider.ts new file mode 100644 index 0000000..11c12de --- /dev/null +++ b/catalog-explorer/lambda/providers/ddb-provider.ts @@ -0,0 +1,408 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0 +// Licensed under the Amazon Software License http://aws.amazon.com/asl/ + +import { Page, ProactiveOffer, PageToken, CatalogProviderRegistry, PropertyResult,CatalogProvider, RecommendationResult, PagingDirection } from "../catalog-provider"; +import * as _ from 'lodash'; +import { AttributeValue, DynamoDBClient, QueryCommand, QueryCommandInput, ScanCommand, ScanCommandInput} from "@aws-sdk/client-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; +import { extend } from "lodash"; +import { DescribeTableCommand } from "@aws-sdk/client-dynamodb"; +// Catalog provider for a fixed/static list of items; useful for testing, debugging, or real +// use cases utilizing a fixed catalog +export class DDBListProvider implements CatalogProvider { + public static NAME = "ac.DDBListCursor"; + private region : string; + private client : DynamoDBClient; + private tableName : string; + private seenList: any[]; + private key : string; //optional + private lek : any; + + constructor(region: string, tableName : string, seenList: any[][] = [],key : string = "undefined",lek: any = undefined) { + this.seenList = seenList; + this.region = region; + this.client = new DynamoDBClient({ region: this.region }); + this.tableName = tableName; + this.key= key, + this.lek = lek; + } + private async search_DDB(searchConditions: SearchConditions, pageSize: number, pageToken?: PageToken, pagingDirection?: PagingDirection): Promise> { + console.log("Incoming page Token: ", pageToken) + let rescopedFlag = false; + let matchingData: any[] = []; + let prevPageToken : PageToken | undefined = undefined; + let currPageToken = pageToken; + let nextPageToken:any; + let loopInFlag = 0; + + //rescoping condition + if(currPageToken === undefined && pagingDirection === PagingDirection.NEXT) + { + console.log("Rescoped"); + let results: any; + try { + results = await this.client.send(new DescribeTableCommand({TableName: this.tableName})); + } catch (err) { + console.error(err); + } + var count =0; + let seenListEntries = new Set(); + for(var i = 0; i < this.seenList.length; i++) { + for(var j = 0; j < this.seenList[i].length; j++) { + seenListEntries.add(this.seenList[i][j].name); + } + } + count = seenListEntries.size; + console.log("Count", count); + console.log("Items in table:", results.Table?.ItemCount) + if(count === results.Table?.ItemCount) + { + loopInFlag = 1; + rescopedFlag = true; + this.seenList.length = 0; + searchConditions = {} as SearchConditions; + this.search_DDB(searchConditions,pageSize,undefined,undefined); + + } + else + { + rescopedFlag = true; + searchConditions = {} as SearchConditions; + let unseenList = await this.getUnseenList(pageSize); + + if(unseenList.length > pageSize) + { + unseenList = unseenList.slice(0, pageSize); + + } + console.log("UnseenList: ", unseenList); + this.seenList.push(unseenList); + prevPageToken = this.seenList.length - 2 ; + + return { + loopInFlag : loopInFlag, + rescoped: rescopedFlag, + searchConditions: searchConditions, + offer: this.getOfferAfterSearch({ + rescoped: rescopedFlag, + searchConditions: searchConditions, + recommendations: { + items: unseenList, + itemCount: unseenList.length, + prevPageToken: prevPageToken, + nextPageToken: undefined + } + }), + recommendations: { + items: unseenList, + itemCount: unseenList.length, + prevPageToken: prevPageToken, + nextPageToken: undefined + } + } as RecommendationResult + + } + } + + //Condition to get the page from the seen list directly instead of going to the DDB + if(typeof(pageToken) === "number" && pageToken <= this.seenList.length-1) + { + matchingData = this.seenList[pageToken] + } + else + { + //When search condition is empty + if(Object.keys(searchConditions as any).length === 0) + { + matchingData = await this.searchWithoutSearchConditions(pageSize,pageToken); + if(matchingData.length === 0) + { + console.log("Matching data length is zero therefore rescoping"); + return this.search_DDB(searchConditions,pageSize,undefined,PagingDirection.NEXT); //rescoped + } + } + else + { + matchingData = await this.searchWithSearchConditions(pageSize,currPageToken,searchConditions); + if(matchingData.length === 0) + { + console.log("Matching data length is zero therefore rescoping"); + return this.search_DDB(searchConditions,pageSize,undefined,PagingDirection.NEXT); //rescoped + } + } + } + if (pagingDirection === PagingDirection.PREVIOUS){ + prevPageToken= pageToken - 1 < 0? undefined : pageToken - 1 ; + nextPageToken = pageToken + 1; + } + else{ + if(typeof(pageToken) === "number") + prevPageToken = pageToken-1; + else + prevPageToken = (this.seenList.length === 0)? undefined : this.seenList.length-1; + if(typeof(pageToken) === "number" && pageToken < this.seenList.length-1) + nextPageToken = pageToken + 1; + else + nextPageToken = this.lek; + + if(typeof(pageToken) !== "number") + this.seenList.push(matchingData); + } + if(matchingData.length < pageSize) + nextPageToken = undefined; + console.log("Matching data", matchingData); + console.log("prev page token", prevPageToken); + console.log("next page token", nextPageToken); + return { + loopInFlag : loopInFlag, + rescoped: rescopedFlag, + searchConditions: searchConditions, + offer: this.getOfferAfterSearch({ + rescoped: rescopedFlag, + searchConditions: searchConditions, + recommendations: { + items: matchingData, + itemCount: matchingData.length, + prevPageToken: prevPageToken, + nextPageToken: nextPageToken + } + }), + recommendations: { + items: matchingData, + itemCount: matchingData.length, + prevPageToken: prevPageToken, + nextPageToken: nextPageToken, + }, + } as RecommendationResult + } + async performSearch(searchConditions: SearchConditions, pageSize: number): Promise> { + const recommendationResult = await this.search_DDB(searchConditions, pageSize); + return { + ...recommendationResult, + offer: this.getOfferAfterSearch(recommendationResult) + } + } + async getRecommendationsPage(searchConditions: SearchConditions, pageSize: number, pageToken: PageToken | undefined, pagingDirection :PagingDirection): Promise >{ + const recommendationResult = await this.search_DDB(searchConditions, pageSize, pageToken, pagingDirection); + return { + ...recommendationResult, + offer: this.getOfferAfterGetRecommendationsPage(recommendationResult) + } + } + protected async getUnseenList(pageSize: number): Promise { + let results: any ; + var key : any; + if(this.key === 'undefined') + { + try { + results = await this.client.send(new DescribeTableCommand({TableName: this.tableName})); + } catch (err) { + console.error(err); + } + results.Table?.KeySchema?.forEach((ele: { KeyType: string; AttributeName: any; }) => + { + if(ele.KeyType === 'HASH') + key= ele.AttributeName; + }) + } + else + { + key=this.key; + } + let excludeList : any = []; + let temp : any =[]; + let lek: any = undefined ; + + for(var i=0;i a[key]); + for(var y = 0; y ":pk" + index).join(", ") + ")"; + const expressionAttributeNames = { "#pk": key }; + let expressionAttributeValue : any = {}; + excludeList.forEach((key: any, index: string) => { + expressionAttributeValue[":pk" + index] = key ; + }); + let unseenList: any[] = []; + let scanResults: any ; + console.log("filterExpression:",filterExpression); + console.log("expressionAttributeNames:",expressionAttributeNames); + console.log("ExpressionAttributeValues:",expressionAttributeValue); + do + { + console.log("INSIDE LOOP"); + let scanCommand: ScanCommandInput = { + Limit : pageSize, + TableName : this.tableName, + ExclusiveStartKey : lek, + FilterExpression: filterExpression, + ExpressionAttributeNames: expressionAttributeNames, + ExpressionAttributeValues: marshall(expressionAttributeValue), + }; + + try { + scanResults = await this.client.send(new ScanCommand(scanCommand)); + (scanResults.Items || []).forEach(function (element: Record, index: any, array: any) { + unseenList.push(unmarshall(element)); + }); + } catch (err) { + console.error(err); + } + lek = scanResults.LastEvaluatedKey; + } while(unseenList.length < pageSize && scanResults.LastEvaluatedKey !== undefined); + return unseenList; + } + + + protected async searchWithoutSearchConditions(pageSize: number, pageToken : PageToken): Promise { + let list: any[] = []; + let params: ScanCommandInput = { + TableName: this.tableName, + Limit: pageSize, + ExclusiveStartKey: pageToken + }; + const results = await this.client.send(new ScanCommand(params)); + // The primary key of the last item in the page, acts as the nextPageToken in this case + this.lek = results.LastEvaluatedKey; + + (results.Items || []).forEach(function (element: any, index: any, array: any) { + list.push(unmarshall(element)); + }) + return list; + } + protected async searchWithSearchConditions(pageSize: number, pageToken : PageToken,searchConditions : SearchConditions): Promise { + let matchingData : any =[]; + var sc_keys = _.keysIn(searchConditions); + var sc_value= _.values(searchConditions); + const values = [] ; + const names = [] ; + for(var i=1; i<= sc_keys.length; i++) + { + let key= ":col_" + i; + values.push(key); + let key1="#col_" + i; + names.push(key1); + + } + const ExpressionAttributeValues = Object.fromEntries(values.map((v, i) => [v, (Number.isNaN(parseFloat(sc_value[i])))? sc_value[i] : parseInt(sc_value[i]) ])); + const ExpressionAttributeNames = Object.fromEntries(names.map((v, i) => [v, sc_keys[i]])); + const key_condition= "#col_1 = :col_1" + + var indexName = sc_keys[0] + "-index" + var filterExp= "" + for(var i=2; i<= sc_keys.length; i++) + { + if (i == sc_keys.length) + filterExp= filterExp + "#col_" + i + " = :col_" + i + else + filterExp= filterExp + "#col_" + i + " = :col_" + i + " and " + } + + const params: QueryCommandInput = { + TableName : this.tableName, + IndexName : indexName, + KeyConditionExpression: key_condition , + ExclusiveStartKey: pageToken , + Limit: pageSize, //1 + ExpressionAttributeNames : ExpressionAttributeNames, + ExpressionAttributeValues: marshall(ExpressionAttributeValues) + }; + if(filterExp !== "") + { + params.FilterExpression=filterExp + } + + try { + const results = await this.client.send(new QueryCommand(params)); + this.lek = results.LastEvaluatedKey; + console.log("LEKKKK:",this.lek); + (results.Items || []).forEach(function (element: any, index: any, array: any) { + matchingData.push(unmarshall(element)); + }); + } catch (err) { + console.error(err); + } + return matchingData; + } + + selectItem(currentPage: Page, index: number): RecommendationResult { + this.seenList.push(currentPage.items[index]); + return { + rescoped: false, + searchConditions: {} as SearchConditions, + offer: this.getOfferAfterSelectItem(currentPage.items[index]), + recommendations: { + items: [currentPage.items[index]], + itemCount: 1, + prevPageToken: currentPage.prevPageToken, + nextPageToken: currentPage.nextPageToken + } as Page + } as RecommendationResult + } + + getProperty(item: Item, propertyName: string): PropertyResult { + if (typeof item == 'object') { + const newItem = item as any; + return { + value: newItem[propertyName], + offer: this.getOfferAfterGetProperty(newItem[propertyName], propertyName) + } as PropertyResult + } + else { + throw new Error("Item isn't an object"); + } + } + + // returning a dummy string by default; skill developer will have + // to override this method based on the requirement + performAction(item: Item, actionName: string): string { + console.log("Following action has been performed:", actionName); + + return "Result: Action successfully performed" + } + + protected getOfferAfterSearch(recommendationResult: RecommendationResult): ProactiveOffer | undefined { + return undefined; // returning no offer by default, skill developer can override this method if required + } + + protected getOfferAfterGetRecommendationsPage(recommendationResult: RecommendationResult): ProactiveOffer | undefined { + return undefined; // returning no offer by default, skill developer can override this method if required + } + + protected getOfferAfterSelectItem(item: Item): ProactiveOffer | undefined { + return undefined; // returning no offer by default, skill developer can override this method if required + } + + protected getOfferAfterGetProperty(item: Item, propertyName: string): ProactiveOffer | undefined { + return undefined // returning no offer by default, skill developer can override this method if required + } + + serialize(): any { + let DDBconfig = { + region : this.region, + tableName : this.tableName, + seenList: this.seenList, + key : this.key, + lek: this.lek + } + return DDBconfig; + } + + getName(): string { + return DDBListProvider.NAME; + } + + static deserialize(DDBconfig: any): DDBListProvider { + return new DDBListProvider(DDBconfig.region, DDBconfig.tableName,DDBconfig.seenList,DDBconfig.key,DDBconfig.lek); + } +} + +// register the DDBProvider by name so it can be serialized properly +CatalogProviderRegistry.register(DDBListProvider.NAME, DDBListProvider.deserialize); diff --git a/catalog-explorer/lambda/providers/index.ts b/catalog-explorer/lambda/providers/index.ts index 96d030f..56f86ed 100644 --- a/catalog-explorer/lambda/providers/index.ts +++ b/catalog-explorer/lambda/providers/index.ts @@ -1 +1,2 @@ -export * from "./fixed-provider"; \ No newline at end of file +export * from "./fixed-provider"; +export * from "./ddb-provider"; \ No newline at end of file diff --git a/catalog-explorer/package.json b/catalog-explorer/package.json index 18073a1..2e03bf6 100644 --- a/catalog-explorer/package.json +++ b/catalog-explorer/package.json @@ -30,6 +30,8 @@ "clean-release": "npm run clean && npm install && npm run release" }, "dependencies": { + "@aws-sdk/client-dynamodb": "^3.262.0", + "@aws-sdk/util-dynamodb": "^3.262.0", "@types/node": "^18.8.4", "ask-sdk-core": "^2.10.1", "ask-sdk-model": "^1.39.0", diff --git a/catalog-explorer/response/display/defaultPresentSelectedItemVisualResponse/document.json b/catalog-explorer/response/display/defaultPresentSelectedItemVisualResponse/document.json new file mode 100644 index 0000000..f6e21b0 --- /dev/null +++ b/catalog-explorer/response/display/defaultPresentSelectedItemVisualResponse/document.json @@ -0,0 +1,252 @@ + + +{ + "type": "APL", + "version": "2022.1", + "license": "Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License http://aws.amazon.com/asl/", + "theme": "dark", + "import": [ + { + "name": "alexa-layouts", + "version": "1.5.0" + } + ], + "resources": [ + { + "description": "Public resource definitions Simple Text", + "colors": { + "colorText": "@colorText" + }, + "dimensions": { + "headerHeight": "${@headerAttributionIconMaxHeight + (2 * @spacingLarge)}", + "simpleTextScrollHeightWithFooter": "${viewport.height - @headerHeight - @footerPaddingTop - @footerPaddingBottom}", + "simpleTextScrollHeightNoFooter": "100%", + "simpleTextImageHorizontalSpacing": "@spacingLarge", + "simpleTextImageVerticalSpacing": "@spacingMedium", + "simpleTextPaddingBottom": "@spacingLarge", + "simpleLineHeight": "1.5" + } + }, + { + "when": "${@viewportProfileCategory == @hubRound}", + "dimensions": { + "simpleTextPaddingBottom": "@spacing3XLarge" + } + }, + { + "when": "${@viewportProfile == @hubLandscapeSmall}", + "dimensions": { + "simpleTextScrollHeightWithFooter": "100%" + } + }, + { + "when": "${viewport.theme == 'light'}", + "colors": { + "colorText": "@colorTextReversed" + } + } + ], + "layouts": { + "SimpleText": { + "parameters": [ + { + "name": "backgroundImageSource", + "description": "URL for the background image source.", + "type": "string" + }, + { + "name": "footerHintText", + "description": "Hint text to display in the footer.", + "type": "string" + }, + { + "name": "foregroundImageLocation", + "description": "Location of the forground image. Options are top, bottom, left, and right. Default is top.", + "type": "string", + "default": "top" + }, + { + "name": "foregroundImageSource", + "description": "URL for the foreground image source. If blank, the template will be full text layout.", + "type": "string" + }, + { + "name": "headerAttributionImage", + "description": "URL for attribution image or logo source (PNG/vector).", + "type": "string" + }, + { + "name": "headerTitle", + "description": "Title text to render in the header.", + "type": "string" + }, + { + "name": "headerSubtitle", + "description": "Subtitle Text to render in the header.", + "type": "string" + }, + { + "name": "primaryText", + "description": "Text for to render below the title text in the body.", + "type": "string" + }, + { + "name": "secondaryText", + "description": "Text for to render below the primary text in the body.", + "type": "string" + }, + { + "name": "titleText", + "description": "Title text to render in the body.", + "type": "string" + }, + { + "name": "textAlignment", + "description": "Alignment of text content. Options are start, and center. Default is start.", + "type": "string", + "default": "start" + } + ], + "item": { + "type": "Container", + "height": "100vh", + "width": "100vw", + "bind": [ + { + "name": "imageCenterAlign", + "type": "boolean", + "value": "${@viewportProfileCategory == @hubRound || foregroundImageLocation == 'top' || foregroundImageLocation == 'bottom'}" + }, + { + "name": "hasFooter", + "type": "boolean", + "value": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}" + } + ], + "items": [ + { + "type": "AlexaBackground", + "backgroundColor": "${backgroundColor}", + "backgroundImageSource": "${backgroundImageSource}", + "colorOverlay": true + }, + { + "when": "${@viewportProfileCategory != @hubRound}", + "type": "AlexaHeader", + "layoutDirection": "${environment.layoutDirection}", + "headerAttributionImage": "${headerAttributionImage}", + "headerTitle": "${headerTitle}", + "headerSubtitle": "${headerSubtitle}", + "headerAttributionPrimacy": true, + "width": "100%" + }, + { + "description": "Footer Hint Text - not displaying on small hubs", + "when": "${@viewportProfileCategory != @hubRound && @viewportProfile != @hubLandscapeSmall && footerHintText}", + "type": "AlexaFooter", + "hintText": "${footerHintText}", + "theme": "${viewport.theme}", + "width": "100%", + "position": "absolute", + "bottom": "0" + }, + { + "type": "ScrollView", + "height": "${hasFooter ? @simpleTextScrollHeightWithFooter : @simpleTextScrollHeightNoFooter}", + "width": "100vw", + "shrink": 1, + "items": [ + { + "type": "Container", + "width": "100%", + "padding": [ + "@marginHorizontal", + 0 + ], + "paddingBottom": "@simpleTextPaddingBottom", + "justifyContent": "center", + "alignItems": "center", + "items": [ + { + "when": "${@viewportProfileCategory == @hubRound}", + "type": "AlexaHeader", + "layoutDirection": "${environment.layoutDirection}", + "headerAttributionImage": "${headerAttributionImage}", + "headerAttributionPrimacy": true, + "width": "100%" + }, + { + "description": "Image and text content block", + "type": "Container", + "width": "100%", + "alignItems": "${imageCenterAlign ? 'center' : 'start'}", + "direction": "${foregroundImageLocation == 'left' ? 'row' : (foregroundImageLocation == 'right' ? 'rowReverse' : (foregroundImageLocation == 'bottom' ? 'columnReverse' : 'column'))}", + "shrink": 1, + "items": [ + { + "shrink": 1, + "items": [ + { + "description": "Title Text", + "when": "${titleText}", + "type": "Text", + "width": "100%", + "style": "textStyleDisplay3", + "text": "${titleText}", + "textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}" + }, + { + "description": "Primary Text", + "when": "${primaryText}", + "type": "Text", + "spacing": "@spacing2XSmall", + "style": "textStyleBody2", + "fontWeight": "@fontWeightLight", + "text": "${primaryText}", + "textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}" + }, + { + "fontWeight": "@fontWeightLight", + "text": "${secondaryText}", + "textAlign": "${@viewportProfileCategory == @hubRound ? 'center' : textAlignment}", + "lineHeight": "@simpleLineHeight", + "description": "Secondary Text", + "when": "${secondaryText}", + "type": "Text", + "style": "textStyleBody", + "paddingTop": "@spacingLarge", + "spacing": "@spacing2XSmall" + } + ], + "alignItems": "start", + "description": "Primary Text and Title Text block", + "when": "${primaryText || titleText}", + "type": "Container", + "width": "100%", + "paddingTop": "${foregroundImageLocation == 'top' ? @simpleTextImageVerticalSpacing : '0dp'}", + "paddingBottom": "${foregroundImageLocation == 'bottom' ? @simpleTextImageVerticalSpacing : '0dp'}", + "paddingStart": "${foregroundImageLocation == 'left' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}", + "paddingEnd": "${foregroundImageLocation == 'right' && foregroundImageSource ? @simpleTextImageHorizontalSpacing : '0dp'}" + } + ] + } + ] + } + ] + } + ] + } + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": [ + { + "type": "SimpleText", + "primaryText": "You selected ${payload.result.recommendations.items[0].label}." + } + ] + } +} \ No newline at end of file diff --git a/catalog-explorer/response/prompts/defaultPresentPageResponse/document.json b/catalog-explorer/response/prompts/defaultPresentPageResponse/document.json index 7d994cc..1608a62 100644 --- a/catalog-explorer/response/prompts/defaultPresentPageResponse/document.json +++ b/catalog-explorer/response/prompts/defaultPresentPageResponse/document.json @@ -14,29 +14,29 @@ "type": "Selector", "items": [ { - "when": "${payload.result.recommendations.items == null || payload.result.recommendations.items.length < 1}", + "when": "${payload.result.loopInFlag != 0 && payload.result.rescoped != 0}", "type": "Speech", "contentType": "text", - "content": "No items could be found, sorry" + "content": "You have seen all options available in our catalog. Re-interating through the catalog for you." }, { - "when": "${payload.result.rescoped != 0 && payload.result.recommendations.items.length == 1}", - "type": "Speech", - "contentType": "text", - "content": "I wasn't able to find any result that precisely matched your request. How about ${payload.result.recommendations.items[0].label}." - }, - { - "when": "${payload.result.rescoped != 0 && payload.result.recommendations.items.length == 2}", + "when": "${payload.result.recommendations.items == null || payload.result.recommendations.items.length < 1}", "type": "Speech", "contentType": "text", - "content": "I wasn't able to find any result that precisely matched your request. How about ${payload.result.recommendations.items[0].label}, or ${payload.result.recommendations.items[1].label}." + "content": "No items could be found, sorry" }, { - "when": "${payload.result.rescoped != 0 && payload.result.recommendations.items.length == 3}", + "when": "${payload.result.rescoped != 0 && payload.result.loopInFlag == 0 }", "type": "Speech", "contentType": "text", - "content": "I wasn't able to find any result that precisely matched your request. How about ${payload.result.recommendations.items[0].label}, ${payload.result.recommendations.items[1].label}, or ${payload.result.recommendations.items[2].label}." - }, + "content": "I wasn't able to find any result that precisely matched your request." + } + + ] + }, + { + "type": "Selector", + "items": [ { "when": "${payload.result.recommendations.items.length == 1}", "type": "Speech", diff --git a/catalog-explorer/response/prompts/defaultPresentSelectedItemResponse/document.json b/catalog-explorer/response/prompts/defaultPresentSelectedItemResponse/document.json new file mode 100644 index 0000000..1f99fc4 --- /dev/null +++ b/catalog-explorer/response/prompts/defaultPresentSelectedItemResponse/document.json @@ -0,0 +1,16 @@ +{ + "type": "APL-A", + "version": "0.1", + "license": "Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.\nSPDX-License-Identifier: LicenseRef-.amazon.com.-AmznSL-1.0\nLicensed under the Amazon Software License http://aws.amazon.com/asl/", + "description": "Default, generic response prompt for presenting a page of items to a user; assumes all items have a 'label' field and only supports up to 5 items per page", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Speech", + "contentType": "text", + "content": "You selected ${payload.result.recommendations.items[0].label}." + } + } +} diff --git a/catalog-explorer/response/prompts/defaultPropertyResultResponse/document.json b/catalog-explorer/response/prompts/defaultPropertyResultResponse/document.json index 309e367..d742895 100644 --- a/catalog-explorer/response/prompts/defaultPropertyResultResponse/document.json +++ b/catalog-explorer/response/prompts/defaultPropertyResultResponse/document.json @@ -7,15 +7,13 @@ "parameters": [ "payload" ], - "item": { - "type": "Sequential", - "items": [ + "item": + { "type": "Speech", "contentType": "text", "content": "${payload.result.propertyName} is ${payload.result.value}. " } - ] - } + } } \ No newline at end of file diff --git a/catalog-explorer/src/.gitkeep b/catalog-explorer/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/catalog-explorer/src/builderDialogs.acdl b/catalog-explorer/src/builderDialogs.acdl index f48f6a1..e886aaf 100644 --- a/catalog-explorer/src/builderDialogs.acdl +++ b/catalog-explorer/src/builderDialogs.acdl @@ -13,11 +13,13 @@ dPAR= apla("../response/prompts/defaultPerformActionResponse/document.json") dPPR= apla("../response/prompts/defaultPresentPageResponse/document.json") dPRR= apla("../response/prompts/defaultPropertyResultResponse/document.json") dAOR= apla("../response/prompts/defaultAcceptOfferResponse/document.json") +dPSR= apla("../response/prompts/defaultPresentSelectedItemResponse/document.json") dVPAR= apl("../response/display/defaultPerformActionVisualResponse/document.json") dVPPR= apl("../response/display/defaultPresentPageVisualResponse/document.json") dVPRR= apl("../response/display/defaultPropertyResultVisualResponse/document.json") dVAOR= apl("../response/display/defaultAcceptOfferVisualResponse/document.json") +dVPSR= apl("../response/display/defaultPresentSelectedItemVisualResponse/document.json") multiModalPerformActionResponse = MultiModalResponse { apla = dPAR, @@ -39,6 +41,11 @@ multiModalAcceptOfferResponse = MultiModalResponse { apl = dVAOR } +multiModalPresentSelectedItemResponse = MultiModalResponse { + apla = dPSR, + apl = dVPSR +} + defaultNextEvent = utterances([ "next", "next one", @@ -114,7 +121,7 @@ dialog NavigationConfig buildNavigationConfig, RecommendationResult > getPageApi, - + Response presentSelectedItemResponse=multiModalPresentSelectedItemResponse, Response presentPageResponse=multiModalPresentPageResponse, Event selectByOrdinalEvent=defaultSelectByOrdinalEvent, Event selectByRelativePositionEvent=defaultSelectByRelativePositionEvent, @@ -139,7 +146,8 @@ dialog NavigationConfig buildNavigationConfig { diff --git a/catalog-explorer/src/navigate.acdl b/catalog-explorer/src/navigate.acdl index 3d6d12e..b38e946 100644 --- a/catalog-explorer/src/navigate.acdl +++ b/catalog-explorer/src/navigate.acdl @@ -131,7 +131,7 @@ dialog RecommendationResult navigateSelectItemByOrdinal< index = convertOrdinalToIndex(slotValues.ordinal) getItem=config.navConfig.selectionConfig.selectItemApi result = getItem(searchResult.searchConditions, searchResult.recommendations ,index,catalogRef) - presentPage(getItem,result,config.navConfig.paginationConfig.presentPageResponse) + presentPage(getItem,result,config.navConfig.selectionConfig.presentSelectedItemResponse) result } } @@ -145,7 +145,7 @@ dialog RecommendationResult navigateSelectItemByRelative index = convertRelativePositionToIndex(slotValues.relativePosition) getItem=config.navConfig.selectionConfig.selectItemApi result = getItem(searchResult.searchConditions, searchResult.recommendations ,index,catalogRef) - presentPage(getItem,result,config.navConfig.paginationConfig.presentPageResponse) + presentPage(getItem,result,config.navConfig.selectionConfig.presentSelectedItemResponse) result } } @@ -158,7 +158,7 @@ dialog RecommendationResult navigateSelectItemByIndex navigateSelectItemByOrdinal_ index = convertOrdinalToIndex(slotValues.ordinal) getItem=config.navConfig.selectionConfig.selectItemApi result = getItem(searchResult.searchConditions, searchResult.recommendations ,index, catalogRef) - presentPage_withHint(config,getItem,result,config.navConfig.paginationConfig.presentPageResponse, catalogRef) + presentPage_withHint(config,getItem,result,config.navConfig.selectionConfig.presentSelectedItemResponse, catalogRef) result } } @@ -186,7 +186,7 @@ dialog RecommendationResult navigateSelectItemByRelative index = convertRelativePositionToIndex(slotValues.relativePosition) getItem=config.navConfig.selectionConfig.selectItemApi result = getItem(searchResult.searchConditions, searchResult.recommendations ,index, catalogRef) - presentPage_withHint(config,getItem,result,config.navConfig.paginationConfig.presentPageResponse, catalogRef) + presentPage_withHint(config,getItem,result,config.navConfig.selectionConfig.presentSelectedItemResponse, catalogRef) result } } @@ -200,7 +200,7 @@ dialog RecommendationResult navigateSelectItemByIndex_wi slotValues = expect(Invoke, config.navConfig.selectionConfig.selectByIndexEvent) getItem=config.navConfig.selectionConfig.selectItemApi result = getItem(searchResult.searchConditions, searchResult.recommendations, slotValues.index, catalogRef) - presentPage_withHint(config,getItem,result,config.navConfig.paginationConfig.presentPageResponse, catalogRef) + presentPage_withHint(config,getItem,result,config.navConfig.selectionConfig.presentSelectedItemResponse, catalogRef) result } } diff --git a/catalog-explorer/src/types.acdl b/catalog-explorer/src/types.acdl index 1527946..fe7b04c 100644 --- a/catalog-explorer/src/types.acdl +++ b/catalog-explorer/src/types.acdl @@ -127,6 +127,7 @@ type SelectionConfig { Optional, // index to select RecommendationResult // results for selection > selectItemApi + Response presentSelectedItemResponse } //offers