@botonic/plugin-flow-builder 0.21.0-alpha.0

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.
Files changed (58) hide show
  1. package/README.md +1 -0
  2. package/lib/action.d.ts +15 -0
  3. package/lib/action.js +62 -0
  4. package/lib/action.js.map +1 -0
  5. package/lib/content-fields/button.d.ts +10 -0
  6. package/lib/content-fields/button.js +33 -0
  7. package/lib/content-fields/button.js.map +1 -0
  8. package/lib/content-fields/carousel.d.ts +10 -0
  9. package/lib/content-fields/carousel.js +37 -0
  10. package/lib/content-fields/carousel.js.map +1 -0
  11. package/lib/content-fields/content-base.d.ts +10 -0
  12. package/lib/content-fields/content-base.js +15 -0
  13. package/lib/content-fields/content-base.js.map +1 -0
  14. package/lib/content-fields/element.d.ts +11 -0
  15. package/lib/content-fields/element.js +25 -0
  16. package/lib/content-fields/element.js.map +1 -0
  17. package/lib/content-fields/image.d.ts +9 -0
  18. package/lib/content-fields/image.js +28 -0
  19. package/lib/content-fields/image.js.map +1 -0
  20. package/lib/content-fields/text.d.ts +12 -0
  21. package/lib/content-fields/text.js +35 -0
  22. package/lib/content-fields/text.js.map +1 -0
  23. package/lib/functions/conditional-provider.d.ts +4 -0
  24. package/lib/functions/conditional-provider.js +11 -0
  25. package/lib/functions/conditional-provider.js.map +1 -0
  26. package/lib/functions/conditional-queue-status.d.ts +3 -0
  27. package/lib/functions/conditional-queue-status.js +25 -0
  28. package/lib/functions/conditional-queue-status.js.map +1 -0
  29. package/lib/functions/index.d.ts +6 -0
  30. package/lib/functions/index.js +10 -0
  31. package/lib/functions/index.js.map +1 -0
  32. package/lib/handoff.d.ts +2 -0
  33. package/lib/handoff.js +39 -0
  34. package/lib/handoff.js.map +1 -0
  35. package/lib/hubtype-models.d.ts +152 -0
  36. package/lib/hubtype-models.js +50 -0
  37. package/lib/hubtype-models.js.map +1 -0
  38. package/lib/index.d.ts +30 -0
  39. package/lib/index.js +171 -0
  40. package/lib/index.js.map +1 -0
  41. package/lib/utils.d.ts +2 -0
  42. package/lib/utils.js +10 -0
  43. package/lib/utils.js.map +1 -0
  44. package/package.json +53 -0
  45. package/src/action.tsx +62 -0
  46. package/src/content-fields/button.tsx +34 -0
  47. package/src/content-fields/carousel.tsx +42 -0
  48. package/src/content-fields/content-base.ts +15 -0
  49. package/src/content-fields/element.tsx +24 -0
  50. package/src/content-fields/image.tsx +22 -0
  51. package/src/content-fields/text.tsx +35 -0
  52. package/src/functions/conditional-provider.ts +5 -0
  53. package/src/functions/conditional-queue-status.ts +9 -0
  54. package/src/functions/index.ts +7 -0
  55. package/src/handoff.ts +27 -0
  56. package/src/hubtype-models.ts +190 -0
  57. package/src/index.ts +210 -0
  58. package/src/utils.ts +6 -0
@@ -0,0 +1,190 @@
1
+ export enum ButtonStyle {
2
+ BUTTON = 'button',
3
+ QUICK_REPLY = 'quick-reply',
4
+ }
5
+
6
+ export enum MessageContentType {
7
+ CAROUSEL = 'carousel',
8
+ IMAGE = 'image',
9
+ TEXT = 'text',
10
+ KEYWORD = 'keyword',
11
+ HANDOFF = 'handoff',
12
+ FUNCTION = 'function',
13
+ INTENT = 'intent',
14
+ }
15
+
16
+ // TODO: refactor types correctly
17
+ export enum NonMessageContentType {
18
+ INTENT = 'intent',
19
+ PAYLOAD = 'payload',
20
+ QUEUE = 'queue',
21
+ URL = 'url',
22
+ }
23
+
24
+ export enum SubContentType {
25
+ BUTTON = 'button',
26
+ ELEMENT = 'element',
27
+ }
28
+
29
+ export enum MediaContentType {
30
+ ASSET = 'asset',
31
+ }
32
+
33
+ export enum StartFieldsType {
34
+ STARTUP = 'startUp',
35
+ }
36
+
37
+ export enum InputContentType {
38
+ INPUT = 'user-input',
39
+ }
40
+
41
+ export enum InputType {
42
+ INTENTS = 'intents',
43
+ KEYWORDS = 'keywords',
44
+ }
45
+
46
+ export const NodeContentType = {
47
+ ...MessageContentType,
48
+ ...InputContentType,
49
+ }
50
+ // eslint-disable-next-line @typescript-eslint/no-redeclare
51
+ export type NodeContentType = MessageContentType | InputContentType
52
+
53
+ export interface HtFlowBuilderData {
54
+ version: string
55
+ name: string
56
+ locales: string[]
57
+ nodes: HtNodeComponent[]
58
+ }
59
+
60
+ export interface HtNodeLink {
61
+ id: string
62
+ type: NodeContentType
63
+ }
64
+
65
+ export interface HtBaseNode {
66
+ id: string
67
+ code: string
68
+ meta: {
69
+ x: number
70
+ y: number
71
+ }
72
+ follow_up?: HtNodeLink
73
+ target?: HtNodeLink
74
+ }
75
+
76
+ export interface HtTextLocale {
77
+ message: string
78
+ locale: string
79
+ }
80
+ export interface HtInputLocale {
81
+ values: string[]
82
+ locale: string
83
+ }
84
+ export interface HtMediaFileLocale {
85
+ id: string
86
+ file: string
87
+ locale: string
88
+ }
89
+
90
+ export interface HtTextNode extends HtBaseNode {
91
+ type: MessageContentType.TEXT
92
+ content: {
93
+ text: HtTextLocale[]
94
+ buttons_style?: ButtonStyle
95
+ buttons: HtButton[]
96
+ }
97
+ }
98
+
99
+ export interface HtButton {
100
+ id: string
101
+ text: HtTextLocale[]
102
+ target?: HtNodeLink
103
+ hidden: string[]
104
+ }
105
+
106
+ export interface HtImageNode extends HtBaseNode {
107
+ type: MessageContentType.IMAGE
108
+ content: {
109
+ image: HtMediaFileLocale[]
110
+ }
111
+ }
112
+
113
+ export interface HtCarouselNode extends HtBaseNode {
114
+ type: MessageContentType.CAROUSEL
115
+ content: {
116
+ elements: HtElement[]
117
+ }
118
+ }
119
+
120
+ export interface HtElement {
121
+ id: string
122
+ title: HtTextLocale[]
123
+ subtitle: HtTextLocale[]
124
+ image: HtMediaFileLocale[]
125
+ button: HtButton
126
+ hidden: string[]
127
+ }
128
+
129
+ export interface HtIntentNode extends HtBaseNode {
130
+ type: MessageContentType.INTENT
131
+ content: {
132
+ title: HtTextLocale[]
133
+ intents: HtInputLocale[]
134
+ confidence: number
135
+ }
136
+ }
137
+ export interface HtKeywordNode extends HtBaseNode {
138
+ type: MessageContentType.KEYWORD
139
+ content: {
140
+ title: HtTextLocale[]
141
+ keywords: HtInputLocale[]
142
+ }
143
+ }
144
+
145
+ export interface HtHandoffNode extends HtBaseNode {
146
+ type: MessageContentType.HANDOFF
147
+ content: {
148
+ queue: HtQueueLocale[]
149
+ message: HtTextLocale[]
150
+ failMessage: HtTextLocale[]
151
+ }
152
+ }
153
+
154
+ export interface HtQueueLocale {
155
+ id: string
156
+ name: string
157
+ locale: string
158
+ }
159
+
160
+ export interface HtFunctionNode extends HtBaseNode {
161
+ type: MessageContentType.FUNCTION
162
+ content: {
163
+ subtype: string
164
+ action: string
165
+ arguments: Array<any>
166
+ result_mapping: Array<any>
167
+ }
168
+ }
169
+
170
+ /*
171
+ subtype: "conditional-queue-status",
172
+ action: "check-queue-status",
173
+ arguments: [{
174
+ locale: "es-ES",
175
+ values: [{type: "string", value: "056236e2-bcb5-452e-9020-e7e44e5769fc"}]
176
+ }],
177
+ result_mapping: [
178
+ { result: "open", target: { id: "0fab7257-6773-4ae9-9deb-cb0b6a864d08", type: "carousel" }},
179
+ { result: "closed", target: { id: "4fb1b430-f719-47d2-8e51-dcbc6d3cfa77", type: "image" }}
180
+ ]
181
+ */
182
+
183
+ export type HtNodeComponent =
184
+ | HtTextNode
185
+ | HtImageNode
186
+ | HtCarouselNode
187
+ | HtIntentNode
188
+ | HtKeywordNode
189
+ | HtHandoffNode
190
+ | HtFunctionNode
package/src/index.ts ADDED
@@ -0,0 +1,210 @@
1
+ import {
2
+ Input,
3
+ Plugin,
4
+ PluginPostRequest,
5
+ PluginPreRequest,
6
+ } from '@botonic/core'
7
+ import axios from 'axios'
8
+
9
+ import { FlowCarousel } from './content-fields/carousel'
10
+ import { FlowContent } from './content-fields/content-base'
11
+ import { FlowImage } from './content-fields/image'
12
+ import { FlowText } from './content-fields/text'
13
+ import { DEFAULT_FUNCTIONS } from './functions'
14
+ import {
15
+ HtBaseNode,
16
+ HtFlowBuilderData,
17
+ HtFunctionNode,
18
+ HtHandoffNode,
19
+ HtIntentNode,
20
+ HtKeywordNode,
21
+ HtNodeComponent,
22
+ MessageContentType,
23
+ } from './hubtype-models'
24
+
25
+ type BotonicPluginFlowBuilderOptions = {
26
+ flowUrl: string
27
+ flow: any
28
+ customFunctions: Record<any, any>
29
+ }
30
+
31
+ export default class BotonicPluginFlowBuilder implements Plugin {
32
+ private flowUrl: string
33
+ private flow: Promise<HtFlowBuilderData>
34
+ private functions: Record<any, any>
35
+ private currentRequest: PluginPreRequest
36
+
37
+ constructor(readonly options: BotonicPluginFlowBuilderOptions) {
38
+ this.flowUrl = options.flowUrl
39
+ this.flow = options.flow || this.readFlowContent()
40
+ const customFunctions = options.customFunctions || {}
41
+ this.functions = { ...DEFAULT_FUNCTIONS, ...customFunctions }
42
+ }
43
+
44
+ async readFlowContent() {
45
+ const response = await axios.get(this.flowUrl)
46
+ const data = await response.data
47
+ //@ts-ignore
48
+ return Promise.resolve(data as HtFlowBuilderData)
49
+ }
50
+
51
+ async pre(request: PluginPreRequest): Promise<void> {
52
+ this.currentRequest = request
53
+ }
54
+
55
+ async post(_request: PluginPostRequest): Promise<void> {}
56
+
57
+ async getContent(id: string): Promise<HtNodeComponent> {
58
+ const flow = await this.flow
59
+ const content = flow.nodes.find((c: HtBaseNode) => c.id === id)
60
+ if (!content) throw Error(`text with id: '${id}' not found`)
61
+ return content
62
+ }
63
+
64
+ async getHandoffContent(): Promise<HtHandoffNode> {
65
+ const flow = await this.flow
66
+ const content = flow.nodes.find(
67
+ (c: HtNodeComponent) => c.type === 'handoff'
68
+ ) as HtHandoffNode
69
+ if (!content) throw Error(`Handoff node not found`)
70
+ return content
71
+ }
72
+
73
+ getFlowContent(
74
+ hubtypeContent: HtNodeComponent,
75
+ locale: string
76
+ ): FlowContent | undefined {
77
+ switch (hubtypeContent.type) {
78
+ case MessageContentType.TEXT:
79
+ return FlowText.fromHubtypeCMS(hubtypeContent, locale)
80
+ case MessageContentType.IMAGE:
81
+ return FlowImage.fromHubtypeCMS(hubtypeContent, locale)
82
+ case MessageContentType.CAROUSEL:
83
+ return FlowCarousel.fromHubtypeCMS(hubtypeContent, locale)
84
+ default:
85
+ return undefined
86
+ }
87
+ }
88
+
89
+ async getContents(
90
+ id: string,
91
+ locale: string,
92
+ prevContents?: FlowContent[]
93
+ ): Promise<FlowContent[]> {
94
+ const contents = prevContents || []
95
+ const hubtypeContent = await this.getContent(id)
96
+ const content = await this.getFlowContent(hubtypeContent, locale)
97
+ if (hubtypeContent.type === MessageContentType.FUNCTION) {
98
+ const targetId = await this.callFunction(
99
+ hubtypeContent as HtFunctionNode,
100
+ locale
101
+ )
102
+ return this.getContents(targetId, locale, contents)
103
+ } else {
104
+ if (content) contents.push(content)
105
+ // TODO: prevent infinite recursive calls
106
+ if (hubtypeContent.follow_up)
107
+ return this.getContents(hubtypeContent.follow_up.id, locale, contents)
108
+ }
109
+ // execute function
110
+ // return this.getContents(function result_mapping target, locale, contents)
111
+ return contents
112
+ }
113
+
114
+ async getPayloadByInput(
115
+ input: Input,
116
+ locale: string
117
+ ): Promise<string | undefined> {
118
+ try {
119
+ const flow = await this.flow
120
+ const intents = flow.nodes.filter(
121
+ node => node.type == MessageContentType.INTENT
122
+ ) as HtIntentNode[]
123
+ if (input.intent) {
124
+ const matchedIntents = intents.filter(node =>
125
+ //@ts-ignore
126
+ this.hasIntent(node, input.intent, locale)
127
+ )
128
+ if (matchedIntents.length > 0) {
129
+ return matchedIntents[0].target?.id
130
+ }
131
+ }
132
+ } catch (error) {
133
+ console.error('Error getting payload by input: ', error)
134
+ }
135
+
136
+ return undefined
137
+ }
138
+
139
+ hasIntent(node: HtIntentNode, intent: string, locale: string) {
140
+ const result = node.content.intents.find(
141
+ i => i.locale === locale && i.values.includes(intent)
142
+ )
143
+ return Boolean(result)
144
+ }
145
+
146
+ async getPayloadByKeyword(
147
+ input: Input,
148
+ locale: string
149
+ ): Promise<string | undefined> {
150
+ try {
151
+ const flow = await this.flow
152
+ const keywordNodes = flow.nodes.filter(
153
+ node => node.type == MessageContentType.KEYWORD
154
+ ) as HtKeywordNode[]
155
+ const matchedKeywordNodes = keywordNodes.filter(node =>
156
+ //@ts-ignore
157
+ this.matchKeywords(node, input.data, locale)
158
+ )
159
+ if (matchedKeywordNodes.length > 0) {
160
+ return matchedKeywordNodes[0].target?.id
161
+ }
162
+ } catch (error) {
163
+ console.error('Error getting payload by input: ', error)
164
+ }
165
+
166
+ return undefined
167
+ }
168
+
169
+ matchKeywords(node: HtKeywordNode, input: string, locale: string) {
170
+ const result = node.content.keywords.find(
171
+ i => i.locale === locale && this.containsAnyKeywords(input, i.values)
172
+ )
173
+ return Boolean(result)
174
+ }
175
+
176
+ containsAnyKeywords(input: string, keywords: string[]) {
177
+ for (let i = 0; i < keywords.length; i++) {
178
+ if (input.includes(keywords[i])) {
179
+ return true
180
+ }
181
+ }
182
+ return false
183
+ }
184
+
185
+ async callFunction(
186
+ functionNode: HtFunctionNode,
187
+ locale: string
188
+ ): Promise<string> {
189
+ // Check if target is missing or missing arguments
190
+ // TODO: get arguments by locale
191
+ const nameValues = functionNode.content.arguments
192
+ .find(arg => arg.locale === locale)
193
+ .values.map(value => ({ [value.name]: value.value }))
194
+ const args = Object.assign(
195
+ {
196
+ session: this.currentRequest.session,
197
+ results: [functionNode.content.result_mapping.map(r => r.result)],
198
+ },
199
+ ...nameValues
200
+ )
201
+ const functionResult = await this.functions[functionNode.content.subtype](
202
+ args
203
+ )
204
+ // TODO define result_mapping per locale??
205
+ const result = functionNode.content.result_mapping.find(
206
+ r => r.result === functionResult
207
+ )
208
+ return result.target.id
209
+ }
210
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { HtMediaFileLocale } from './hubtype-models'
2
+
3
+ export function getImageByLocale(locale: string, image: HtMediaFileLocale[]) {
4
+ const result = image.find(t => t.locale == locale)
5
+ return result?.file ?? ''
6
+ }