diff --git a/.github/workflows/botonic-ci-test-all-packages.yml b/.github/workflows/botonic-ci-test-all-packages.yml index 7c3f94b03b..0458999abd 100644 --- a/.github/workflows/botonic-ci-test-all-packages.yml +++ b/.github/workflows/botonic-ci-test-all-packages.yml @@ -1,4 +1,4 @@ -name: Botonic test all packages +name: Botonic CI test all packages on: workflow_dispatch: @@ -49,6 +49,7 @@ jobs: PACKAGE_NAME: Botonic plugin-dialogflow tests PACKAGE: botonic-plugin-dialogflow UNIT_TEST_COMMAND: npm run test_ci + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-plugin-dialogflow && npm run build' PUBLISH_TESTS_RESULTS: 'yes' NEEDS_CODECOV_UPLOAD: 'yes' @@ -68,7 +69,7 @@ jobs: with: PACKAGE_NAME: Botonic plugin-google-analytics tests PACKAGE: botonic-plugin-google-analytics - BUILD_COMMAND: '' + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-plugin-google-analytics && npm run build' UNIT_TEST_COMMAND: '' botonic-plugin-inbenta-tests: @@ -113,4 +114,5 @@ jobs: with: PACKAGE_NAME: Botonic react tests PACKAGE: botonic-react + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-react && npm run build' NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-common-workflow.yml b/.github/workflows/botonic-common-workflow.yml index 92f2519f2b..43b66122f1 100644 --- a/.github/workflows/botonic-common-workflow.yml +++ b/.github/workflows/botonic-common-workflow.yml @@ -6,7 +6,7 @@ on: NODE_VERSION: type: string description: 'Node version of the package' - default: '14' + default: '20' required: false PACKAGE_NAME: type: string @@ -44,20 +44,20 @@ on: required: false jobs: - bot-tests: + botonic-tests: name: ${{ inputs.PACKAGE_NAME }} tests runs-on: ubuntu-latest steps: - name: Checking out to current branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setting up node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ inputs.NODE_VERSION }} - name: Setting up cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -75,8 +75,8 @@ jobs: role-duration-seconds: 900 role-session-name: HubtypeCITests - - name: Install dev dependencies - run: (cd ./packages/${{ inputs.PACKAGE }} && npm install -D) + - name: Install dependencies + run: npm install - name: Build if: ${{ inputs.BUILD_COMMAND != '' }} @@ -95,7 +95,7 @@ jobs: CONTENTFUL_TEST_MANAGE_TOKEN: ${{ secrets.CONTENTFUL_TEST_MANAGE_TOKEN }} - name: Verify lint - run: (cd ./packages/${{ inputs.PACKAGE }} && npm run lint_ci) + run: (cd ./packages/${{ inputs.PACKAGE }} && npm run lint_core) - name: Publish Unit Test Results if: ${{ inputs.PUBLISH_TESTS_RESULTS != '' && always() }} diff --git a/.github/workflows/botonic-nlp-tests.yml b/.github/workflows/botonic-nlp-tests.yml deleted file mode 100644 index d5a7710dcd..0000000000 --- a/.github/workflows/botonic-nlp-tests.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Botonic nlp tests - -on: - push: - paths: - - 'packages/botonic-nlp/**' - - '.github/workflows/botonic-nlp-tests.yml' - workflow_dispatch: - -jobs: - botonic-plugin-contentful-tests: - uses: ./.github/workflows/botonic-common-workflow.yml - secrets: inherit #pragma: allowlist secret - with: - PACKAGE_NAME: Botonic nlp tests - PACKAGE: botonic-nlp - UNIT_TEST_COMMAND: npm run test_ci - NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-nlu-tests.yml b/.github/workflows/botonic-nlu-tests.yml deleted file mode 100644 index 8a96b92027..0000000000 --- a/.github/workflows/botonic-nlu-tests.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Botonic nlu tests - -on: - push: - paths: - - 'packages/botonic-nlu/**' - - '.github/workflows/botonic-nlu-tests.yml' - workflow_dispatch: - -jobs: - botonic-plugin-nlu-tests: - uses: ./.github/workflows/botonic-common-workflow.yml - secrets: inherit #pragma: allowlist secret - with: - PACKAGE_NAME: Botonic nlu tests - PACKAGE: botonic-nlu - UNIT_TEST_COMMAND: npm run test_ci - PUBLISH_TESTS_RESULTS: 'yes' - NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-plugin-dialogflow-tests.yml b/.github/workflows/botonic-plugin-dialogflow-tests.yml index 6daa7d9d9b..b144058c4e 100644 --- a/.github/workflows/botonic-plugin-dialogflow-tests.yml +++ b/.github/workflows/botonic-plugin-dialogflow-tests.yml @@ -14,6 +14,7 @@ jobs: with: PACKAGE_NAME: Botonic plugin-dialogflow tests PACKAGE: botonic-plugin-dialogflow + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-plugin-dialogflow && npm run build' UNIT_TEST_COMMAND: npm run test_ci PUBLISH_TESTS_RESULTS: 'yes' NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-plugin-dynamo-tests.yml b/.github/workflows/botonic-plugin-dynamo-tests.yml index a9efcc6685..25a2d778e6 100644 --- a/.github/workflows/botonic-plugin-dynamo-tests.yml +++ b/.github/workflows/botonic-plugin-dynamo-tests.yml @@ -14,6 +14,6 @@ jobs: with: PACKAGE_NAME: Botonic plugin-dynamodb Tests PACKAGE: botonic-plugin-dynamodb - BUILD_COMMAND: npm run build_unit_tests + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-plugin-dynamodb && npm run build_unit_tests' NEEDS_CODECOV_UPLOAD: 'yes' NEEDS_AWS_CRED: 'yes' diff --git a/.github/workflows/botonic-plugin-google-analytics-tests.yml b/.github/workflows/botonic-plugin-google-analytics-tests.yml index c1b8524af0..71c69d87be 100644 --- a/.github/workflows/botonic-plugin-google-analytics-tests.yml +++ b/.github/workflows/botonic-plugin-google-analytics-tests.yml @@ -14,5 +14,5 @@ jobs: with: PACKAGE_NAME: Botonic plugin-google-analytics tests PACKAGE: botonic-plugin-google-analytics - BUILD_COMMAND: '' + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-plugin-google-analytics && npm run build' UNIT_TEST_COMMAND: '' diff --git a/.github/workflows/botonic-plugin-intent-classification-tests.yml b/.github/workflows/botonic-plugin-intent-classification-tests.yml deleted file mode 100644 index 06b0d8e5c2..0000000000 --- a/.github/workflows/botonic-plugin-intent-classification-tests.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Botonic plugin-intent-classification tests - -on: - push: - paths: - - 'packages/botonic-plugin-intent-classification/**' - - '.github/workflows/botonic-plugin-intent-classification-tests.yml' - workflow_dispatch: - -jobs: - botonic-plugin-intent-classification-tests: - uses: ./.github/workflows/botonic-common-workflow.yml - secrets: inherit #pragma: allowlist secret - with: - PACKAGE_NAME: Botonic plugin-intent-classification tests - PACKAGE: botonic-plugin-intent-classification - UNIT_TEST_COMMAND: npm run test_ci - PUBLISH_TESTS_RESULTS: 'yes' - NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-plugin-ner-tests.yml b/.github/workflows/botonic-plugin-ner-tests.yml deleted file mode 100644 index 69a871b902..0000000000 --- a/.github/workflows/botonic-plugin-ner-tests.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Botonic plugin-ner tests - -on: - push: - paths: - - 'packages/botonic-plugin-ner/**' - - '.github/workflows/botonic-plugin-ner-tests.yml' - workflow_dispatch: - -jobs: - botonic-plugin-ner-tests: - uses: ./.github/workflows/botonic-common-workflow.yml - secrets: inherit #pragma: allowlist secret - with: - PACKAGE_NAME: Botonic plugin-ner tests - PACKAGE: botonic-plugin-ner - UNIT_TEST_COMMAND: npm run test_ci - PUBLISH_TESTS_RESULTS: 'yes' - NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-plugin-nlu-tests.yml b/.github/workflows/botonic-plugin-nlu-tests.yml deleted file mode 100644 index 28a8200b36..0000000000 --- a/.github/workflows/botonic-plugin-nlu-tests.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Botonic plugin-nlu tests - -on: - push: - paths: - - 'packages/botonic-plugin-nlu/**' - - '.github/workflows/botonic-plugin-nlu-tests.yml' - workflow_dispatch: - -jobs: - botonic-plugin-nlu-tests: - uses: ./.github/workflows/botonic-common-workflow.yml - secrets: inherit #pragma: allowlist secret - with: - PACKAGE_NAME: Botonic plugin-nlu tests - PACKAGE: botonic-plugin-nlu - UNIT_TEST_COMMAND: npm run test_ci - PUBLISH_TESTS_RESULTS: 'yes' - NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.github/workflows/botonic-react-tests.yml b/.github/workflows/botonic-react-tests.yml index db9bb72b84..198abd6386 100644 --- a/.github/workflows/botonic-react-tests.yml +++ b/.github/workflows/botonic-react-tests.yml @@ -14,4 +14,5 @@ jobs: with: PACKAGE_NAME: Botonic react tests PACKAGE: botonic-react + BUILD_COMMAND: 'cd ../botonic-core && npm run build && cd ../botonic-react && npm run build' NEEDS_CODECOV_UPLOAD: 'yes' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a3e7129d0a..9eebd83758 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,30 +20,13 @@ repos: entry: bash scripts/qa/old/lint-package.sh packages/botonic-cli language: system files: ^packages/botonic-cli/ + - id: core name: core entry: scripts/qa/old/lint-package.sh packages/botonic-core language: system files: ^packages/botonic-core/ - - id: core-d-ts - name: core-d-ts - entry: scripts/qa/old/lint-d-ts.sh packages/botonic-core - language: system - files: ^packages/botonic-core/src/.*\.d\.ts - - - id: nlu - name: nlu - entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-nlu - language: system - files: ^packages/botonic-plugin-nlu/ - - - id: nlp - name: nlp - entry: scripts/qa/old/lint-package.sh packages/botonic-nlp - language: system - files: ^packages/botonic-nlp - - id: contentful name: contentful entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-contentful @@ -74,12 +57,6 @@ repos: language: system files: ^packages/botonic-plugin-luis/ - - id: plugin-nlu - name: plugin-nlu - entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-nlu - language: system - files: ^packages/botonic-plugin-nlu/ - - id: segment name: segment entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-segment @@ -98,12 +75,6 @@ repos: language: system files: ^packages/botonic-react/ - - id: react-d-ts - name: react-d-ts - entry: scripts/qa/old/lint-d-ts.sh packages/botonic-react - language: system - files: ^packages/botonic-react/src/.*\.d\.ts - - id: google-analytics name: google-analytics entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-google-analytics @@ -122,24 +93,12 @@ repos: language: system files: ^docs/ - - id: plugin-ner - name: plugin-ner - entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-ner - language: system - files: ^packages/botonic-plugin-ner/ - - - id: plugin-intent-classification - name: plugin-intent-classification - entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-intent-classification - language: system - files: ^packages/botonic-plugin-intent-classification/ - - id: plugin-flow-builder name: plugin-flow-builder entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-flow-builder language: system files: ^packages/botonic-plugin-flow-builder/ - + - id: hubtype-analytics name: hubtype-analytics entry: scripts/qa/old/lint-package.sh packages/botonic-plugin-hubtype-analytics diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..89453f2d69 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,165 @@ +# Botonic Examples + +This repository contains a set of projects available implemented in +[Botonic](https://botonic.io). + +Each example is standalone and can be initialized by running: + +```bash +$ botonic new +``` + +and select it from the selector. + +## Overview of Examples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLive DemoDescription
+ + Blank + + + Template with empty actions. The bot will always respond with the + default `404` action "I don't understand you" when you test it. +
+ + Blank Typescript + + + Template with empty actions prepared to be used with Typescript. +
+ + Booking Platform + + + 🔗 + + This example shows you how to make a reservation in a hotel using a + cover component, custom messages and webviews. +
+ + Childs + + + Simple example on how childRoutes work. It allows you to build a bot + with deep flows and navigate a decision tree using interactive + elements like buttons. +
+ + Custom Webchat + + Customizable webchat that can be embedded in your website.
+ + Dynamic Carousel + + + Bot that gets data from an external API and renders a Carousel. + Carousels are horizontal scrollable elements with image, title and + buttons for users to trigger an action. +
+ + DynamoDB + + + DynamoDB: Using AWS DynamoDB to track events. +
+ + Human Handoff + + Simple bot that transfers the conversation to Hubtype Desk.
+ + Intent + + Bot that uses external AI like DialogFlow.
+ + Telco Offers + + + 🔗 + + This example shows you a multi-language conversation flow to acquire an Internet or a cell phone rate using buttons and replies. +
+ + Tutorial + + Example with comments to learn by reading the source files.
+ +## Requirements + +- Node.js version 20 or higher +- [NPM cli](https://docs.npmjs.com/cli/npm) or [Yarn](https://yarnpkg.com/en/) + +## Contributing with new examples + +1. Clone this project. +2. Create a new directory within `examples` directory: + + ```bash + $ botonic new + ``` + +3. Select an example from the prompted list to start with. +4. Let your imagination run wild. +5. Push your code. +6. Open a new [Pull Request](https://github.com/hubtype/botonic/pulls). +7. We will slightly evaluate and test the project and will be merged as soon as possible. 😊 diff --git a/examples/blank-typescript/.gitignore b/examples/blank-typescript/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/blank-typescript/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/blank-typescript/babel.config.js b/examples/blank-typescript/babel.config.js new file mode 100644 index 0000000000..072f7fb7f1 --- /dev/null +++ b/examples/blank-typescript/babel.config.js @@ -0,0 +1,30 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + '@babel/typescript', + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/blank-typescript/jest.config.js b/examples/blank-typescript/jest.config.js new file mode 100644 index 0000000000..5914ebf081 --- /dev/null +++ b/examples/blank-typescript/jest.config.js @@ -0,0 +1,17 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.[j|t]sx?$": [ + "ts-jest", + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/blank-typescript/package.json b/examples/blank-typescript/package.json new file mode 100644 index 0000000000..d96b2e3625 --- /dev/null +++ b/examples/blank-typescript/package.json @@ -0,0 +1,17 @@ +{ + "name": "@botonic/example-blank-typescript", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + } +} diff --git a/packages/create-botonic-app/dev-template/bot/src/assets/.gitkeep b/examples/blank-typescript/src/actions/.gitkeep similarity index 100% rename from packages/create-botonic-app/dev-template/bot/src/assets/.gitkeep rename to examples/blank-typescript/src/actions/.gitkeep diff --git a/packages/create-botonic-app/dev-template/bot/src/nlp/data/en/.gitkeep b/examples/blank-typescript/src/assets/.gitkeep similarity index 100% rename from packages/create-botonic-app/dev-template/bot/src/nlp/data/en/.gitkeep rename to examples/blank-typescript/src/assets/.gitkeep diff --git a/examples/blank-typescript/src/index.ts b/examples/blank-typescript/src/index.ts new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/blank-typescript/src/index.ts @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/packages/create-botonic-app/dev-template/bot/src/nlp/tasks/intent-classification/models/en/.gitkeep b/examples/blank-typescript/src/locales/.gitkeep similarity index 100% rename from packages/create-botonic-app/dev-template/bot/src/nlp/tasks/intent-classification/models/en/.gitkeep rename to examples/blank-typescript/src/locales/.gitkeep diff --git a/packages/create-botonic-app/dev-template/bot/src/locales/index.js b/examples/blank-typescript/src/locales/index.ts similarity index 100% rename from packages/create-botonic-app/dev-template/bot/src/locales/index.js rename to examples/blank-typescript/src/locales/index.ts diff --git a/examples/blank-typescript/src/plugins.ts b/examples/blank-typescript/src/plugins.ts new file mode 100644 index 0000000000..9f7cc5e633 --- /dev/null +++ b/examples/blank-typescript/src/plugins.ts @@ -0,0 +1,3 @@ +import type { PluginConfig } from '@botonic/core' + +export const plugins: PluginConfig[] = [] diff --git a/examples/blank-typescript/src/routes.ts b/examples/blank-typescript/src/routes.ts new file mode 100644 index 0000000000..bcd2d3893f --- /dev/null +++ b/examples/blank-typescript/src/routes.ts @@ -0,0 +1,3 @@ +import { Route } from '@botonic/react' + +export const routes: Route[] = [] diff --git a/examples/blank-typescript/src/webchat/index.ts b/examples/blank-typescript/src/webchat/index.ts new file mode 100644 index 0000000000..b2aefe20c3 --- /dev/null +++ b/examples/blank-typescript/src/webchat/index.ts @@ -0,0 +1,3 @@ +import { WebchatAppArgs } from '@botonic/react' + +export const webchat: WebchatAppArgs = {} diff --git a/examples/blank-typescript/src/webviews/index.ts b/examples/blank-typescript/src/webviews/index.ts new file mode 100644 index 0000000000..254c289243 --- /dev/null +++ b/examples/blank-typescript/src/webviews/index.ts @@ -0,0 +1,3 @@ +import { Webview } from '@botonic/react' + +export const webviews: Webview[] = [] diff --git a/examples/blank-typescript/tests/__mocks__/fileMock.js b/examples/blank-typescript/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/blank-typescript/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/blank-typescript/tests/__mocks__/styleMock.js b/examples/blank-typescript/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/blank-typescript/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/blank-typescript/tests/app.test.js b/examples/blank-typescript/tests/app.test.js new file mode 100644 index 0000000000..e9d89624ea --- /dev/null +++ b/examples/blank-typescript/tests/app.test.js @@ -0,0 +1,20 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) diff --git a/examples/blank-typescript/tsconfig.json b/examples/blank-typescript/tsconfig.json new file mode 100644 index 0000000000..8c9b50db78 --- /dev/null +++ b/examples/blank-typescript/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + // skipLibCheck to avoid error below (I checked and all @types/react have the same versipn) + // ERROR in [at-loader] ../../../botonic/packages/@botonic/plugin-contentful/node_modules/@botonic/react/node_modules/@types/react/index.d.ts:2814:14 + //TS2300: Duplicate identifier 'LibraryManagedAttributes'. + // + //ERROR in [at-loader] ./node_modules/@botonic/react/node_modules/@types/react/index.d.ts:2814:14 + //TS2300: Duplicate identifier 'LibraryManagedAttributes'. + "sourceMap": true, + "target": "ES2015", + "module": "Node16", + "moduleResolution": "Node16", + "baseUrl": "src", + "paths": { + "*": ["src/*", "lib/*", "types/*"] + }, + "outDir": "lib", + "jsx": "react-jsx", + "allowJs": true, + "lib": ["DOM", "ES2022"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib"] +} diff --git a/examples/blank-typescript/webpack-entries/dev-entry.js b/examples/blank-typescript/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/blank-typescript/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/blank-typescript/webpack-entries/node-entry.js b/examples/blank-typescript/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/blank-typescript/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/blank-typescript/webpack-entries/webchat-entry.js b/examples/blank-typescript/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/blank-typescript/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/blank-typescript/webpack-entries/webviews-entry.js b/examples/blank-typescript/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/blank-typescript/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/blank-typescript/webpack.config.js b/examples/blank-typescript/webpack.config.js new file mode 100644 index 0000000000..bc9a260421 --- /dev/null +++ b/examples/blank-typescript/webpack.config.js @@ -0,0 +1,339 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelTypescriptLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + '@babel/typescript', + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelTypescriptLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelTypescriptLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelTypescriptLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelTypescriptLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/blank/.gitignore b/examples/blank/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/blank/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/blank/babel.config.js b/examples/blank/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/blank/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/blank/jest.config.js b/examples/blank/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/blank/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/blank/package.json b/examples/blank/package.json new file mode 100644 index 0000000000..e078b41938 --- /dev/null +++ b/examples/blank/package.json @@ -0,0 +1,20 @@ +{ + "name": "@botonic/example-blank", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/packages/create-botonic-app/dev-template/bot/src/nlp/tasks/ner/models/en/.gitkeep b/examples/blank/src/actions/.gitkeep similarity index 100% rename from packages/create-botonic-app/dev-template/bot/src/nlp/tasks/ner/models/en/.gitkeep rename to examples/blank/src/actions/.gitkeep diff --git a/packages/create-botonic-app/template/bot/src/assets/.gitkeep b/examples/blank/src/assets/.gitkeep similarity index 100% rename from packages/create-botonic-app/template/bot/src/assets/.gitkeep rename to examples/blank/src/assets/.gitkeep diff --git a/examples/blank/src/index.js b/examples/blank/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/blank/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/packages/create-botonic-app/template/bot/src/nlp/data/en/.gitkeep b/examples/blank/src/locales/.gitkeep similarity index 100% rename from packages/create-botonic-app/template/bot/src/nlp/data/en/.gitkeep rename to examples/blank/src/locales/.gitkeep diff --git a/packages/create-botonic-app/template/bot/src/locales/index.js b/examples/blank/src/locales/index.js similarity index 100% rename from packages/create-botonic-app/template/bot/src/locales/index.js rename to examples/blank/src/locales/index.js diff --git a/examples/blank/src/plugins.js b/examples/blank/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/blank/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/blank/src/routes.js b/examples/blank/src/routes.js new file mode 100644 index 0000000000..3b2f9035aa --- /dev/null +++ b/examples/blank/src/routes.js @@ -0,0 +1 @@ +export const routes = [] diff --git a/packages/create-botonic-app/template/webchat/webchat-config.js b/examples/blank/src/webchat/index.js similarity index 100% rename from packages/create-botonic-app/template/webchat/webchat-config.js rename to examples/blank/src/webchat/index.js diff --git a/examples/blank/src/webviews/index.js b/examples/blank/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/blank/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/blank/tests/__mocks__/fileMock.js b/examples/blank/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/blank/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/blank/tests/__mocks__/styleMock.js b/examples/blank/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/blank/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/blank/tests/app.test.js b/examples/blank/tests/app.test.js new file mode 100644 index 0000000000..e9d89624ea --- /dev/null +++ b/examples/blank/tests/app.test.js @@ -0,0 +1,20 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) diff --git a/examples/blank/webpack-entries/dev-entry.js b/examples/blank/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/blank/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/blank/webpack-entries/node-entry.js b/examples/blank/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/blank/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/blank/webpack-entries/webchat-entry.js b/examples/blank/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/blank/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/blank/webpack-entries/webviews-entry.js b/examples/blank/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/blank/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/blank/webpack.config.js b/examples/blank/webpack.config.js new file mode 100644 index 0000000000..03250b1239 --- /dev/null +++ b/examples/blank/webpack.config.js @@ -0,0 +1,338 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/booking-platform/.gitignore b/examples/booking-platform/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/booking-platform/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/booking-platform/README.md b/examples/booking-platform/README.md new file mode 100644 index 0000000000..853eb9e717 --- /dev/null +++ b/examples/booking-platform/README.md @@ -0,0 +1,571 @@ +# Booking Platform + +This example shows you how to make a reservation in a hotel by taking all the profit of webviews and custom messages. + + +**What's in this document?** + +- [How to use this example](#how-to-use-this-example) +- [Webchat components](#webchat-components) + - [1. Cover Component](#1-cover-component) + - [2. Custom Messages](#2-custom-messages) + - [2.1 Custom Message from bot](#21-custom-message-from-bot) + - [2.2 Custom Message from user](#22-custom-message-from-user) + - [3. Persistent Menu](#3-persistent-menu) +- [Carousel](#carousel) +- [Webviews](#webviews) + + +## How to use this example + +1. From your command line, download the example by running: + ```bash + $ botonic new booking-platform + ``` +2. `cd` into `` directory that has been created. +3. Run `botonic serve` to test it in your local machine. + +## Webchat components + +In this section, we will see the most relevant webchat components that have been used to create this example. + + +#### 1. Cover Component + +The [Cover Component](https://botonic.io/docs/webchat/webchat-covercomponent/) is the first component that will appear when a user opens the webchat. + +We have used `styled-components` that allows us to define new React component with our styles attached to it. + +**src/webchat/cover-component.js** + +```javascript +import styled from 'styled-components' + +const Container = styled.div` + position: absolute; + height: 432px; + width: calc(100%-60px); + left: 0; + top: 48px; + background: white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px 30px 20px 30px; + z-index: 3; +` +const Button = styled.button` + width: 80px; + height: 40px; + background: #2f2f2f; + border-radius: 8px; + margin-top: 20px; + text-align: center; + color: white; +` +const Text = styled.a` + position: relative; + fontfamily: Verdana; + fontweight: normal; + fontsize: 14px; + text-align: center; + width: 85%; + line-height: 1.4; + color: #000000; + margin: 0px 30px 20px 30px; +` +``` + +We have also used the **TextField** component from `material-ui` to let the users enter and edit text. + +```javascript +import TextField from '@material-ui/core/TextField' + +export function MyTextField(props) { + let helperText = '' + if (props.error) + helperText = + props.error && props.value === '' + ? 'This field is required' + : props.errorMessage || '' + return ( + + ) +} +``` + +After defining these components, we have created our **CustomCover** component as: + +```javascript +export default class CustomCover extends React.Component { + static contextType = WebchatContext + constructor(props) { + super(props) + this.state = { + name: '', + email: '', + error: false, + } + } + + close() { + if (this.verifiedForm()) { + this.context.updateUser({ + name: this.state.name, + extra_data: { email: this.state.email, hotels: [] }, + }) + this.context.sendText('Start', 'PAYLOAD') + this.props.closeComponent() + } else { + this.setState({ error: true }) + } + } + + verifiedForm() { + if (!this.incorrectName() && !this.incorrectEmail()) { + return true + } + return false + } + + incorrectName() { + return this.state.name == '' + } + + incorrectEmail() { + return !this.state.email.match(emailRegex) || this.state.email == '' + } + + handleName = event => { + this.setState({ name: event.target.value }) + } + + handleEmail = event => { + this.setState({ email: event.target.value }) + this.setState({ error: false }) + } + + render() { + return ( + + + Welcome to Botonic Booking Platform First of all, I would need your + name and email. + + + + +

+ + We will not store the fulfilled information. You can fake the data. + +

+
+ ) + } +} +``` +We have created some functions to check and handle the user data. Moreover, to store the relevant information in the session we have used the `updateUser()` function. + +Finally, to close the component, we have called the `closeComponent()` function that can be found in the props. In this case, we also used `sendText` to add a message form the user after the component is closed. That then will be captured in the routes and will execute the next action. + +**src/routes.js** +```javascript + { path: 'start', text: /^start$/i, action: Start }, +``` + +After creating our component we just need to assign it to the `coverComponent` property: + +**src/webchat/index.js** + +```javascript +import CustomCover from './cover-component' + +export const webchat = { + coverComponent: CustomCover, +} +``` + +#### 2. Custom Messages + +[Custom Messages](https://botonic.io/docs/components/message/) allows us to create any kind of message that we want. + + +###### 2.1 Custom Message from bot + +We have used a CustomMessage to create the **Hotel Form** that appears after the user chooses the hotel to book. + +In this component, we have also used `styled-components` and `material-ui` to create the **TextField**, the **Autocomplete** and the **DatePicker** components. + +The structure is very similar to the **CustomCover** component, but in this case we need to export the component as: + +**src/webchat/hotel-form-message.js** + +```javascript +export default customMessage({ + name: 'hotel-form', + component: HotelForm, + defaultProps: { + style: { + width: '100%', + backgroundColor: '#ffffff', + border: 'none', + boxShadow: 'none', + paddingLeft: '5px', + }, + imageStyle: { display: 'none' }, + blob: false, + enableTimestamps: false, + }, +}) +``` + +The `name` and `component` props are mandatory, and in this case, we have also defined the `defaultProps` to change some properties of the message. + +The last important step to do when we define a `CustomMessage`, is to add it in the custom types: + +**src/webchat/index.js** + +```javascript +import HotelForm from './hotel-form-message' + +export const webchat = { + theme: { + message: { + customTypes: [HotelForm, RateMessage, RateUserMessage], + }, + }, +} +``` + +Then we can call this component in the actions. + +**src/actions/book-hotel.jsx** + +```javascript +import React from 'react' +import { Text } from '@botonic/react' +import HotelForm from '../webchat/hotel-form-message' + +export default class extends React.Component { + static async botonicInit(request) { + const hotel = request.input.payload.split('-')[1] + const name = request.session.user.name + return { hotel, name } + } + render() { + return ( + <> + + {this.props.name} you have selected **{this.props.hotel}**. To confirm + the reservation, we would need some more information. + + + + ) + } +} +``` + +###### 2.2 Custom Message from user +It is also possible to add a custom message on the user side, as we have done with RateUserMessage. + +In this case, we have used **react-stars** to display the rate of the user. To add this message on the user side the only extra thing we need to do is change the `from` property to 'user'. + +**src/webchat/rate-user-message.js** +```javascript +import React from 'react' +import { customMessage, WebchatContext } from '@botonic/react' +import ReactStars from 'react-stars' + +class RateUserMessage extends React.Component { + static contextType = WebchatContext + render() { + return ( + + ) + } +} + +export default customMessage({ + name: 'rate-user-message', + component: RateUserMessage, + defaultProps: { + from: 'user', + }, +}) +``` + +#### 3. Persistent Menu + +The [Persistent Menu](https://botonic.io/docs/webchat/webchat-persistentmenu/) is a component that will be shown whenever the user clicks to the button placed in the bottom left corner. + +**src/webchat/index.js** + +```javascript +import CheckReservationsWebview from '../webviews/components/check-reservations' +import { CustomPersistentMenu } from './custom-persistentMenu' +export const webchat = { + theme: { + userInput: { + persistentMenu: [ + { label: 'Check your reservation', webview: CheckReservationsWebview }, + { label: 'Book a hotel', payload: 'carousel' }, + { closeLabel: 'Close' }, + ], + menu: { + darkBackground: true, + custom: CustomPersistentMenu, + }, + }, + }, +} +``` +In this case, we have created a `persistentMenu` with three buttons: the first one will open a webview, the second one will send the payload 'carousel' and the last one will close the menu. + +In this example, we have also customized our menu in **custom-persistentMenu.js** and we have enabled the `darkBackgroud` property to darken the background of the webchat and let the user focus on the persistent menu only. + +## Carousel + +The [Carousel](https://botonic.io/docs/components/carousel/) component allows you to show a collection of images in a cyclic view. In the example, we have used it to show the different hotel options. + +**src/actions/carousel.js** + +```javascript +import React from 'react' +import { + Text, + Carousel, + Element, + Pic, + Button, + Title, + Subtitle, +} from '@botonic/react' + +export default class extends React.Component { + render() { + const hotels = [ + { + name: 'Hotel Alabama', + desc: '* * * *', + payload: 'hotel-Hotel Alabama', + pic: + 'https://cdn.styleblueprint.com/wp-content/uploads/2017/06/4512594599_9edc8fee0a_b.jpg', + }, + { + name: 'Hotel Arizona', + desc: '* * * * *', + payload: 'hotel-Hotel Arizona', + pic: + 'https://images.trvl-media.com/hotels/10000000/9760000/9754700/9754671/88c37982_z.jpg', + }, + { + name: 'Hotel California', + desc: '* *', + payload: 'hotel-Hotel California', + pic: + 'https://estaticos.elperiodico.com/resources/jpg/4/0/hotel-california-todos-santos-baja-california-1493803840904.jpg', + }, + ] + return ( + <> + Select an hotel among these options: + + {hotels.map((e, i) => ( + + + {e.name} + {e.desc} + + + ))} + + + ) + } +} +``` + +## Webviews + +[Webviews](https://botonic.io/docs/concepts/webviews/) allow us to open standard webpages during a chat conversation. In this example, we have used it to create a webpage where the user can check the hotel reservations. As we have mentioned before we can open it using the persistentMenu or with one of the button in the Start action. + +**src/actions/start.jsx** + +```javascript +import React, { useEffect } from 'react' +import { Text, Button } from '@botonic/react' +import CheckReservationsWebview from '../webviews/components/check-reservations' + +export default class extends React.Component { + static async botonicInit(request) { + const name = request.session.user.name + return { name } + } + + render() { + return ( + <> + + Hi {this.props.name}, Im your virtual assistant of Botonic Booking + Platform. I will help you manage your hotel reservations and much + more. + + + Select an option: + + + + + ) + } +} +``` + +In this case, we have also used `styled-components` and the **TextFiled**. + +**src/webviews/components/check-reservations.js** + +```javascript +render() { + this.state.hotels = this.getHotels(this.context) + this.state.correctName = this.getName(this.context) + this.state.correctEmail = this.getEmail(this.context) + + const InfoDatos = (props) => { + return ( + <> +
+ {props.hotel} + + + Name: {this.state.correctName} +
+ Guests: {props.guests} +
+ Date: {props.date} +
+
+ Email: + + {this.state.correctEmail} + +
+ Phone: + {props.phone} +
+
+
+ + ) + } + return ( +
+ {this.state.identified ? ( + <> +

Your reservation

+ {this.state.hotels.map((h, i) => ( + + ))} + + + ) : ( + <> + + To check your reservation, enter your name and email. + + + + + + )} + + ) + } + ``` + +In order to continue with the conversation flow, we call the `closeWebview` function which closes the webview and sends a payload: + +```javascript +close() { + this.context.closeWebview({ + payload: 'close-webview', + }) +} +``` +The last step, is to add the new webview in the index: + +**src/webviews/index.js** + +```javascript +import WebviewReserva from './components/check-reservations' + +export const webviews = [WebviewReserva] +``` + + ...and we are done 🎉 diff --git a/examples/booking-platform/babel.config.js b/examples/booking-platform/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/booking-platform/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/booking-platform/jest.config.js b/examples/booking-platform/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/booking-platform/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/booking-platform/package.json b/examples/booking-platform/package.json new file mode 100644 index 0000000000..83a2b4f539 --- /dev/null +++ b/examples/booking-platform/package.json @@ -0,0 +1,26 @@ +{ + "name": "@botonic/example-booking-platform", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0", + "@date-io/date-fns": "^1.3.13", + "@material-ui/core": "4.11.0", + "@material-ui/lab": "4.0.0-alpha.56", + "@material-ui/pickers": "^3.3.11", + "date-fns": "^2.30.0", + "react-stars": "^2.2.5" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/booking-platform/src/actions/book-hotel.jsx b/examples/booking-platform/src/actions/book-hotel.jsx new file mode 100644 index 0000000000..0a7c979352 --- /dev/null +++ b/examples/booking-platform/src/actions/book-hotel.jsx @@ -0,0 +1,22 @@ +import React from 'react' +import { Text } from '@botonic/react' +import HotelForm from '../webchat/hotel-form-message' + +export default class extends React.Component { + static async botonicInit(request) { + const hotel = request.input.payload.split('-')[1] + const name = request.session.user.name + return { hotel, name } + } + render() { + return ( + <> + + {this.props.name} you have selected **{this.props.hotel}**. To confirm + the reservation, we would need some more information. + + + + ) + } +} diff --git a/examples/booking-platform/src/actions/bye.jsx b/examples/booking-platform/src/actions/bye.jsx new file mode 100644 index 0000000000..c9c6c15ea4 --- /dev/null +++ b/examples/booking-platform/src/actions/bye.jsx @@ -0,0 +1,19 @@ +import React from 'react' +import { Text } from '@botonic/react' +import RateUserMessage from '../webchat/rate-user-message' + +export default class extends React.Component { + static async botonicInit(request) { + const payload = request.input.payload + const rate = payload && payload.split('-')[1] + return { rate } + } + render() { + return ( + <> + {this.props.rate && } + Thanks for contacting us. Have a nice day! + + ) + } +} diff --git a/examples/booking-platform/src/actions/carousel.jsx b/examples/booking-platform/src/actions/carousel.jsx new file mode 100644 index 0000000000..bb4e2fd780 --- /dev/null +++ b/examples/booking-platform/src/actions/carousel.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import { + Text, + Carousel, + Element, + Pic, + Button, + Title, + Subtitle, +} from '@botonic/react' + +export default class extends React.Component { + render() { + const hotels = [ + { + name: 'Hotel Alabama', + desc: '* * * *', + payload: 'hotel-Hotel Alabama', + pic: + 'https://cdn.styleblueprint.com/wp-content/uploads/2017/06/4512594599_9edc8fee0a_b.jpg', + }, + { + name: 'Hotel Arizona', + desc: '* * * * *', + payload: 'hotel-Hotel Arizona', + pic: + 'https://images.trvl-media.com/hotels/10000000/9760000/9754700/9754671/88c37982_z.jpg', + }, + { + name: 'Hotel California', + desc: '* *', + payload: 'hotel-Hotel California', + pic: + 'https://estaticos.elperiodico.com/resources/jpg/4/0/hotel-california-todos-santos-baja-california-1493803840904.jpg', + }, + ] + return ( + <> + Select an hotel among these options: + + {hotels.map((e, i) => ( + + + {e.name} + {e.desc} + + + ))} + + + ) + } +} diff --git a/examples/booking-platform/src/actions/close-webview.jsx b/examples/booking-platform/src/actions/close-webview.jsx new file mode 100644 index 0000000000..0d433e09bf --- /dev/null +++ b/examples/booking-platform/src/actions/close-webview.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import { Text, Button } from '@botonic/react' + +export default class extends React.Component { + render() { + return ( + <> + + If you want to book a hotel, click on the menu on the bottom left + corner and select _Book a hotel_ + + + Is there anything else I can help you with? + + + + + ) + } +} diff --git a/examples/booking-platform/src/actions/info-reservation.jsx b/examples/booking-platform/src/actions/info-reservation.jsx new file mode 100644 index 0000000000..86c1b3e55a --- /dev/null +++ b/examples/booking-platform/src/actions/info-reservation.jsx @@ -0,0 +1,45 @@ +import React from 'react' +import { Text, Button } from '@botonic/react' + +export default class extends React.Component { + static async botonicInit(request) { + const name = request.session.user.name + const email = request.session.user.extra_data.email + const reservationInfo = request.input.payload.split('_') + return { + name, + email, + phone: reservationInfo[1], + people: reservationInfo[2], + date: reservationInfo[3], + } + } + render() { + return ( + <> + + Reservation completed: {'\n'} + **Name**: {this.props.name} + {'\n'} + **Email**: {this.props.email} + {'\n'} + **Phone**: {this.props.phone} + {'\n'} + **Guests**: {this.props.people} + {'\n'} + **Date**: {this.props.date} + {'\n'} + + + If you want to see your reservation, click on the menu on the bottom + left corner and select _Check your reservation_ + + + Is there anything else I can help you with? + + + + + ) + } +} diff --git a/examples/booking-platform/src/actions/more-help.jsx b/examples/booking-platform/src/actions/more-help.jsx new file mode 100644 index 0000000000..0cd9bc03c9 --- /dev/null +++ b/examples/booking-platform/src/actions/more-help.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import { Text, Button } from '@botonic/react' +import WebviewReserva from '../webviews/components/check-reservations' +import RateMessage from '../webchat/rate-message' + +export default class extends React.Component { + static async botonicInit(request) { + const moreHelp = + request.input.payload && request.input.payload.split('-')[1] + return { moreHelp } + } + render() { + return ( + <> + {this.props.moreHelp == 'no' ? ( + + ) : ( + + Select an option: + + + + )} + + ) + } +} diff --git a/examples/booking-platform/src/actions/start.jsx b/examples/booking-platform/src/actions/start.jsx new file mode 100644 index 0000000000..ec87951636 --- /dev/null +++ b/examples/booking-platform/src/actions/start.jsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react' +import { Text, Button } from '@botonic/react' +import CheckReservationsWebview from '../webviews/components/check-reservations' + +export default class extends React.Component { + static async botonicInit(request) { + const name = request.session.user.name + return { name } + } + + render() { + return ( + <> + + Hi {this.props.name}, I'm your virtual assistant of Botonic Booking + Platform. I will help you manage your hotel reservations and much + more. + + + Select an option: + + + + + ) + } +} diff --git a/examples/booking-platform/src/assets/burger-menu.svg b/examples/booking-platform/src/assets/burger-menu.svg new file mode 100644 index 0000000000..fbeb019b8a --- /dev/null +++ b/examples/booking-platform/src/assets/burger-menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/booking-platform/src/assets/cancel.svg b/examples/booking-platform/src/assets/cancel.svg new file mode 100644 index 0000000000..f47a275268 --- /dev/null +++ b/examples/booking-platform/src/assets/cancel.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/booking-platform/src/assets/check-reservation.svg b/examples/booking-platform/src/assets/check-reservation.svg new file mode 100644 index 0000000000..da76fae125 --- /dev/null +++ b/examples/booking-platform/src/assets/check-reservation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/booking-platform/src/assets/close.svg b/examples/booking-platform/src/assets/close.svg new file mode 100644 index 0000000000..21ae175b36 --- /dev/null +++ b/examples/booking-platform/src/assets/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/booking-platform/src/assets/comment.svg b/examples/booking-platform/src/assets/comment.svg new file mode 100644 index 0000000000..e902eb39d4 --- /dev/null +++ b/examples/booking-platform/src/assets/comment.svg @@ -0,0 +1,2 @@ + + diff --git a/examples/booking-platform/src/assets/home.svg b/examples/booking-platform/src/assets/home.svg new file mode 100644 index 0000000000..de1ac8990b --- /dev/null +++ b/examples/booking-platform/src/assets/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/booking-platform/src/assets/hotel.svg b/examples/booking-platform/src/assets/hotel.svg new file mode 100644 index 0000000000..17a26c0336 --- /dev/null +++ b/examples/booking-platform/src/assets/hotel.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/booking-platform/src/assets/send.svg b/examples/booking-platform/src/assets/send.svg new file mode 100644 index 0000000000..a0134ae605 --- /dev/null +++ b/examples/booking-platform/src/assets/send.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/examples/booking-platform/src/index.js b/examples/booking-platform/src/index.js new file mode 100644 index 0000000000..04ead986d9 --- /dev/null +++ b/examples/booking-platform/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0.9 } diff --git a/packages/create-botonic-app/template/bot/src/nlp/tasks/intent-classification/models/en/.gitkeep b/examples/booking-platform/src/locales/.gitkeep similarity index 100% rename from packages/create-botonic-app/template/bot/src/nlp/tasks/intent-classification/models/en/.gitkeep rename to examples/booking-platform/src/locales/.gitkeep diff --git a/examples/booking-platform/src/locales/index.js b/examples/booking-platform/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/booking-platform/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/booking-platform/src/plugins.js b/examples/booking-platform/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/booking-platform/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/booking-platform/src/routes.js b/examples/booking-platform/src/routes.js new file mode 100644 index 0000000000..63d5427f48 --- /dev/null +++ b/examples/booking-platform/src/routes.js @@ -0,0 +1,44 @@ +import Start from './actions/start' +import Carousel from './actions/carousel' +import BookHotel from './actions/book-hotel' +import InfoReservation from './actions/info-reservation' +import CloseWebview from './actions/close-webview' +import Bye from './actions/bye' +import MoreHelp from './actions/more-help' + +export const routes = [ + { path: 'start', text: /^start$/i, action: Start }, + { + path: 'book-hotel', + payload: /hotel-.*/, + action: BookHotel, + }, + { + path: 'info-reservation', + payload: /enviar_.*/, + action: InfoReservation, + }, + { + path: 'close-webview', + payload: 'close-webview', + action: CloseWebview, + }, + { + path: 'carousel', + payload: 'carousel', + text: /^.*\b(hotel|book)\b.*$/i, + action: Carousel, + }, + { + path: 'Bye', + payload: /rating-.*/, + text: /^bye$/i, + action: Bye, + }, + { + path: 'help', + text: /.*/, + payload: /help-.*/, + action: MoreHelp, + }, +] diff --git a/examples/booking-platform/src/utils.js b/examples/booking-platform/src/utils.js new file mode 100644 index 0000000000..68cce76228 --- /dev/null +++ b/examples/booking-platform/src/utils.js @@ -0,0 +1,27 @@ +import React from 'react' +import TextField from '@material-ui/core/TextField' + +export const emailRegex = /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/ + +export function MyTextField(props) { + let helperText = '' + if (props.error) + helperText = + props.error && props.value === '' + ? 'This field is required' + : props.errorMessage || '' + return ( + + ) +} diff --git a/examples/booking-platform/src/webchat/common.js b/examples/booking-platform/src/webchat/common.js new file mode 100644 index 0000000000..0f28c3c86d --- /dev/null +++ b/examples/booking-platform/src/webchat/common.js @@ -0,0 +1,10 @@ +import styled from 'styled-components' + +export const IconContainer = styled.div` + cursor: pointer; + width: 56px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; +` diff --git a/examples/booking-platform/src/webchat/cover-component.js b/examples/booking-platform/src/webchat/cover-component.js new file mode 100644 index 0000000000..ecd33948e8 --- /dev/null +++ b/examples/booking-platform/src/webchat/cover-component.js @@ -0,0 +1,120 @@ +import React from 'react' +import styled from 'styled-components' +import { WebchatContext } from '@botonic/react' +import { emailRegex, MyTextField } from '../utils' + +const Container = styled.div` + position: absolute; + height: calc(100% - 48px); + left: 0; + top: 48px; + background: white; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px 30px 20px 30px; + z-index: 3; +` +const Button = styled.button` + width: 80px; + height: 40px; + background: #2f2f2f; + border-radius: 8px; + margin: 20px; + text-align: center; + color: white; +` + +const Text = styled.a` + position: relative; + fontfamily: Verdana; + fontweight: normal; + fontsize: 14px; + text-align: center; + width: 85%; + line-height: 1.4; + color: #000000; + margin: 0px 30px 20px 30px; +` + +export default class CustomCover extends React.Component { + static contextType = WebchatContext + constructor(props) { + super(props) + this.state = { + name: '', + email: '', + error: false, + } + } + + close() { + if (this.verifiedForm()) { + this.context.updateUser({ + name: this.state.name, + extra_data: { email: this.state.email, hotels: [] }, + }) + this.context.sendText('Start') + this.props.closeComponent() + } else { + this.setState({ error: true }) + } + } + + verifiedForm() { + if (!this.incorrectName() && !this.incorrectEmail()) { + return true + } + return false + } + + incorrectName() { + return this.state.name == '' + } + + incorrectEmail() { + return !this.state.email.match(emailRegex) || this.state.email == '' + } + + handleName = event => { + this.setState({ name: event.target.value }) + } + + handleEmail = event => { + this.setState({ email: event.target.value }) + this.setState({ error: false }) + } + + render() { + return ( + + + Welcome to Botonic Booking Platform First of all, I would need your + name and email. + + + + +

+ + We will not store the fulfilled information. You can fake the data. + +

+
+ ) + } +} diff --git a/examples/booking-platform/src/webchat/custom-button.js b/examples/booking-platform/src/webchat/custom-button.js new file mode 100644 index 0000000000..4689c3df41 --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-button.js @@ -0,0 +1,25 @@ +import React from 'react' +import styled from 'styled-components' + +const StyledButton = styled.div` + cursor: pointer; + padding: 10px 10px; + margin: 5px 10px 10px 10px; + background: white; + border: 1px solid black; + font-size: 15px; + color: black; + text-align: center; + white-space: normal; + &:hover { + opacity: 0.5; + } +` + +export const CustomButton = (props) => { + return ( + + {props.children} + + ) +} diff --git a/examples/booking-platform/src/webchat/custom-header.js b/examples/booking-platform/src/webchat/custom-header.js new file mode 100644 index 0000000000..0de84c2c5b --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-header.js @@ -0,0 +1,51 @@ +import React, { useContext } from 'react' +import styled from 'styled-components' +import { IconContainer } from './common' +import Close from '../assets/cancel.svg' +import Comment from '../assets/comment.svg' +import { staticAsset } from '@botonic/react' + +const Header = styled.div` + height: 48px; + background: #495e86; + z-index: 2; + display: flex; + align-items: center; +` +const Title = styled.h1` + font-family: inherit; + font-size: 16px; + font-weight: 400; + line-height: 1px; + color: #ffffff; + width: 80%; + margin: 0; +` + +export const CustomHeader = () => { + return ( +
+ + + + Botonic Booking Platform + { + Botonic.close() + }} + > + + +
+ ) +} diff --git a/examples/booking-platform/src/webchat/custom-icons.js b/examples/booking-platform/src/webchat/custom-icons.js new file mode 100644 index 0000000000..b5dbde7dfb --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-icons.js @@ -0,0 +1,22 @@ +import React from 'react' +import styled from 'styled-components' +import { IconContainer } from './common' +import Send from '../assets/send.svg' +import BurgerMenu from '../assets/burger-menu.svg' +import { staticAsset } from '@botonic/react' + +export const Icon = styled.img` + width: 18px; +` + +export const CustomSendButton = () => ( + + + +) + +export const CustomMenuButton = () => ( + + + +) diff --git a/examples/booking-platform/src/webchat/custom-persistentMenu-button.js b/examples/booking-platform/src/webchat/custom-persistentMenu-button.js new file mode 100644 index 0000000000..932bf8c352 --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-persistentMenu-button.js @@ -0,0 +1,56 @@ +import React, { useContext } from 'react' +import styled from 'styled-components' +import { WebchatContext } from '@botonic/react' +import { IconContainer } from './common' + +const StyledButton = styled.div` + cursor: pointer; + height: 50px; + width: 100%; + background: #ffffff; + display: flex; + justify-content: left; + align-items: center; + &:hover { + opacity: 0.5; + } +` + +const Text = styled.p` + font-size: 15px; + font-weight: 400; + color: #000000; + text-align: left; + margin: 0; +` + +export const CustomMenuButton = (props) => { + const { sendInput, openWebview } = useContext(WebchatContext) + + const handleClick = (event) => { + if (props.webview) openWebview(props.webview, props.params) + else if (props.payload) { + sendInput({ + type: 'text', + data: String(props.label), + payload: props.payload, + }) + } else if (props.onClick) props.onClick() + } + + return ( + handleClick(e)} + > + + + + {props.label} + + ) +} diff --git a/examples/booking-platform/src/webchat/custom-persistentMenu.js b/examples/booking-platform/src/webchat/custom-persistentMenu.js new file mode 100644 index 0000000000..99e4afeccb --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-persistentMenu.js @@ -0,0 +1,39 @@ +import React, { useContext } from 'react' +import styled from 'styled-components' +import { WebchatContext } from '@botonic/react' +import { CustomMenuButton } from './custom-persistentMenu-button' +import Home from '../assets/home.svg' +import CheckReservation from "../assets/check-reservation.svg" +import Close from "../assets/close.svg" +import { staticAsset } from '@botonic/react' + +const ButtonsContainer = styled.div` + width: 100%; + bottom: 0; + position: absolute; + z-index: 2; + text-align: center; + background: white; +` + +export const CustomPersistentMenu = ({ onClick, options }) => { + return ( + + + + + + ) +} diff --git a/examples/booking-platform/src/webchat/custom-trigger.js b/examples/booking-platform/src/webchat/custom-trigger.js new file mode 100644 index 0000000000..b438f01c56 --- /dev/null +++ b/examples/booking-platform/src/webchat/custom-trigger.js @@ -0,0 +1,75 @@ +import React from 'react' +import styled, { keyframes } from 'styled-components' +import Hotel from '../assets/hotel.svg' +import { staticAsset } from '@botonic/react' + +const AnimatedText = styled.div` + top: 12px; + right: -${(props) => props.widthText}px; + position: absolute; + animation: ${(props) => props.move} 4s; + animation-delay: 1s; + width: ${(props) => props.widthText}px; + opacity: 0; + color: #495e86; + font-family: Arial; + font-weight: 300; + font-size: 13px; + letter-spacing: 0.3px; + line-height: 20px; +` + +const AnimatedContainer = styled.div` + cursor: pointer; + position: fixed; + display: flex; + align-items: center; + justify-content: space-between; + bottom: 16px; + right: 16px; + border-radius: 100px; + height: 44px; + width: 24px; + border: 1px solid #d7d7d8; + background-color: #ffffff; + z-index: 1002; + overflow: hidden; + animation: ${(props) => props.resize} 4s; + animation-delay: 1s; + box-sizing: content-box; + padding: 0px 10px; +` + +export const CustomTrigger = () => { + const widthText = 170 + const maxWidthResize = 15 + widthText + + let move = keyframes` + 0% {right: -${widthText}px; opacity: 0;} + 20% {right: -6px; opacity: 1;} + 80% {right: -6px; opacity: 1;} + 100% {right: -${widthText}px; opacity: 0;} +` + let resize = keyframes` + 0% {width: 24px;} + 20% {width: ${maxWidthResize}px;} + 80% {width: ${maxWidthResize}px;} + 100% {width: 24px;} +` + + return ( + + + + Botonic Booking Platform + + + ) +} diff --git a/examples/booking-platform/src/webchat/hotel-form-message.js b/examples/booking-platform/src/webchat/hotel-form-message.js new file mode 100644 index 0000000000..9dac3a1de2 --- /dev/null +++ b/examples/booking-platform/src/webchat/hotel-form-message.js @@ -0,0 +1,190 @@ +import React from 'react' +import styled from 'styled-components' +import { WebchatContext, customMessage } from '@botonic/react' +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers' +import DateFnsUtils from '@date-io/date-fns' +import deLocale from 'date-fns/locale/en-US' +import Autocomplete from '@material-ui/lab/Autocomplete' +import { MyTextField } from '../utils' + +const Form = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + width: 100%; +` + +const Button = styled.button` + height: 40px; + background: #2f2f2f; + border-radius: 8px; + margin-top: 5px; + text-align: center; + color: white; +` + +class HotelForm extends React.Component { + static contextType = WebchatContext + constructor(props) { + super(props) + this.state = { + phone: '', + guests: '', + date: null, + error: false, + edit: true, + } + } + + formatDate(date) { + return date.toISOString().substring(0, 10).split('-').reverse().join('/') + } + + close() { + if (this.verifiedForm()) { + const date = this.formatDate(this.state.date) + const payload = `enviar_${this.state.phone}_${this.state.guests}_${date}` + this.setState({ edit: false, error: false }) + const formInfo = { + hotel: this.props.hotel, + guests: this.state.guests, + date: date, + phone: this.state.phone, + } + const hotels = this.context.webchatState.session.user.extra_data.hotels + hotels.unshift(formInfo) + this.context.updateUser({ + extra_data: { hotels }, + }) + this.context.sendPayload(payload) + } else { + this.setState({ error: true }) + } + } + + verifiedForm() { + if ( + this.state.phone === '' || + this.state.guests === '' || + this.state.date === null + ) + return false + return true + } + + handlePhone = event => { + this.setState({ phone: event.target.value }) + } + + handleDate = date => { + this.setState({ date: date }) + } + + handleGuests = value => { + this.setState({ guests: value ? value.guests : '' }) + } + + render() { + const guestsOptions = [ + { guests: '1' }, + { guests: '2' }, + { guests: '3' }, + { guests: '4' }, + { guests: '5' }, + ] + return ( +
+

+ + We will not store the fulfilled information. You can fake the data. + +

+ + option.guests} + getOptionSelected={(option, value) => option.guests == value.guests} + onChange={(event, newValue) => { + this.handleGuests(newValue) + }} + style={{ + width: '100%', + margin: '0px -63px 5px 0px', + }} + renderInput={params => ( + + )} + /> + + + + {this.state.edit ? ( + + ) : ( + + )} + + ) + } +} + +export default customMessage({ + name: 'hotel-form', + component: HotelForm, + defaultProps: { + style: { + width: '100%', + backgroundColor: '#ffffff', + border: 'none', + boxShadow: 'none', + paddingLeft: '5px', + }, + imageStyle: { display: 'none' }, + blob: false, + enableTimestamps: false, + }, +}) diff --git a/examples/booking-platform/src/webchat/index.js b/examples/booking-platform/src/webchat/index.js new file mode 100644 index 0000000000..00c5cdebb7 --- /dev/null +++ b/examples/booking-platform/src/webchat/index.js @@ -0,0 +1,108 @@ +import { CustomHeader } from './custom-header' +import { CustomTrigger } from './custom-trigger' +import { CustomPersistentMenu } from './custom-persistentMenu' +import { CustomSendButton, CustomMenuButton } from './custom-icons' +import { CustomButton } from './custom-button' +import CustomCover from './cover-component' +import HotelForm from './hotel-form-message' +import RateMessage from './rate-message' +import RateUserMessage from './rate-user-message' +import Hotel from '../assets/hotel.svg' +import CheckReservationsWebview from '../webviews/components/check-reservations' + +export const webchat = { + storage: sessionStorage, + storageKey: 'botonic-hotel-reservation-example', + coverComponent: CustomCover, + + theme: { + mobileBreakpoint: 460, + style: { + position: 'fixed', + right: 20, + bottom: 20, + width: 400, + height: 500, + margin: 'auto', + backgroundColor: 'white', + borderRadius: 8, + boxShadow: '0 0 50px #C1CED7', + overflow: 'hidden', + fontFamily: 'Arial', + lineHeight: 1.3, + }, + + message: { + customTypes: [HotelForm, RateMessage, RateUserMessage], + bot: { + image: Hotel, + imageStyle: { + alignItems: 'flex-start', + }, + style: { + color: '#000000', + background: '#ffffff', + borderRadius: '5px', + border: '1px solid #495e86', + borderColor: '#495e86', + }, + blobTickStyle: { + alignItems: 'flex-start', + }, + }, + user: { + style: { + color: '#ffffff', + borderRadius: '5px', + background: '#495e86', + }, + }, + timestamps: { + format: () => { + return new Date().toISOString().substring(11, 16) + }, + style: { + color: 'black', + fontFamily: 'Arial', + fontSize: '12px', + padding: '1px 16px 0px 50px', + height: '30px', + marginTop: '-5px', + }, + }, + }, + + userInput: { + style: { + background: 'white', + minHeight: '45px', + }, + box: { + style: { + border: 'none', + color: 'black', + background: 'white', + paddingLeft: 20, + marginRight: 10, + }, + placeholder: 'Write a message...', + }, + + persistentMenu: [ + { label: 'Check your reservation', webview: CheckReservationsWebview }, + { label: 'Book a hotel', payload: 'carousel' }, + { closeLabel: 'Close' }, + ], + menu: { + darkBackground: true, + custom: CustomPersistentMenu, + }, + }, + + customTrigger: CustomTrigger, + customHeader: CustomHeader, + customButton: CustomButton, + customMenuButton: CustomMenuButton, + customSendButton: CustomSendButton, + }, +} diff --git a/examples/booking-platform/src/webchat/rate-message.js b/examples/booking-platform/src/webchat/rate-message.js new file mode 100644 index 0000000000..65115a2a67 --- /dev/null +++ b/examples/booking-platform/src/webchat/rate-message.js @@ -0,0 +1,54 @@ +import React from 'react' +import styled from 'styled-components' +import { WebchatContext, customMessage } from '@botonic/react' +import ReactStars from 'react-stars' + +const Text = styled.p` + color: black; + text-align: flex-start; + margin: 0px; +` + +class RateMessage extends React.Component { + static contextType = WebchatContext + constructor(props) { + super(props) + this.state = { + rate: 0, + edit: true, + } + } + + ratingChanged = (newRating) => { + console.log(newRating) + if (this.state.edit) { + this.setState({ rate: newRating }) + this.setState({ edit: false }) + const payload = `rating-${newRating}` + this.context.sendPayload(payload) + } + } + render() { + return ( + <> + Before we say goodbye, please rate our service + {this.state.edit && ( + + )} + + ) + } +} + +export default customMessage({ + name: 'rate-message', + component: RateMessage, +}) diff --git a/examples/booking-platform/src/webchat/rate-user-message.js b/examples/booking-platform/src/webchat/rate-user-message.js new file mode 100644 index 0000000000..afb5c22224 --- /dev/null +++ b/examples/booking-platform/src/webchat/rate-user-message.js @@ -0,0 +1,26 @@ +import React from 'react' +import { customMessage, WebchatContext } from '@botonic/react' +import ReactStars from 'react-stars' + +class RateUserMessage extends React.Component { + static contextType = WebchatContext + render() { + return ( + + ) + } +} + +export default customMessage({ + name: 'rate-user-message', + component: RateUserMessage, + defaultProps: { + from: 'user', + }, +}) diff --git a/examples/booking-platform/src/webviews/components/check-reservations.js b/examples/booking-platform/src/webviews/components/check-reservations.js new file mode 100644 index 0000000000..1b22ee9f33 --- /dev/null +++ b/examples/booking-platform/src/webviews/components/check-reservations.js @@ -0,0 +1,188 @@ +import React from 'react' +import { RequestContext } from '@botonic/react' +import styled from 'styled-components' +import { MyTextField } from '../../utils' + +const Form = styled.div` + position: absolute; + width: calc(100% - 60px); + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 20px 30px 20px 30px; +` + +const Button = styled.button` + width: 80px; + height: 40px; + background: #2f2f2f; + border-radius: 8px; + margin-top: 20px; + text-align: center; + color: white; +` + +const Text = styled.p` + margin: 5px; + width: 85%; + color: #000000; + line-height: 1.2; +` + +export default class CheckReservationsWebview extends React.Component { + static contextType = RequestContext + constructor(props) { + super(props) + this.state = { + name: '', + email: '', + hotels: undefined, + correctName: '', + correctEmail: '', + errorName: false, + errorEmail: false, + identified: false, + } + } + + close() { + this.context.closeWebview({ + payload: 'close-webview', + }) + } + + singIn() { + if (this.verifiedForm()) { + this.setState({ identified: true }) + } + } + + verifiedForm() { + const correctName = !this.incorrectName() + const correctEmail = !this.incorrectEmail() + return correctName && correctEmail + } + + incorrectName() { + if (this.state.name !== this.state.correctName) { + this.setState({ errorName: true }) + return true + } + return false + } + + incorrectEmail() { + if (this.state.email !== this.state.correctEmail) { + this.setState({ errorEmail: true }) + return true + } + return false + } + + handleName = (event) => { + this.setState({ name: event.target.value, errorName: false }) + } + handleEmail = (event) => { + this.setState({ email: event.target.value, errorEmail: false }) + } + + getName(botContext) { + return botContext.session.user.name + } + + getEmail(botContext) { + return botContext.session.user.extra_data.email + } + + getHotels(botContext) { + return botContext.session.user.extra_data.hotels + } + + render() { + this.state.hotels = this.getHotels(this.context) + this.state.correctName = this.getName(this.context) + this.state.correctEmail = this.getEmail(this.context) + + const InfoDatos = (props) => { + return ( + <> +
+ {props.hotel} + + + Name: {this.state.correctName} +
+ Guests: {props.guests} +
+ Date: {props.date} +
+
+ Email: + + {this.state.correctEmail} + +
+ Phone: + {props.phone} +
+
+
+ + ) + } + return ( +
+ {this.state.identified ? ( + <> +

Your reservation

+ {this.state.hotels.map((h, i) => ( + + ))} + + + ) : ( + <> + + To check your reservation, enter your name and email. + + + + + + )} + + ) + } +} diff --git a/examples/booking-platform/src/webviews/index.js b/examples/booking-platform/src/webviews/index.js new file mode 100644 index 0000000000..6f79d9d2f8 --- /dev/null +++ b/examples/booking-platform/src/webviews/index.js @@ -0,0 +1,3 @@ +import WebviewReserva from './components/check-reservations' + +export const webviews = [WebviewReserva] diff --git a/packages/create-botonic-app/template/bot/src/nlp/tasks/ner/models/en/.gitkeep b/examples/booking-platform/tests/.gitkeep similarity index 100% rename from packages/create-botonic-app/template/bot/src/nlp/tasks/ner/models/en/.gitkeep rename to examples/booking-platform/tests/.gitkeep diff --git a/examples/booking-platform/webpack-entries/dev-entry.js b/examples/booking-platform/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..00aece8237 --- /dev/null +++ b/examples/booking-platform/webpack-entries/dev-entry.js @@ -0,0 +1,14 @@ +import { DevApp } from '@botonic/react' +import { routes } from '../src/routes' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { webchat } from '../src/webchat' +import { config } from '../src' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/booking-platform/webpack-entries/node-entry.js b/examples/booking-platform/webpack-entries/node-entry.js new file mode 100644 index 0000000000..cc3df2f809 --- /dev/null +++ b/examples/booking-platform/webpack-entries/node-entry.js @@ -0,0 +1,7 @@ +import { NodeApp } from '@botonic/react' +import { routes } from '../src/routes' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { config } from '../src' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/booking-platform/webpack-entries/webchat-entry.js b/examples/booking-platform/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..03c0e4485f --- /dev/null +++ b/examples/booking-platform/webpack-entries/webchat-entry.js @@ -0,0 +1,4 @@ +import { WebchatApp } from '@botonic/react' +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/booking-platform/webpack-entries/webviews-entry.js b/examples/booking-platform/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..f9052ba84b --- /dev/null +++ b/examples/booking-platform/webpack-entries/webviews-entry.js @@ -0,0 +1,5 @@ +import { WebviewApp } from '@botonic/react' +import { webviews } from '../src/webviews' +import { locales } from '../src/locales' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/booking-platform/webpack.config.js b/examples/booking-platform/webpack.config.js new file mode 100644 index 0000000000..03250b1239 --- /dev/null +++ b/examples/booking-platform/webpack.config.js @@ -0,0 +1,338 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/childs/.gitignore b/examples/childs/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/childs/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/childs/babel.config.js b/examples/childs/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/childs/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/childs/jest.config.js b/examples/childs/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/childs/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/childs/package.json b/examples/childs/package.json new file mode 100644 index 0000000000..602ed9ff20 --- /dev/null +++ b/examples/childs/package.json @@ -0,0 +1,20 @@ +{ + "name": "@botonic/example-childs", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/childs/src/actions/bacon.jsx b/examples/childs/src/actions/bacon.jsx new file mode 100644 index 0000000000..977bd9090f --- /dev/null +++ b/examples/childs/src/actions/bacon.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return You chose Bacon on Pizza + } +} diff --git a/examples/childs/src/actions/cheese.jsx b/examples/childs/src/actions/cheese.jsx new file mode 100644 index 0000000000..4e2b8fbb19 --- /dev/null +++ b/examples/childs/src/actions/cheese.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return You chose Cheese on Pasta + } +} diff --git a/examples/childs/src/actions/hi.jsx b/examples/childs/src/actions/hi.jsx new file mode 100644 index 0000000000..fa8886c33d --- /dev/null +++ b/examples/childs/src/actions/hi.jsx @@ -0,0 +1,14 @@ +import { Reply, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + + Hi! Choose what you want to eat: + Pizza + Pasta + + ) + } +} diff --git a/examples/childs/src/actions/pasta.jsx b/examples/childs/src/actions/pasta.jsx new file mode 100644 index 0000000000..b6e27f5fdd --- /dev/null +++ b/examples/childs/src/actions/pasta.jsx @@ -0,0 +1,14 @@ +import { Reply, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + + You chose Pasta! Choose one ingredient: + Cheese + Tomato + + ) + } +} diff --git a/examples/childs/src/actions/pizza.jsx b/examples/childs/src/actions/pizza.jsx new file mode 100644 index 0000000000..4fd831c176 --- /dev/null +++ b/examples/childs/src/actions/pizza.jsx @@ -0,0 +1,14 @@ +import { Reply, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + + You chose Pizza! Choose one ingredient: + Sausage + Bacon + + ) + } +} diff --git a/examples/childs/src/actions/sausage.jsx b/examples/childs/src/actions/sausage.jsx new file mode 100644 index 0000000000..b797071e68 --- /dev/null +++ b/examples/childs/src/actions/sausage.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return You chose Sausage on Pizza + } +} diff --git a/examples/childs/src/actions/tomato.jsx b/examples/childs/src/actions/tomato.jsx new file mode 100644 index 0000000000..ee1f920cc0 --- /dev/null +++ b/examples/childs/src/actions/tomato.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return You chose Tomato on Pasta + } +} diff --git a/examples/childs/src/assets/.gitkeep b/examples/childs/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/childs/src/index.js b/examples/childs/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/childs/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/childs/src/locales/.gitkeep b/examples/childs/src/locales/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/childs/src/locales/index.js b/examples/childs/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/childs/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/childs/src/plugins.js b/examples/childs/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/childs/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/childs/src/routes.js b/examples/childs/src/routes.js new file mode 100644 index 0000000000..de6f556b3b --- /dev/null +++ b/examples/childs/src/routes.js @@ -0,0 +1,35 @@ +import Bacon from './actions/bacon' +import Cheese from './actions/cheese' +import Hi from './actions/hi' +import Pasta from './actions/pasta' +import Pizza from './actions/pizza' +import Sausage from './actions/sausage' +import Tomato from './actions/tomato' + +export const routes = [ + { + path: 'hi', + text: /^hi$/i, + action: Hi, + childRoutes: [ + { + path: 'pizza', + payload: /^pizza$/i, + action: Pizza, + childRoutes: [ + { path: 'sausage', payload: /^sausage$/i, action: Sausage }, + { path: 'bacon', payload: /^bacon$/i, action: Bacon }, + ], + }, + { + path: 'pasta', + payload: /^pasta$/i, + action: Pasta, + childRoutes: [ + { path: 'cheese', payload: /^cheese$/i, action: Cheese }, + { path: 'tomato', payload: /^tomato$/i, action: Tomato }, + ], + }, + ], + }, +] diff --git a/examples/childs/src/webchat/index.js b/examples/childs/src/webchat/index.js new file mode 100644 index 0000000000..80a09f176c --- /dev/null +++ b/examples/childs/src/webchat/index.js @@ -0,0 +1 @@ +export const webchat = {} diff --git a/examples/childs/src/webviews/index.js b/examples/childs/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/childs/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/childs/tests/__mocks__/fileMock.js b/examples/childs/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/childs/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/childs/tests/__mocks__/styleMock.js b/examples/childs/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/childs/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/childs/tests/app.test.js b/examples/childs/tests/app.test.js new file mode 100644 index 0000000000..3bc2becfbd --- /dev/null +++ b/examples/childs/tests/app.test.js @@ -0,0 +1,89 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: hi.js', async () => { + const response = await input.text('Hi') + await expect(response).toBe( + output.text( + 'Hi! Choose what you want to eat:', + output.replies( + { text: 'Pizza', payload: 'pizza' }, + { text: 'Pasta', path: 'pasta' } + ) + ) + ) +}) + +test('TEST: pizza.js', async () => { + const response = await input.payload('pizza', undefined, 'hi') + expect(response).toBe( + output.text( + 'You chose Pizza! Choose one ingredient:', + output.replies( + { text: 'Sausage', payload: 'sausage' }, + { text: 'Bacon', payload: 'bacon' } + ) + ) + ) +}) + +test('TEST: sausage.js', async () => { + const response = await input.payload('sausage', undefined, 'hi/pizza') + expect(response).toBe( + output.text('You chose Sausage on Pizza') + ) +}) + +test('TEST: bacon.js', async () => { + const response = await input.path('bacon', undefined, 'hi/pizza') + expect(response).toBe( + output.text('You chose Bacon on Pizza') + ) +}) + +test('TEST: pasta.js', async () => { + const response = await input.payload('pasta', undefined, 'hi') + expect(response).toBe( + output.text( + 'You chose Pasta! Choose one ingredient:', + output.replies( + { text: 'Cheese', payload: 'cheese' }, + { text: 'Tomato', payload: 'tomato' } + ) + ) + ) +}) + +test('TEST: cheese.js', async () => { + const response = await input.payload('cheese', undefined, 'hi/pasta') + expect(response).toBe( + output.text('You chose Cheese on Pasta') + ) +}) + +test('TEST: tomato.js', async () => { + const response = await input.path('tomato', undefined, 'hi/pasta') + expect(response).toBe( + output.text('You chose Tomato on Pasta') + ) +}) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe( + output.text("I don't understand you") + ) +}) diff --git a/examples/childs/webpack-entries/dev-entry.js b/examples/childs/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/childs/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/childs/webpack-entries/node-entry.js b/examples/childs/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/childs/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/childs/webpack-entries/webchat-entry.js b/examples/childs/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/childs/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/childs/webpack-entries/webviews-entry.js b/examples/childs/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/childs/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/childs/webpack.config.js b/examples/childs/webpack.config.js new file mode 100644 index 0000000000..a40a3bf230 --- /dev/null +++ b/examples/childs/webpack.config.js @@ -0,0 +1,339 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/custom-webchat/.gitignore b/examples/custom-webchat/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/custom-webchat/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/custom-webchat/babel.config.js b/examples/custom-webchat/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/custom-webchat/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/custom-webchat/jest.config.js b/examples/custom-webchat/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/custom-webchat/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/custom-webchat/package.json b/examples/custom-webchat/package.json new file mode 100644 index 0000000000..2db1141c14 --- /dev/null +++ b/examples/custom-webchat/package.json @@ -0,0 +1,21 @@ +{ + "name": "@botonic/example-custom-webchat", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0", + "react-calendar": "2.19.2" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/custom-webchat/src/actions/carousel.jsx b/examples/custom-webchat/src/actions/carousel.jsx new file mode 100644 index 0000000000..7fc56b6c45 --- /dev/null +++ b/examples/custom-webchat/src/actions/carousel.jsx @@ -0,0 +1,41 @@ +import { + Button, + Carousel, + Element, + Pic, + RequestContext, + Subtitle, + Title, +} from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + static contextType = RequestContext + + render() { + return ( + + + + Buttons + Buttons + + + + + Replies + Replies + + + + ) + } +} diff --git a/examples/custom-webchat/src/actions/help.jsx b/examples/custom-webchat/src/actions/help.jsx new file mode 100644 index 0000000000..4b46e9044d --- /dev/null +++ b/examples/custom-webchat/src/actions/help.jsx @@ -0,0 +1,22 @@ +import { Text } from '@botonic/react' +import React from 'react' + +import MainCarousel from './carousel' + +export default class extends React.Component { + render() { + return ( + <> + + You can customize me by modifying the components I have under + 'webchat' directory. + + + Play with all the available attributes and let's see if you can + overcome my current styling. + + + + ) + } +} diff --git a/examples/custom-webchat/src/actions/not-found.jsx b/examples/custom-webchat/src/actions/not-found.jsx new file mode 100644 index 0000000000..92cc814d86 --- /dev/null +++ b/examples/custom-webchat/src/actions/not-found.jsx @@ -0,0 +1,8 @@ +import { Button, Reply, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return I don't understand you + } +} diff --git a/examples/custom-webchat/src/actions/show-buttons.jsx b/examples/custom-webchat/src/actions/show-buttons.jsx new file mode 100644 index 0000000000..f3c8d363f5 --- /dev/null +++ b/examples/custom-webchat/src/actions/show-buttons.jsx @@ -0,0 +1,26 @@ +import { Button, RequestContext, Text } from '@botonic/react' +import React from 'react' + +import { MyWebview } from '../webviews/my-webview' + +export default class extends React.Component { + constructor(props) { + super(props) + } + static contextType = RequestContext + + render() { + return ( + <> + + What about these buttons? + + + + + + ) + } +} diff --git a/examples/custom-webchat/src/actions/show-replies.jsx b/examples/custom-webchat/src/actions/show-replies.jsx new file mode 100644 index 0000000000..77fd349d5a --- /dev/null +++ b/examples/custom-webchat/src/actions/show-replies.jsx @@ -0,0 +1,24 @@ +import { Reply, RequestContext, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + constructor(props) { + super(props) + } + static contextType = RequestContext + + render() { + return ( + <> + + Look at these nice replies! + First Reply + Second Reply + Third Reply + Fourth Reply + Fifth Reply + + + ) + } +} diff --git a/examples/custom-webchat/src/actions/start.jsx b/examples/custom-webchat/src/actions/start.jsx new file mode 100644 index 0000000000..c1a6754da1 --- /dev/null +++ b/examples/custom-webchat/src/actions/start.jsx @@ -0,0 +1,24 @@ +import { Button, Reply, Text } from '@botonic/react' +import React from 'react' + +import CalendarMessage from '../webchat/calendar-message' + +export default class extends React.Component { + render() { + return ( + <> + + This is an example bot of how to customize your webchat. + + + For example, this is a custom message type: + + + Something else? + Show me replies + Show me buttons + + + ) + } +} diff --git a/examples/custom-webchat/src/assets/c3po-logo.png b/examples/custom-webchat/src/assets/c3po-logo.png new file mode 100644 index 0000000000..2e0af5dbe2 Binary files /dev/null and b/examples/custom-webchat/src/assets/c3po-logo.png differ diff --git a/examples/custom-webchat/src/assets/intro-image.jpg b/examples/custom-webchat/src/assets/intro-image.jpg new file mode 100644 index 0000000000..a2797a9c82 Binary files /dev/null and b/examples/custom-webchat/src/assets/intro-image.jpg differ diff --git a/examples/custom-webchat/src/assets/launcher-logo.png b/examples/custom-webchat/src/assets/launcher-logo.png new file mode 100644 index 0000000000..df45344dbd Binary files /dev/null and b/examples/custom-webchat/src/assets/launcher-logo.png differ diff --git a/examples/custom-webchat/src/assets/r2d2-logo.png b/examples/custom-webchat/src/assets/r2d2-logo.png new file mode 100644 index 0000000000..e80944bd3a Binary files /dev/null and b/examples/custom-webchat/src/assets/r2d2-logo.png differ diff --git a/examples/custom-webchat/src/assets/replies.png b/examples/custom-webchat/src/assets/replies.png new file mode 100644 index 0000000000..9abfe6f135 Binary files /dev/null and b/examples/custom-webchat/src/assets/replies.png differ diff --git a/examples/custom-webchat/src/assets/trigger-button.png b/examples/custom-webchat/src/assets/trigger-button.png new file mode 100644 index 0000000000..df45344dbd Binary files /dev/null and b/examples/custom-webchat/src/assets/trigger-button.png differ diff --git a/examples/custom-webchat/src/index.js b/examples/custom-webchat/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/custom-webchat/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/custom-webchat/src/locales/index.js b/examples/custom-webchat/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/custom-webchat/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/custom-webchat/src/plugins.js b/examples/custom-webchat/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/custom-webchat/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/custom-webchat/src/routes.js b/examples/custom-webchat/src/routes.js new file mode 100644 index 0000000000..4461207c9d --- /dev/null +++ b/examples/custom-webchat/src/routes.js @@ -0,0 +1,20 @@ +import Help from './actions/help' +import NotFound from './actions/not-found' +import ShowButtons from './actions/show-buttons' +import ShowReplies from './actions/show-replies' +import Start from './actions/start' +export const routes = [ + { path: 'help', payload: 'help', action: Help }, + { + path: 'buttons', + input: i => i.payload == 'buttons' || i.data == 'buttons', + action: ShowButtons, + }, + { + path: 'replies', + input: i => i.payload == 'replies' || i.data == 'replies', + action: ShowReplies, + }, + { path: 'start', text: /^(hi|start|hello)$/i, action: Start }, + { path: '404', text: /.*/, action: NotFound }, +] diff --git a/packages/create-botonic-app/dev-template/webchat/custom-messages/calendar-message.jsx b/examples/custom-webchat/src/webchat/calendar-message.js similarity index 79% rename from packages/create-botonic-app/dev-template/webchat/custom-messages/calendar-message.jsx rename to examples/custom-webchat/src/webchat/calendar-message.js index 947e21c9eb..6d4cdfd4c6 100644 --- a/packages/create-botonic-app/dev-template/webchat/custom-messages/calendar-message.jsx +++ b/examples/custom-webchat/src/webchat/calendar-message.js @@ -1,6 +1,4 @@ -import 'react-calendar/dist/Calendar.css' - -import { customMessage, WebchatContext } from '@botonic/react/src/experimental' +import { customMessage, WebchatContext } from '@botonic/react' import React from 'react' import Calendar from 'react-calendar' diff --git a/examples/custom-webchat/src/webchat/custom-button.js b/examples/custom-webchat/src/webchat/custom-button.js new file mode 100644 index 0000000000..e97b1c4f6f --- /dev/null +++ b/examples/custom-webchat/src/webchat/custom-button.js @@ -0,0 +1,16 @@ +import React from 'react' + +export const CustomButton = props => ( +
+ {props.children} +
+) diff --git a/examples/custom-webchat/src/webchat/custom-header.js b/examples/custom-webchat/src/webchat/custom-header.js new file mode 100644 index 0000000000..a57af85c39 --- /dev/null +++ b/examples/custom-webchat/src/webchat/custom-header.js @@ -0,0 +1,52 @@ +import { staticAsset } from '@botonic/react' +import React from 'react' + +import Icon from '../assets/r2d2-logo.png' + +export const CustomHeader = () => { + return ( +
+ +

+ My customized header +

+
{ + Botonic.close() + }} + > + ✕ +
+
+ ) +} diff --git a/examples/custom-webchat/src/webchat/custom-intro.js b/examples/custom-webchat/src/webchat/custom-intro.js new file mode 100644 index 0000000000..22c792a354 --- /dev/null +++ b/examples/custom-webchat/src/webchat/custom-intro.js @@ -0,0 +1,7 @@ +import { staticAsset } from '@botonic/react' +import React from 'react' + +import Img from '../assets/intro-image.jpg' +export const CustomIntro = () => { + return +} diff --git a/examples/custom-webchat/src/webchat/custom-reply.js b/examples/custom-webchat/src/webchat/custom-reply.js new file mode 100644 index 0000000000..6585d122ba --- /dev/null +++ b/examples/custom-webchat/src/webchat/custom-reply.js @@ -0,0 +1,16 @@ +import React from 'react' + +export const CustomReply = props => ( +
+ {props.children} +
+) diff --git a/examples/custom-webchat/src/webchat/custom-trigger.js b/examples/custom-webchat/src/webchat/custom-trigger.js new file mode 100644 index 0000000000..1ab3d58dcb --- /dev/null +++ b/examples/custom-webchat/src/webchat/custom-trigger.js @@ -0,0 +1,35 @@ +import { staticAsset } from '@botonic/react' +import React from 'react' + +import Icon from '../assets/trigger-button.png' + +export const CustomTrigger = () => { + return ( +
+ +

I am customizable

+
+ ) +} diff --git a/examples/custom-webchat/src/webchat/index.js b/examples/custom-webchat/src/webchat/index.js new file mode 100644 index 0000000000..41550d4dee --- /dev/null +++ b/examples/custom-webchat/src/webchat/index.js @@ -0,0 +1,194 @@ +import C3POLogo from '../assets/c3po-logo.png' +import IntroImage from '../assets/intro-image.jpg' +import launcherIcon from '../assets/launcher-logo.png' +import R2D2Logo from '../assets/r2d2-logo.png' +import CalendarMessage from './calendar-message' +import { CustomButton } from './custom-button' +import { CustomHeader } from './custom-header' +import { CustomIntro } from './custom-intro' +import { CustomReply } from './custom-reply' +import { CustomTrigger } from './custom-trigger' + +export const webchat = { + theme: { + mobileBreakpoint: 460, + style: { + position: 'fixed', + right: 20, + bottom: 20, + width: 400, + height: 500, + margin: 'auto', + backgroundColor: 'white', + borderRadius: 25, + boxShadow: '0 0 50px rgba(0,0,255,.30)', + overflow: 'hidden', + backgroundImage: + 'linear-gradient(to top, #ffffff,#ffffff 11%,#9a9ae3 40%,#0000ff 85%,#0000ff 85%)', + fontFamily: '"Comic Sans MS", cursive, sans-serif', + }, + webview: { + style: { + top: 0, + right: 0, + height: 500, + width: '100%', + }, + header: { + style: { + background: '#6677FF', + }, + }, + }, + + brand: { + // color: 'blue', + image: R2D2Logo, + }, + triggerButton: { + image: launcherIcon, + style: { + width: '200px', + }, + // custom: CustomTrigger, + }, + intro: { + // image: IntroImage, + // style: { + // padding: 20 + // } + custom: CustomIntro, + }, + header: { + title: 'My customized webchat', + subtitle: 'R2D2', + image: R2D2Logo, + style: { + height: 70, + }, + // custom: CustomHeader + }, + /* + * brandImage will set both headerImage and botMessageImage with its current logo + * you can overwrite these values by redefining them individually + */ + message: { + bot: { + image: C3POLogo, // set it to 'null' to hide this image + style: { + border: 'none', + color: 'black', + borderRadius: '20px', + background: '#e1fcfb', + }, + }, + user: { + style: { + // border:'none', + color: 'white', + background: '#2b81b6', + borderRadius: '10px', + }, + }, + customTypes: [CalendarMessage], + }, + + button: { + style: { + color: 'black', + background: 'white', + borderRadius: 20, + }, + hoverBackground: '#b3fcfa', + hoverTextColor: 'black', + + // custom: CustomButton, + }, + replies: { + align: 'center', + wrap: 'nowrap', + }, + reply: { + style: { + color: 'black', + background: '#e1fcfb', + borderColor: 'black', + }, + // custom: CustomReply, + }, + userInput: { + style: { + background: 'black', + }, + box: { + style: { + border: '2px solid #2b81b6', + color: '#2b81b6', + background: '#F0F0F0', + width: '90%', + borderRadius: 20, + paddingLeft: 20, + marginRight: 10, + }, + placeholder: 'Type something...', + }, + + // enable: false, + attachments: { + enable: true, + }, + + emojiPicker: true, + // These are the set of inputs which are not allowed. + blockInputs: [ + { + match: [/ugly/i, /bastard/i], + message: 'We cannot tolerate these kind of words.', + }, + ], + persistentMenu: [ + { label: 'Help', payload: 'help' }, + { + label: 'See docs', + url: 'https://botonic.io/docs/welcome/', + }, + { closeLabel: 'Close' }, + ], + }, + scrollbar: { + // enable: false, + autoHide: true, + thumb: { + opacity: 1, + // color: 'yellow', + bgcolor: + 'linear-gradient(-131deg,rgba(231, 176, 43) 0%,rgb(193, 62, 81) 100%);', + border: '20px', + }, + // track: { + // color: 'black', + // bgcolor: + // 'linear-gradient(-131deg,rgba(50, 40, 43) 0%,rgb(125, 62, 81) 100%);', + // border: '20px', + // }, + }, + }, + + // Webchat listeners + onInit: app => { + // You can combine webchat listeners with the Webchat SDK's Api in order + // to obtain extra functionalities. This will open automatically the webchat. + app.open() + }, + onOpen: app => { + // app.addBotText('Hi human!') + // app.addUserText('Hi bot!') + // app.addUserPayload('POSTBACK_INITCHAT') + }, + onClose: app => { + console.log('I have been closed!') + }, + onMessage: app => { + console.log('New message!') + }, +} diff --git a/examples/custom-webchat/src/webviews/index.js b/examples/custom-webchat/src/webviews/index.js new file mode 100644 index 0000000000..4a7960cd6b --- /dev/null +++ b/examples/custom-webchat/src/webviews/index.js @@ -0,0 +1,2 @@ +import { MyWebview } from './my-webview' +export const webviews = [MyWebview] diff --git a/examples/custom-webchat/src/webviews/my-webview.js b/examples/custom-webchat/src/webviews/my-webview.js new file mode 100644 index 0000000000..6b9033f3a9 --- /dev/null +++ b/examples/custom-webchat/src/webviews/my-webview.js @@ -0,0 +1,48 @@ +import { RequestContext } from '@botonic/react' +import React from 'react' + +export class MyWebview extends React.Component { + static contextType = RequestContext + state = { + counter: 0, + } + + componentDidMount() { + document.title = 'MyBot | MyWebview' + } + + handleClick() { + this.setState({ + counter: this.state.counter + 1, + }) + } + + close() { + // Here we want to explicitly emit a message after closing a webview. + this.context.closeWebview({ + payload: 'closed_webview', + }) + } + + render() { + return ( +
+

This is a Botonic Webview!

+ +

{this.state.counter}

+ +
+ ) + } +} diff --git a/examples/custom-webchat/tests/__mocks__/fileMock.js b/examples/custom-webchat/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/custom-webchat/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/custom-webchat/tests/__mocks__/styleMock.js b/examples/custom-webchat/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/custom-webchat/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/custom-webchat/tests/app.test.js b/examples/custom-webchat/tests/app.test.js new file mode 100644 index 0000000000..35bf4b6e75 --- /dev/null +++ b/examples/custom-webchat/tests/app.test.js @@ -0,0 +1,21 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) + diff --git a/examples/custom-webchat/webpack-entries/dev-entry.js b/examples/custom-webchat/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/custom-webchat/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/custom-webchat/webpack-entries/node-entry.js b/examples/custom-webchat/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/custom-webchat/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/custom-webchat/webpack-entries/webchat-entry.js b/examples/custom-webchat/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/custom-webchat/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/custom-webchat/webpack-entries/webviews-entry.js b/examples/custom-webchat/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/custom-webchat/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/custom-webchat/webpack.config.js b/examples/custom-webchat/webpack.config.js new file mode 100644 index 0000000000..03250b1239 --- /dev/null +++ b/examples/custom-webchat/webpack.config.js @@ -0,0 +1,338 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/dynamic-carousel/.gitignore b/examples/dynamic-carousel/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/dynamic-carousel/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/dynamic-carousel/babel.config.js b/examples/dynamic-carousel/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/dynamic-carousel/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/dynamic-carousel/jest.config.js b/examples/dynamic-carousel/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/dynamic-carousel/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/dynamic-carousel/package.json b/examples/dynamic-carousel/package.json new file mode 100644 index 0000000000..3fa09eae10 --- /dev/null +++ b/examples/dynamic-carousel/package.json @@ -0,0 +1,21 @@ +{ + "name": "@botonic/example-dynamic-carousel", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0", + "isomorphic-fetch": "^2.2.1" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/dynamic-carousel/src/actions/get-shirts.jsx b/examples/dynamic-carousel/src/actions/get-shirts.jsx new file mode 100644 index 0000000000..4657e96a9b --- /dev/null +++ b/examples/dynamic-carousel/src/actions/get-shirts.jsx @@ -0,0 +1,49 @@ +import { + Button, + Carousel, + Element, + Pic, + RequestContext, + Subtitle, + Title, +} from '@botonic/react' +import fetch from 'isomorphic-fetch' +import React from 'react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit({ input, session, params, lastRoutePath }) { + /* This is how you fetch data from an API: */ + //const res = await fetch('https://api.example.com/user') + //const user = await res.json() + + const api_key = 'YOUR_API_KEY' // pragma: allowlist secret + const url = + 'http://api.shopstyle.com/api/v2/products?pid=' + + api_key + + '&fts=' + + input.data + + '&offset=0&limit=5' + const res = await fetch(url, { + url: url, + method: 'GET', + params: {}, + }) + session.resp = await res.json() + } + + render() { + return ( + + {this.context.session.resp.products.map((e, i) => ( + + + {e.name} + {e.priceLabel} + + + ))} + + ) + } +} diff --git a/examples/dynamic-carousel/src/actions/hi.jsx b/examples/dynamic-carousel/src/actions/hi.jsx new file mode 100644 index 0000000000..339d751e6f --- /dev/null +++ b/examples/dynamic-carousel/src/actions/hi.jsx @@ -0,0 +1,14 @@ +import { Reply, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + + Hey, what clothes are you interested in? + Mens shirts + Womens shirts + + ) + } +} diff --git a/examples/dynamic-carousel/src/assets/.gitkeep b/examples/dynamic-carousel/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/dynamic-carousel/src/index.js b/examples/dynamic-carousel/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/dynamic-carousel/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/dynamic-carousel/src/locales/.gitkeep b/examples/dynamic-carousel/src/locales/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/dynamic-carousel/src/locales/index.js b/examples/dynamic-carousel/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/dynamic-carousel/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/dynamic-carousel/src/plugins.js b/examples/dynamic-carousel/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/dynamic-carousel/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/dynamic-carousel/src/routes.js b/examples/dynamic-carousel/src/routes.js new file mode 100644 index 0000000000..cf35492737 --- /dev/null +++ b/examples/dynamic-carousel/src/routes.js @@ -0,0 +1,11 @@ +import GetShirts from './actions/get-shirts' +import Hi from './actions/hi' + +export const routes = [ + /* The first rule matches if and only if we get the text 'hi' and will execute the + React component defined in pages/actions/hi.js */ + { path: 'hi', text: 'hi', action: Hi }, + + /* These rules capture different payloads */ + { path: 'shirts', payload: /(women-shirts|men-shirts)/, action: GetShirts }, +] diff --git a/examples/dynamic-carousel/src/webchat/index.js b/examples/dynamic-carousel/src/webchat/index.js new file mode 100644 index 0000000000..80a09f176c --- /dev/null +++ b/examples/dynamic-carousel/src/webchat/index.js @@ -0,0 +1 @@ +export const webchat = {} diff --git a/examples/dynamic-carousel/src/webviews/index.js b/examples/dynamic-carousel/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/dynamic-carousel/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/dynamic-carousel/tests/__mocks__/fileMock.js b/examples/dynamic-carousel/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/dynamic-carousel/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/dynamic-carousel/tests/__mocks__/styleMock.js b/examples/dynamic-carousel/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/dynamic-carousel/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/dynamic-carousel/tests/app.test.js b/examples/dynamic-carousel/tests/app.test.js new file mode 100644 index 0000000000..e9d89624ea --- /dev/null +++ b/examples/dynamic-carousel/tests/app.test.js @@ -0,0 +1,20 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) diff --git a/examples/dynamic-carousel/webpack-entries/dev-entry.js b/examples/dynamic-carousel/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/dynamic-carousel/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/dynamic-carousel/webpack-entries/node-entry.js b/examples/dynamic-carousel/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/dynamic-carousel/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/dynamic-carousel/webpack-entries/webchat-entry.js b/examples/dynamic-carousel/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/dynamic-carousel/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/dynamic-carousel/webpack-entries/webviews-entry.js b/examples/dynamic-carousel/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/dynamic-carousel/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/dynamic-carousel/webpack.config.js b/examples/dynamic-carousel/webpack.config.js new file mode 100644 index 0000000000..03250b1239 --- /dev/null +++ b/examples/dynamic-carousel/webpack.config.js @@ -0,0 +1,338 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/dynamodb/.gitignore b/examples/dynamodb/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/dynamodb/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/dynamodb/babel.config.js b/examples/dynamodb/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/dynamodb/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/dynamodb/jest.config.js b/examples/dynamodb/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/dynamodb/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/dynamodb/package.json b/examples/dynamodb/package.json new file mode 100644 index 0000000000..5fab969d62 --- /dev/null +++ b/examples/dynamodb/package.json @@ -0,0 +1,21 @@ +{ + "name": "@botonic/example-dynamodb", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/plugin-dynamodb": "0.25.0-beta.0", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/dynamodb/src/actions/hi.jsx b/examples/dynamodb/src/actions/hi.jsx new file mode 100644 index 0000000000..a3d9e7daf0 --- /dev/null +++ b/examples/dynamodb/src/actions/hi.jsx @@ -0,0 +1,18 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + static async botonicInit({ plugins, session }) { + const user = session.user.id + const botId = session.bot.id + + try { + await plugins.track.track(botId, user, { arg1: 'val1' }) + } catch (e) { + console.error(e) + } + } + render() { + return Hi + } +} diff --git a/examples/dynamodb/src/assets/.gitkeep b/examples/dynamodb/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/dynamodb/src/index.js b/examples/dynamodb/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/dynamodb/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/dynamodb/src/locales/index.js b/examples/dynamodb/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/dynamodb/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/dynamodb/src/plugins.js b/examples/dynamodb/src/plugins.js new file mode 100644 index 0000000000..fa285ac1e3 --- /dev/null +++ b/examples/dynamodb/src/plugins.js @@ -0,0 +1,13 @@ +export const plugins = [ + { + id: 'track', + resolve: require('@botonic/plugin-dynamodb'), + options: { + // TODO update configuration below + env: 'dev', + accessKeyId: 'YOUR AWS ACCESS KEY HERE', + secretAccessKey: 'YOUR AWS SECRET KEY HERE', // pragma: allowlist secret + region: 'eu-west-1', + }, + }, +] diff --git a/examples/dynamodb/src/routes.js b/examples/dynamodb/src/routes.js new file mode 100644 index 0000000000..45fde1e510 --- /dev/null +++ b/examples/dynamodb/src/routes.js @@ -0,0 +1,3 @@ +import Hi from './actions/hi' + +export const routes = [{ path: 'hi', text: 'hi', action: Hi }] diff --git a/examples/dynamodb/src/webchat/index.js b/examples/dynamodb/src/webchat/index.js new file mode 100644 index 0000000000..80a09f176c --- /dev/null +++ b/examples/dynamodb/src/webchat/index.js @@ -0,0 +1 @@ +export const webchat = {} diff --git a/examples/dynamodb/src/webviews/index.js b/examples/dynamodb/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/dynamodb/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/dynamodb/tests/__mocks__/fileMock.js b/examples/dynamodb/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/dynamodb/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/dynamodb/tests/__mocks__/styleMock.js b/examples/dynamodb/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/dynamodb/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/dynamodb/tests/app.test.js b/examples/dynamodb/tests/app.test.js new file mode 100644 index 0000000000..e9d89624ea --- /dev/null +++ b/examples/dynamodb/tests/app.test.js @@ -0,0 +1,20 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) diff --git a/examples/dynamodb/webpack-entries/dev-entry.js b/examples/dynamodb/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/dynamodb/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/dynamodb/webpack-entries/node-entry.js b/examples/dynamodb/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/dynamodb/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/dynamodb/webpack-entries/webchat-entry.js b/examples/dynamodb/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/dynamodb/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/dynamodb/webpack-entries/webviews-entry.js b/examples/dynamodb/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/dynamodb/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/dynamodb/webpack.config.js b/examples/dynamodb/webpack.config.js new file mode 100644 index 0000000000..fe92e04b77 --- /dev/null +++ b/examples/dynamodb/webpack.config.js @@ -0,0 +1,340 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + // important to exclude @botonic/dynamo. Otherwise, the plugin does not export the + // class within a "default" key, and you'd get "Uncaught TypeError: Plugin is not a constructor" + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/handoff/.gitignore b/examples/handoff/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/handoff/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/handoff/babel.config.js b/examples/handoff/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/handoff/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/handoff/jest.config.js b/examples/handoff/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/handoff/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/handoff/package.json b/examples/handoff/package.json new file mode 100644 index 0000000000..76618ead12 --- /dev/null +++ b/examples/handoff/package.json @@ -0,0 +1,20 @@ +{ + "name": "@botonic/example-handoff", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/handoff/src/actions/thanks.jsx b/examples/handoff/src/actions/thanks.jsx new file mode 100644 index 0000000000..53cc9fa641 --- /dev/null +++ b/examples/handoff/src/actions/thanks.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return Thanks for contacting us! + } +} diff --git a/examples/handoff/src/actions/transfer-agent.jsx b/examples/handoff/src/actions/transfer-agent.jsx new file mode 100644 index 0000000000..f381bf5485 --- /dev/null +++ b/examples/handoff/src/actions/transfer-agent.jsx @@ -0,0 +1,53 @@ +import { + getAvailableAgents, + getOpenQueues, + HandOffBuilder, +} from '@botonic/core' +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + static async botonicInit({ input, session, params, lastRoutePath }) { + /* + Uncomment the lines below before deploying the bot to Hubtype + in order to test the getOpenQueues call for 'Customer Support'. + */ + // let openQueues = await getOpenQueues(session) + let agentEmail = '' + try { + agentEmail = ( + await getAvailableAgents(session, 'HUBTYPE_DESK_QUEUE_ID') + ).filter(agent => agent == 'agent-name@hubtype.com')[0] + } catch (e) {} + + let isHandOff = false + // if (openQueues.queues.indexOf('Customer Support') !== -1) { + const handOffBuilder = new HandOffBuilder(session) + handOffBuilder.withQueue('HUBTYPE_DESK_QUEUE_ID') + handOffBuilder.withAgentEmail('agent-1@hubtype.com') + handOffBuilder.withOnFinishPath('thanks-for-contacting') // or handOffBuilder.withOnFinishPayload('thanks-for-contacting') + handOffBuilder.withCaseInfo( + 'This is some case information that will be available in the new created case' + ) + handOffBuilder.withNote( + 'This is a note that will be attached to the case as a reminder' + ) + await handOffBuilder.handOff() + + isHandOff = true + // } + return { isHandOff } + } + + render() { + if (this.props.isHandOff) { + return You are being transferred to an agent! + } else { + return ( + + Sorry, right now we can't serve you... Please contact us later! + + ) + } + } +} diff --git a/examples/handoff/src/assets/.gitkeep b/examples/handoff/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/handoff/src/index.js b/examples/handoff/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/handoff/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/handoff/src/locales/.gitkeep b/examples/handoff/src/locales/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/handoff/src/locales/index.js b/examples/handoff/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/handoff/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/handoff/src/plugins.js b/examples/handoff/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/handoff/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/handoff/src/routes.js b/examples/handoff/src/routes.js new file mode 100644 index 0000000000..fc915865bf --- /dev/null +++ b/examples/handoff/src/routes.js @@ -0,0 +1,7 @@ +import Thanks from './actions/thanks' +import TransferAgent from './actions/transfer-agent' + +export const routes = [ + { path: 'agent', text: /^handoff$/i, action: TransferAgent }, + { path: 'thanks-for-contacting', action: Thanks }, +] diff --git a/examples/handoff/src/webchat/index.js b/examples/handoff/src/webchat/index.js new file mode 100644 index 0000000000..80a09f176c --- /dev/null +++ b/examples/handoff/src/webchat/index.js @@ -0,0 +1 @@ +export const webchat = {} diff --git a/examples/handoff/src/webviews/index.js b/examples/handoff/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/handoff/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/handoff/tests/__mocks__/fileMock.js b/examples/handoff/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/handoff/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/handoff/tests/__mocks__/styleMock.js b/examples/handoff/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/handoff/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/handoff/tests/app.test.js b/examples/handoff/tests/app.test.js new file mode 100644 index 0000000000..e9d89624ea --- /dev/null +++ b/examples/handoff/tests/app.test.js @@ -0,0 +1,20 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, plugins, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text("I don't understand you")) +}) diff --git a/examples/handoff/webpack-entries/dev-entry.js b/examples/handoff/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/handoff/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/handoff/webpack-entries/node-entry.js b/examples/handoff/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/handoff/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/handoff/webpack-entries/webchat-entry.js b/examples/handoff/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/handoff/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/handoff/webpack-entries/webviews-entry.js b/examples/handoff/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/handoff/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/handoff/webpack.config.js b/examples/handoff/webpack.config.js new file mode 100644 index 0000000000..a40a3bf230 --- /dev/null +++ b/examples/handoff/webpack.config.js @@ -0,0 +1,339 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/intent/.gitignore b/examples/intent/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/intent/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/intent/babel.config.js b/examples/intent/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/intent/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/intent/jest.config.js b/examples/intent/jest.config.js new file mode 100644 index 0000000000..d2514ba654 --- /dev/null +++ b/examples/intent/jest.config.js @@ -0,0 +1,18 @@ +const path = require('path') + +module.exports = { + rootDir: "tests", + transform: { + "^.+\\.jsx?$": [ + "babel-jest", + { "configFile": path.resolve(__dirname, "babel.config.js") }, + ], + }, + transformIgnorePatterns: [ + "/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$" + ], + moduleNameMapper: { + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(scss|css|less)$": "/__mocks__/styleMock.js" + } +} \ No newline at end of file diff --git a/examples/intent/package.json b/examples/intent/package.json new file mode 100644 index 0000000000..67adbd7b0b --- /dev/null +++ b/examples/intent/package.json @@ -0,0 +1,21 @@ +{ + "name": "@botonic/example-intent", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@botonic/plugin-dialogflow": "0.25.0-beta.0", + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/intent/src/actions/404.js b/examples/intent/src/actions/404.js new file mode 100644 index 0000000000..5e2f74dc03 --- /dev/null +++ b/examples/intent/src/actions/404.js @@ -0,0 +1,19 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + static async botonicInit({ input }) { + if (input['intent'] === undefined) return { errors: true } + } + + render() { + if (this.props.errors) + return ( + + Enter the generated JSON key for dialogflowV2 in plugins.js to test + the bot. + + ) + return Try typing "hello" to start the bot. + } +} diff --git a/examples/intent/src/actions/bye.js b/examples/intent/src/actions/bye.js new file mode 100644 index 0000000000..84a4863093 --- /dev/null +++ b/examples/intent/src/actions/bye.js @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return Bye bye! 👋 + } +} diff --git a/examples/intent/src/actions/hi.js b/examples/intent/src/actions/hi.js new file mode 100644 index 0000000000..140cade017 --- /dev/null +++ b/examples/intent/src/actions/hi.js @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return Hello! 👋 + } +} diff --git a/examples/intent/src/assets/.gitkeep b/examples/intent/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/intent/src/index.js b/examples/intent/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/intent/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/intent/src/locales/.gitkeep b/examples/intent/src/locales/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/intent/src/locales/index.js b/examples/intent/src/locales/index.js new file mode 100644 index 0000000000..1fb527a896 --- /dev/null +++ b/examples/intent/src/locales/index.js @@ -0,0 +1 @@ +export const locales = {} diff --git a/examples/intent/src/plugins.js b/examples/intent/src/plugins.js new file mode 100644 index 0000000000..93a8e0909d --- /dev/null +++ b/examples/intent/src/plugins.js @@ -0,0 +1,23 @@ +import * as pluginDialogflow from '@botonic/plugin-dialogflow' + +export const plugins = [ + { + id: 'dialogflow', + resolve: pluginDialogflow, + // Copy-past here the generated JSON: https://dialogflow.com/docs/reference/v2-auth-setup + options: { + credentials: { + type: 'service_account', + project_id: 'YOUR_PROJECT_ID', + private_key_id: 'YOUR_PRIVATE_KEY_ID', + private_key: 'YOUR_PRIVATE_KEY', // pragma: allowlist secret + client_email: 'YOUR_CLIENT_EMAIL', + client_id: 'CLIENT_ID', + auth_uri: 'AUTH_URI', + token_uri: 'TOKEN_URI', + auth_provider_x509_cert_url: 'AUT_PROVIDER_X509_CERT_URL', + client_x509_cert_url: 'CLIENT_X509_CERT_URL', + }, + }, + }, +] diff --git a/examples/intent/src/routes.js b/examples/intent/src/routes.js new file mode 100644 index 0000000000..0ae80ccadd --- /dev/null +++ b/examples/intent/src/routes.js @@ -0,0 +1,23 @@ +import NotFound from './actions/404' +import Bye from './actions/bye' +import Hi from './actions/hi' + +export const routes = [ + // Captures different intents (enable Dialogflow in src/plugins.js) + // You can trigger your actions through an intent with 'input' or 'intent' rules + // Make sure the name of the intent corresponds exactly with the one defined in your NLU service (case sensitive) + { + path: 'hi', + input: i => + i.intent == 'Default Welcome Intent' || + i.intent == 'smalltalk.greetings.bye', + action: Hi, + }, + { path: 'bye', intent: 'smalltalk.greetings.bye', action: Bye }, + { path: 'not_found', type: /.*/, action: NotFound }, + + /* There's an implicit rule that captures any other input and maps it to + the 404 action, it would be equivalent to: + {type: /^.*$/, action: "404"} + */ +] diff --git a/examples/intent/src/webchat/index.js b/examples/intent/src/webchat/index.js new file mode 100644 index 0000000000..80a09f176c --- /dev/null +++ b/examples/intent/src/webchat/index.js @@ -0,0 +1 @@ +export const webchat = {} diff --git a/examples/intent/src/webviews/index.js b/examples/intent/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/intent/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/intent/tests/__mocks__/fileMock.js b/examples/intent/tests/__mocks__/fileMock.js new file mode 100644 index 0000000000..0e56c5b5f7 --- /dev/null +++ b/examples/intent/tests/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub' diff --git a/examples/intent/tests/__mocks__/styleMock.js b/examples/intent/tests/__mocks__/styleMock.js new file mode 100644 index 0000000000..4ba52ba2c8 --- /dev/null +++ b/examples/intent/tests/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/examples/intent/tests/app.test.js b/examples/intent/tests/app.test.js new file mode 100644 index 0000000000..91ccbcbbdc --- /dev/null +++ b/examples/intent/tests/app.test.js @@ -0,0 +1,22 @@ +import { + BotonicInputTester, + BotonicOutputTester, + NodeApp, +} from '@botonic/react' + +import { config } from '../src/' +import { locales } from '../src/locales' +import { routes } from '../src/routes' + +const app = new NodeApp({ routes, locales, ...config }) + +const input = new BotonicInputTester(app) +const output = new BotonicOutputTester(app) + +test('TEST: (404) NOT FOUND', async () => { + const response = await input.text('whatever') + expect(response).toBe(output.text( + // replace with 'Try typing "hello" to start the bot.' after configuring dialogflow + `Enter the generated JSON key for dialogflowV2 in plugins.js to test the bot.` + )) +}) diff --git a/examples/intent/webpack-entries/dev-entry.js b/examples/intent/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..6e32a58053 --- /dev/null +++ b/examples/intent/webpack-entries/dev-entry.js @@ -0,0 +1,15 @@ +import { DevApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' +import { webchat } from '../src/webchat' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/intent/webpack-entries/node-entry.js b/examples/intent/webpack-entries/node-entry.js new file mode 100644 index 0000000000..fbb24edb77 --- /dev/null +++ b/examples/intent/webpack-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/intent/webpack-entries/webchat-entry.js b/examples/intent/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..4bd967e0c4 --- /dev/null +++ b/examples/intent/webpack-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react' + +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/intent/webpack-entries/webviews-entry.js b/examples/intent/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..030c2fc4f9 --- /dev/null +++ b/examples/intent/webpack-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/intent/webpack.config.js b/examples/intent/webpack.config.js new file mode 100644 index 0000000000..03250b1239 --- /dev/null +++ b/examples/intent/webpack.config.js @@ -0,0 +1,338 @@ +const path = require('path') +const webpack = require('webpack') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin') + +const ROOT = path.resolve(__dirname, 'src') +const ASSETS_DIRNAME = 'assets' + +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageMinimizerPlugin({ + minimizer: { + implementation: ImageMinimizerPlugin.imageminMinify, + options: { + plugins: [ + "imagemin-gifsicle", + "imagemin-jpegtran", + "imagemin-optipng", + "imagemin-svgo", + ], + }, + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/telco-offers/.gitignore b/examples/telco-offers/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/telco-offers/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/telco-offers/README.md b/examples/telco-offers/README.md new file mode 100644 index 0000000000..e221cee560 --- /dev/null +++ b/examples/telco-offers/README.md @@ -0,0 +1,199 @@ +# Botonic Telco + +This example shows you a multi-language conversation flow to acquire an Internet or a cell phone rate using buttons and replies. + +**What's in this document?** + +- [How to use this example](#how-to-use-this-example) +- [Routes and Actions](#routes-and-actions) +- [Locales](#locales) +- [Webchat Settings](#webchat-settings) + +## How to use this example + +1. From your command line, download the example by running: + ```bash + $ botonic new telco-offers + ``` +2. `cd` into `` directory that has been created. +3. Run `botonic serve` to test it in your local machine. + +## Routes and Actions + +[Routes](https://botonic.io/docs/concepts/routes) map user inputs to [actions](https://botonic.io/docs/concepts/actions) which consist of simple units of logic that your bot can perform and the response that your bot generates. + +Here we can see a few examples of how we have captured the user input. + +**src/routes.js** + +```javascript +import Start from './actions/start' +import ChooseLanguage from './actions/choose_language' +import Phone from './actions/phone' +import BuyPhone from './actions/buy-phone' +import Bye from './actions/bye' + +export const routes = [ + { path: 'hi', payload: 'hi', action: ChooseLanguage }, + { path: 'set-language', payload: /language-.*/, action: Start }, + { + path: 'phone', + payload: 'phone', + action: Phone, + childRoutes: [ + { + path: 'buyPhone', + payload: /buyPhone-.*/, + action: BuyPhone, + }, + ], + }, + { path: 'bye', text: /.*/, payload: /bye-.*/, action: Bye }, +] +``` + +If a rule matches it will trigger an action: + +**src/actions/choose-language.jsx** + +```javascript +import React from 'react' +import { Text, Reply } from '@botonic/react' + +export default class extends React.Component { + render() { + return ( + <> + Hi! Before we start choose a language: {'\n'} + + Hola! Antes de empezar elige un idioma: + Español + English + + + ) + } +} +``` + +## Locales + +The [Locales](https://botonic.io/docs/concepts/i18n/) allows us to build a bot that supports different languages. To do so, we have separated our string literals from the code components. +In the `src/locales` folder we have added a js file for each language we want to support. + +**src/locales/en.js** + +```javascript +export default { + internet: ['Internet'], + phone: ['Cell Phone'], + tv: ['TV'], + extra_phone: ['Extra Cell Phone'], + speed: ['Speed'], + price: ['Price'], + + start_text: [ + 'Welcome, I am your virtual assistant of Botonic Telco, select which service you want to hire?', + ], + ask_more: ['Do you want to hire any more rate?'], +} +``` + +**src/locales/es.js** + +```javascript +export default { + internet: ['Fibra'], + phone: ['Móvil'], + tv: ['TV'], + extra_phone: ['Extra móvil'], + speed: ['Velocidad'], + price: ['Precio'], + + start_text: [ + 'Bienvenido soy tu asistente virtual de Botonic Telco, selecciona que servicio quieres contratar?', + ], + ask_more: ['Quieres contratar alguna tarifa más?'], +} +``` + +Then, we have exported these languages. + +**src/locales/index.js** + +```javascript +import en from './en' +import es from './es' + +export const locales = { en, es } +``` + +In the initial action we have set the locale and then we can access an object from locales with `this.context.getString` method. + +**src/actions/start.jsx** + +```javascript +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const language = request.input.payload.split('-')[1] + return { language } + } + render() { + this.props.language && this.context.setLocale(this.props.language) + let _ = this.context.getString + return ( + <> + + {_('start_text')} + + + + + ) + } +} +``` + +## Webchat Settings + +The [Webchat Settings](https://botonic.io/docs/components/webchatsettings/) component can be appended at the end of a message to change Webchat properties dynamically. + +We have used it to enable the user input in one of the last actions. + +**src/actions/confirm.jsx** + +```javascript +import React from 'react' +import { RequestContext, Text, WebchatSettings } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + + render() { + let _ = this.context.getString + return ( + <> + {_('confirm.text')} + + + ) + } +} +``` + +...and we are done 🎉 diff --git a/examples/telco-offers/babel.config.js b/examples/telco-offers/babel.config.js new file mode 100644 index 0000000000..7325b9844d --- /dev/null +++ b/examples/telco-offers/babel.config.js @@ -0,0 +1,30 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-proposal-class-properties', + '@babel/plugin-transform-runtime', + ], +} diff --git a/examples/telco-offers/jest.config.js b/examples/telco-offers/jest.config.js new file mode 100644 index 0000000000..6b56f40225 --- /dev/null +++ b/examples/telco-offers/jest.config.js @@ -0,0 +1,17 @@ +const path = require('path') + +module.exports = { + rootDir: 'tests', + transform: { + '^.+\\.jsx?$': [ + 'babel-jest', + { configFile: path.resolve(__dirname, 'babel.config.js') }, + ], + }, + transformIgnorePatterns: ['/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$'], + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.js', + '\\.(scss|css|less)$': '/__mocks__/styleMock.js', + }, +} diff --git a/examples/telco-offers/package.json b/examples/telco-offers/package.json new file mode 100644 index 0000000000..d2ae39be1b --- /dev/null +++ b/examples/telco-offers/package.json @@ -0,0 +1,17 @@ +{ + "name": "@botonic/example-telco-offers", + "version": "0.25.0-beta.0", + "scripts": { + "build": "webpack --env target=all --mode=production", + "start": "webpack-dev-server --env target=dev --mode=development", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@botonic/react": "0.25.0-beta.0" + }, + "devDependencies": { + "@botonic/dx": "0.25.0-beta.0" + } +} diff --git a/examples/telco-offers/src/actions/buy-internet.jsx b/examples/telco-offers/src/actions/buy-internet.jsx new file mode 100644 index 0000000000..8d99330583 --- /dev/null +++ b/examples/telco-offers/src/actions/buy-internet.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const reservationInfo = request.input.payload.split('-') + const internet = { + data: reservationInfo[1], + price: reservationInfo[2], + } + request.session.user.extra_data.internet = internet + return { + minutes: internet.minutes, + data: internet.data, + price: internet.price, + } + } + + render() { + let _ = this.context.getString + let payloadYes = `buyOffer-${_('offer.tv')}-6.50` + return ( + <> + + {_('after_buy_internet')} {'\n'} + **{_('data')}**: {this.props.data} + {'\n'} + **{_('price')}**: {this.props.price}${'\n'} + + + {_('offer.text')} + + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/buy-offer.jsx b/examples/telco-offers/src/actions/buy-offer.jsx new file mode 100644 index 0000000000..15026f335f --- /dev/null +++ b/examples/telco-offers/src/actions/buy-offer.jsx @@ -0,0 +1,42 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const reservationInfo = request.input.payload.split('-') + const hasPhone = request.session.user.extra_data.phone.length > 0 + if (reservationInfo[1] !== 'no') { + const tv = { + name: reservationInfo[1], + price: reservationInfo[2], + } + request.session.user.extra_data.tv = tv + return { tv: tv, hasPhone } + } + return { hasPhone } + } + + render() { + let _ = this.context.getString + return ( + <> + {this.props.tv && ( + + {_('after_buy_offer')} {'\n'} + **TV**: {this.props.tv.name} + {'\n'} + **{_('price')}**: {this.props.tv.price}$ + + )} + + {_('ask_more')} + + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/buy-phone.jsx b/examples/telco-offers/src/actions/buy-phone.jsx new file mode 100644 index 0000000000..be940f3ccb --- /dev/null +++ b/examples/telco-offers/src/actions/buy-phone.jsx @@ -0,0 +1,42 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const reservationInfo = request.input.payload.split('-') + const phone = { + minutes: reservationInfo[1], + data: reservationInfo[2], + price: reservationInfo[3], + } + request.session.user.extra_data.phone.push(phone) + return { + minutes: phone.minutes, + data: phone.data, + price: phone.price, + } + } + + render() { + let _ = this.context.getString + return ( + <> + + {_('after_buy_phone')} {'\n'} + **{_('minutes')}**: {this.props.minutes} + {'\n'} + **{_('data')}**: {this.props.data} + {'\n'} + **{_('price')}**: {this.props.price}${'\n'} + + + {_('ask_more')} + + + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/bye.jsx b/examples/telco-offers/src/actions/bye.jsx new file mode 100644 index 0000000000..99c0a69a8b --- /dev/null +++ b/examples/telco-offers/src/actions/bye.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import { RequestContext, Text, Button, WebchatSettings } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const hasCancel = request.input.payload || false + const user = request.input.data + return { hasCancel, user } + } + + render() { + let _ = this.context.getString + return ( + <> + {this.props.hasCancel ? ( + + {_('bye.cancel')} + + ) : ( + + {_('bye.confirm1')} {this.props.user} {_('bye.confirm2')} + + + )} + + + ) + } +} diff --git a/examples/telco-offers/src/actions/choose-language.jsx b/examples/telco-offers/src/actions/choose-language.jsx new file mode 100644 index 0000000000..886bea4c9c --- /dev/null +++ b/examples/telco-offers/src/actions/choose-language.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Text, Reply } from '@botonic/react' + +export default class extends React.Component { + render() { + return ( + <> + Hi! Before we start choose a language: {'\n'} + + Hola! Antes de empezar elige un idioma: + Español + English + + + ) + } +} diff --git a/examples/telco-offers/src/actions/confirm.jsx b/examples/telco-offers/src/actions/confirm.jsx new file mode 100644 index 0000000000..63bda34c9d --- /dev/null +++ b/examples/telco-offers/src/actions/confirm.jsx @@ -0,0 +1,27 @@ +import React from 'react' +import { RequestContext, Text, WebchatSettings } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + + render() { + let _ = this.context.getString + return ( + <> + {_('confirm.text')} + + + ) + } +} diff --git a/examples/telco-offers/src/actions/internet.jsx b/examples/telco-offers/src/actions/internet.jsx new file mode 100644 index 0000000000..03a8030014 --- /dev/null +++ b/examples/telco-offers/src/actions/internet.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + render() { + let _ = this.context.getString + + const renderTable = (data, price) => { + return ( + `| ${_('speed')} | ${_('price')} |\n` + + `| :-: | :-: |\n` + + `| ${data} | ${price} |\n` + ) + } + return ( + <> + {_('contract_internet')} + + {renderTable('100Mb', '29.50$')} + + + + {renderTable('600Mb', '36.50$')} + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/phone.jsx b/examples/telco-offers/src/actions/phone.jsx new file mode 100644 index 0000000000..2056d2da88 --- /dev/null +++ b/examples/telco-offers/src/actions/phone.jsx @@ -0,0 +1,37 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + render() { + let _ = this.context.getString + + const renderTable = (minutes, data, price) => { + return ( + `| ${_('minutes')} | ${_('data')} | ${_('price')} |\n` + + `| :-: | :-: | :-: |\n` + + `| ${minutes} | ${data} | ${price} |\n` + ) + } + const payloadUnlimited = `buyPhone-${_('unlimited')}-${_( + 'unlimited' + )}-23.50` + return ( + <> + {_('contract_phone')} + + {renderTable(`${_('unlimited')}`, `${_('unlimited')}`, '23.50$')} + + + + {renderTable('200', '20GB', '15.50$')} + + + + {renderTable('50', '5GB', '7.50$')} + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/start.jsx b/examples/telco-offers/src/actions/start.jsx new file mode 100644 index 0000000000..0196425ba0 --- /dev/null +++ b/examples/telco-offers/src/actions/start.jsx @@ -0,0 +1,26 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const language = request.input.payload.split('-')[1] + const extra_data = { phone: [] } + request.session.user.extra_data = extra_data + return { language } + } + + render() { + this.props.language && this.context.setLocale(this.props.language) + let _ = this.context.getString + return ( + <> + + {_('start_text')} + + + + + ) + } +} diff --git a/examples/telco-offers/src/actions/summary.jsx b/examples/telco-offers/src/actions/summary.jsx new file mode 100644 index 0000000000..54304339ba --- /dev/null +++ b/examples/telco-offers/src/actions/summary.jsx @@ -0,0 +1,68 @@ +import React from 'react' +import { RequestContext, Text, Button } from '@botonic/react' + +export default class extends React.Component { + static contextType = RequestContext + static async botonicInit(request) { + const tv = request.session.user.extra_data.tv + const internet = request.session.user.extra_data.internet + const phone = request.session.user.extra_data.phone + const priceTV = (tv && parseFloat(tv.price)) || 0.0 + const priceInternet = (internet && parseFloat(internet.price)) || 0.0 + let pricePhone = 0.0 + phone.forEach(m => { + pricePhone = pricePhone + parseFloat(m.price) + }) + const price = priceTV + priceInternet + pricePhone + return { tv, internet, phone, price } + } + + render() { + let _ = this.context.getString + + const getInternet = () => { + if (this.props.internet) + return `**${_('internet')}**: + ${_('data')}: ${this.props.internet.data} + ${_('price')}: ${this.props.internet.price}$ + ` + return null + } + const getPhone = () => { + if (this.props.phone) + return this.props.phone.map( + (m, i) => + `**${_('phone')} ${i + 1}**: + ${_('minutes')}: ${m.minutes} + ${_('data')}: ${m.data} + ${_('price')}: ${m.price}$ + ` + ) + return null + } + const getTV = () => { + if (this.props.tv) + return `\n**${_('tv')}**: + ${_('data')}: ${this.props.tv.name} + ${_('price')}: ${this.props.tv.price}$ + ` + return null + } + return ( + <> + + {_('summary')} {'\n'} + {getInternet()} + {getPhone()} + {getTV()} + **Total: {this.props.price}$** + + + {_('continue')} + + + + + ) + } +} diff --git a/examples/telco-offers/src/assets/.gitkeep b/examples/telco-offers/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/telco-offers/src/assets/phone.svg b/examples/telco-offers/src/assets/phone.svg new file mode 100644 index 0000000000..097cbd2cf0 --- /dev/null +++ b/examples/telco-offers/src/assets/phone.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/telco-offers/src/assets/white_phone.svg b/examples/telco-offers/src/assets/white_phone.svg new file mode 100644 index 0000000000..e7da7e8d48 --- /dev/null +++ b/examples/telco-offers/src/assets/white_phone.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/telco-offers/src/index.js b/examples/telco-offers/src/index.js new file mode 100644 index 0000000000..f7334846a7 --- /dev/null +++ b/examples/telco-offers/src/index.js @@ -0,0 +1 @@ +export const config = { defaultDelay: 0, defaultTyping: 0 } diff --git a/examples/telco-offers/src/locales/en.js b/examples/telco-offers/src/locales/en.js new file mode 100644 index 0000000000..53e552b542 --- /dev/null +++ b/examples/telco-offers/src/locales/en.js @@ -0,0 +1,52 @@ +export default { + internet: ['Internet'], + phone: ['Cell Phone'], + tv: ['TV'], + extra_phone: ['Extra Cell Phone'], + done: ['Done'], + data: ['Data'], + speed: ['Speed'], + price: ['Price'], + minutes: ['Minutes'], + choose: ['Choose Plan'], + unlimited: ['unlimited'], + + start_text: [ + 'Welcome, I am your virtual assistant of Botonic Telco, select which service you want to contract.', + ], + start_again: ['Start Again'], + ask_more: ['Do you want to contract any more rate?'], + + contract_internet: ['Select one of our Internet rates:'], + contract_phone: ['Select one of our cell phone rates:'], + + after_buy_internet: ['You have selected the Internet rate of:'], + after_buy_phone: ['You have selected the cell phone rate of:'], + after_buy_offer: ['The offer has been added:'], + + offer: { + text: [ + '**OFFER**: Get a package of series, movies and sports for only 6.50$/month for the next three months.', + ], + tv: ['Sports + Series + Movies'], + yes: ['Add'], + no: ['Refuse'], + }, + + summary: ['Here you can see the list of the products you have selected:'], + continue: ['To continue with the purchase click confirm.'], + + confirm: { + text: ['Please enter your user number'], + yes: ['Confirm'], + no: ['Cancel'], + }, + + bye: { + confirm1: ['User'], + confirm2: [ + 'confirmed! Thank you for your purchase, I hope you have a nice day.', + ], + cancel: ['The purchase has been canceled.'], + }, +} diff --git a/examples/telco-offers/src/locales/es.js b/examples/telco-offers/src/locales/es.js new file mode 100644 index 0000000000..d62a4e8037 --- /dev/null +++ b/examples/telco-offers/src/locales/es.js @@ -0,0 +1,52 @@ +export default { + internet: ['Fibra'], + phone: ['Móvil'], + tv: ['TV'], + extra_phone: ['Extra móvil'], + done: ['Terminar'], + data: ['Datos'], + speed: ['Velocidad'], + price: ['Precio'], + minutes: ['Minutos'], + choose: ['Contratar'], + unlimited: ['ilimitado'], + + start_text: [ + 'Bienvenido soy tu asistente virtual de Botonic Telco, selecciona qué servicio quieres contratar.', + ], + start_again: ['Volver a empezar'], + ask_more: ['Quieres contratar alguna tarifa más?'], + + contract_internet: ['Selecciona una de nuestras tarifas de Fibra:'], + contract_phone: ['Selecciona una de nuestras tarifas de móvil:'], + + after_buy_internet: ['Has seleccionado la tarifa de fibra de:'], + after_buy_phone: ['Has seleccionado la tarifa de móvil de:'], + after_buy_offer: ['Has añadido la oferta:'], + + offer: { + text: [ + '**OFERTA**: Llévate un paquete de series, películas y deportes por solo 6.50$/mes durante los próximos tres meses.', + ], + tv: ['Deportes + Series + Pelis'], + yes: ['Añadir'], + no: ['Rechazar'], + }, + + summary: ['Aquí puedes ver la lista de los productos que has seleccionado:'], + continue: ['Para continuar con la compra haga clic en confirmar.'], + + confirm: { + text: ['Por favor, introduce su número de usuario:'], + yes: ['Confirmar'], + no: ['Cancelar'], + }, + + bye: { + confirm1: ['Usuario'], + confirm2: [ + 'confirmado! Muchas gracias por su compra, espero que tenga un buen día.', + ], + cancel: ['Has cancelado la compra.'], + }, +} diff --git a/examples/telco-offers/src/locales/index.js b/examples/telco-offers/src/locales/index.js new file mode 100644 index 0000000000..195b99128f --- /dev/null +++ b/examples/telco-offers/src/locales/index.js @@ -0,0 +1,4 @@ +import en from './en' +import es from './es' + +export const locales = { en, es } diff --git a/examples/telco-offers/src/plugins.js b/examples/telco-offers/src/plugins.js new file mode 100644 index 0000000000..e571fed32b --- /dev/null +++ b/examples/telco-offers/src/plugins.js @@ -0,0 +1 @@ +export const plugins = [] diff --git a/examples/telco-offers/src/routes.js b/examples/telco-offers/src/routes.js new file mode 100644 index 0000000000..1eb871db68 --- /dev/null +++ b/examples/telco-offers/src/routes.js @@ -0,0 +1,49 @@ +import Start from './actions/start' +import ChooseLanguage from './actions/choose-language' +import Summary from './actions/summary' +import Bye from './actions/bye' +import Phone from './actions/phone' +import Internet from './actions/internet' +import BuyPhone from './actions/buy-phone' +import BuyInternet from './actions/buy-internet' +import BuyOffer from './actions/buy-offer' +import Confirm from './actions/confirm' + +export const routes = [ + { path: 'hi', payload: 'hi', action: ChooseLanguage }, + { path: 'set-language', payload: /language-.*/, action: Start }, + { + path: 'phone', + payload: 'phone', + action: Phone, + childRoutes: [ + { + path: 'buyPhone', + payload: /buyPhone-.*/, + action: BuyPhone, + }, + ], + }, + { + path: 'internet', + payload: 'internet', + action: Internet, + childRoutes: [ + { + path: 'buyInternet', + payload: /buyInternet-.*/, + action: BuyInternet, + childRoutes: [ + { path: 'buyOffer', payload: /buyOffer-.*/, action: BuyOffer }, + ], + }, + ], + }, + { + path: 'summary', + payload: 'summary', + action: Summary, + childRoutes: [{ path: 'confirm', payload: 'confirm', action: Confirm }], + }, + { path: 'bye', text: /.*/, payload: /bye-.*/, action: Bye }, +] diff --git a/examples/telco-offers/src/webchat/constants.js b/examples/telco-offers/src/webchat/constants.js new file mode 100644 index 0000000000..0190ce41a5 --- /dev/null +++ b/examples/telco-offers/src/webchat/constants.js @@ -0,0 +1,4 @@ +export const COLORS = { + PRIMARY_COLOR: '#FF5500', + MAIN_COLOR: 'rgba(186, 76, 0, 0.15)', +} diff --git a/examples/telco-offers/src/webchat/custom-trigger.js b/examples/telco-offers/src/webchat/custom-trigger.js new file mode 100644 index 0000000000..ccd2206e43 --- /dev/null +++ b/examples/telco-offers/src/webchat/custom-trigger.js @@ -0,0 +1,50 @@ +import React, { useState } from 'react' +import styled, { keyframes } from 'styled-components' +import Icon from '../assets/phone.svg' +import { staticAsset } from '@botonic/react' + +const shake = keyframes` + 10%, + 90% { + transform: translate3d(-1px, 0, 0); + } + 20%, + 80% { + transform: translate3d(2px, 0, 0); + } + 30%, + 50%, + 70% { + transform: translate3d(-4px, 0, 0); + } + 40%, + 60% { + transform: translate3d(4px, 0, 0); + } +` +const Container = styled.div` + animation: ${props => !props.hover && shake} 0.82s + cubic-bezier(0.36, 0.07, 0.19, 0.97) both infinite; + bottom: 50px; + right: 50px; + position: fixed; + cursor: pointer; +` + +export const CustomTrigger = () => { + let [hover, setHover] = useState(false) + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + hover={hover} + > + + + ) +} diff --git a/examples/telco-offers/src/webchat/index.js b/examples/telco-offers/src/webchat/index.js new file mode 100644 index 0000000000..edfd5ab179 --- /dev/null +++ b/examples/telco-offers/src/webchat/index.js @@ -0,0 +1,135 @@ +import BotIconWhite from '../assets/white_phone.svg' +import BotIcon from '../assets/phone.svg' +import { CustomTrigger } from './custom-trigger' +import { COLORS } from './constants' + +export const webchat = { + storage: sessionStorage, + storageKey: 'botonic-telco-example', + shadowDOM: true, + + onOpen: app => { + app.clearMessages() + app.addUserPayload('hi') + }, + + theme: { + customTrigger: CustomTrigger, + style: { + fontFamily: '"Helvetica Neue",Arial,sans-serif', + width: 370, + borderRadius: 10, + background: '#F5F5F5', + lineHeight: 1.3, + }, + header: { + image: BotIconWhite, + title: 'Botonic Telco Offers', + style: { + background: COLORS.PRIMARY_COLOR, + }, + }, + brand: { + color: COLORS.PRIMARY_COLOR, + image: BotIcon, + }, + triggerButton: { + image: BotIcon, + }, + + button: { + style: { + color: '#000000', + background: '#ffffff', + borderRadius: 10, + border: '1px solid #000000', + margin: '8px 25px', + padding: '10px', + width: '200px', + }, + hoverBackground: COLORS.MAIN_COLOR, + hoverTextColor: 'black', + }, + message: { + bot: { + blobTick: false, + blobWidth: '255px', + imageStyle: { + alignItems: 'flex-end', + }, + style: { + border: 'none', + color: 'black', + borderRadius: '7px', + background: 'white', + boxShadow: '1px -1px 6px rgba(0, 0, 0, 0.3)', + }, + }, + user: { + blobTick: false, + style: { + background: COLORS.PRIMARY_COLOR, + borderRadius: '12px 12px 0px 12px', + }, + }, + }, + enableUserInput: false, + markdownStyle: ` + p { + margin: 0px; + } + table { + margin-top: 10px; + border-collapse: collapse; + overflow: hidden; + box-shadow: 0 0 20px rgba(0,0,0,0.1); + border-radius:5px; + } + + th, + td { + border-radius:5px; + padding: 7px; + background-color: rgba(255,255,255,0.2); + color: black; + text-align: center; + } + + thead { + border-radius:5px; + border: 2px solid ${COLORS.PRIMARY_COLOR}; + th { + border-radius:0px; + border: 1px solid ${COLORS.PRIMARY_COLOR}; + background-color: ${COLORS.MAIN_COLOR}; + } + } + + tbody { + border-radius:5px; + border: 2px solid ${COLORS.PRIMARY_COLOR}; + tr { + &:hover { + background-color: rgba(255,255,255,0.3); + } + border: 1px solid ${COLORS.PRIMARY_COLOR}; + } + td { + border: 1px solid ${COLORS.PRIMARY_COLOR}; + position: relative; + &:hover { + &:before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: -9999px; + bottom: -9999px; + background-color: rgba(255,255,255,0.2); + z-index: -1; + } + } + } + `, + }, +} diff --git a/examples/telco-offers/src/webviews/index.js b/examples/telco-offers/src/webviews/index.js new file mode 100644 index 0000000000..4ca4089ae0 --- /dev/null +++ b/examples/telco-offers/src/webviews/index.js @@ -0,0 +1 @@ +export const webviews = [] diff --git a/examples/telco-offers/tests/.gitkeep b/examples/telco-offers/tests/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/telco-offers/webpack-entries/dev-entry.js b/examples/telco-offers/webpack-entries/dev-entry.js new file mode 100644 index 0000000000..00aece8237 --- /dev/null +++ b/examples/telco-offers/webpack-entries/dev-entry.js @@ -0,0 +1,14 @@ +import { DevApp } from '@botonic/react' +import { routes } from '../src/routes' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { webchat } from '../src/webchat' +import { config } from '../src' + +export const app = new DevApp({ + routes, + locales, + plugins, + ...webchat, + ...config, +}) diff --git a/examples/telco-offers/webpack-entries/node-entry.js b/examples/telco-offers/webpack-entries/node-entry.js new file mode 100644 index 0000000000..cc3df2f809 --- /dev/null +++ b/examples/telco-offers/webpack-entries/node-entry.js @@ -0,0 +1,7 @@ +import { NodeApp } from '@botonic/react' +import { routes } from '../src/routes' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { config } from '../src' + +export const app = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/telco-offers/webpack-entries/webchat-entry.js b/examples/telco-offers/webpack-entries/webchat-entry.js new file mode 100644 index 0000000000..03c0e4485f --- /dev/null +++ b/examples/telco-offers/webpack-entries/webchat-entry.js @@ -0,0 +1,4 @@ +import { WebchatApp } from '@botonic/react' +import { webchat } from '../src/webchat' + +export const app = new WebchatApp(webchat) diff --git a/examples/telco-offers/webpack-entries/webviews-entry.js b/examples/telco-offers/webpack-entries/webviews-entry.js new file mode 100644 index 0000000000..f9052ba84b --- /dev/null +++ b/examples/telco-offers/webpack-entries/webviews-entry.js @@ -0,0 +1,5 @@ +import { WebviewApp } from '@botonic/react' +import { webviews } from '../src/webviews' +import { locales } from '../src/locales' + +export const app = new WebviewApp({ webviews, locales }) diff --git a/examples/telco-offers/webpack.config.js b/examples/telco-offers/webpack.config.js new file mode 100644 index 0000000000..4b545faed8 --- /dev/null +++ b/examples/telco-offers/webpack.config.js @@ -0,0 +1,366 @@ +const path = require('path') +const webpack = require('webpack') +const CopyPlugin = require('copy-webpack-plugin') +const TerserPlugin = require('terser-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { CleanWebpackPlugin } = require('clean-webpack-plugin') +const ImageminPlugin = require('imagemin-webpack') + +const ROOT = path.resolve(__dirname, 'src') +const NLP_DIRNAME = 'nlp' +const ASSETS_DIRNAME = 'assets' +const MODELS_DIRNAME = 'models' +const TASKS_DIRNAME = 'tasks' + +const INTENT_CLASSIFICATION_DIRNAME = 'intent-classification' +const OUTPUT_PATH = path.resolve(__dirname, 'dist') +const WEBVIEWS_PATH = path.resolve(OUTPUT_PATH, 'webviews') +const TASKS_PATH = path.join(ROOT, NLP_DIRNAME, TASKS_DIRNAME) + +const INTENT_CLASSIFICATION_MODELS_PATH = path.join( + NLP_DIRNAME, + TASKS_DIRNAME, + INTENT_CLASSIFICATION_DIRNAME, + MODELS_DIRNAME +) +const INTENTS_ASSETS_MODELS_PATH = path.join( + ASSETS_DIRNAME, + TASKS_DIRNAME, + INTENT_CLASSIFICATION_DIRNAME, + MODELS_DIRNAME +) + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react' +) + +const WEBPACK_MODE = { + DEVELOPMENT: 'development', + PRODUCTION: 'production', +} + +const BOTONIC_TARGETS = { + ALL: 'all', + DEV: 'dev', + NODE: 'node', + WEBVIEWS: 'webviews', + WEBCHAT: 'webchat', +} + +const WEBPACK_ENTRIES_DIRNAME = 'webpack-entries' +const WEBPACK_ENTRIES = { + DEV: 'dev-entry.js', + NODE: 'node-entry.js', + WEBCHAT: 'webchat-entry.js', + WEBVIEWS: 'webviews-entry.js', +} + +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} + +const UMD_LIBRARY_TARGET = 'umd' +const BOTONIC_LIBRARY_NAME = 'Botonic' +const WEBCHAT_FILENAME = 'webchat.botonic.js' + +function sourceMap(mode) { + if (mode === WEBPACK_MODE.PRODUCTION) return 'hidden-source-map' + else if (mode === WEBPACK_MODE.DEVELOPMENT) return 'eval-cheap-source-map' + else + throw new Error( + 'Invalid mode argument (' + mode + '). See package.json scripts' + ) +} + +const optimizationConfig = { + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: true, + terserOptions: { + keep_fnames: true, + }, + }), + ], +} + +const resolveConfig = { + extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.mjs'], + alias: { + react: path.resolve(__dirname, 'node_modules', 'react'), + 'styled-components': path.resolve( + __dirname, + 'node_modules', + 'styled-components' + ), + }, + fallback: { + util: require.resolve('util'), + }, +} + +const babelLoaderConfig = { + test: /\.(js|jsx|ts|tsx|mjs)$/, + exclude: /node_modules\/(?!@botonic)/, + use: { + loader: 'babel-loader', + options: { + sourceType: 'unambiguous', + cacheDirectory: true, + presets: [ + '@babel/react', + [ + '@babel/preset-env', + { + modules: false, + }, + ], + ], + plugins: [ + '@babel/plugin-proposal-object-rest-spread', + '@babel/plugin-proposal-class-properties', + '@babel/plugin-transform-runtime', + ], + }, + }, +} + +function fileLoaderConfig(outputPath) { + return { + test: /\.(jpe?g|png|gif|svg)$/i, + use: [ + { + loader: 'file-loader', + options: { + outputPath: outputPath, + }, + }, + ], + } +} + +const nullLoaderConfig = { + test: /\.(scss|css)$/, + use: 'null-loader', +} + +const stylesLoaderConfig = { + test: /\.(scss|css)$/, + use: [ + { + loader: 'style-loader', + options: { + insert: function (element) { + if (!window._botonicInsertStyles) window._botonicInsertStyles = [] + window._botonicInsertStyles.push(element) + }, + }, + }, + 'css-loader', + 'sass-loader', + ], +} + +const imageminPlugin = new ImageminPlugin({ + bail: false, + cache: false, + imageminOptions: { + plugins: [ + ['imagemin-gifsicle', { interlaced: true }], + ['imagemin-jpegtran', { progressive: true }], + ['imagemin-optipng', { optimizationLevel: 5 }], + ['imagemin-svgo', { removeViewBox: true }], + ], + }, +}) + +function botonicDevConfig(mode) { + return { + mode: mode, + devtool: sourceMap(mode), + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.DEV), + target: 'web', + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + devServer: { + static: [OUTPUT_PATH, TASKS_PATH], + liveReload: true, + historyApiFallback: true, + hot: true, + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + new webpack.HotModuleReplacementPlugin(), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + ...(mode === 'development' + ? { MODELS_BASE_URL: JSON.stringify('http://localhost:8080') } + : {}), + }), + new webpack.ProvidePlugin({ + process: 'process/browser', + }), + ], + } +} + +function botonicWebchatConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBCHAT), + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + stylesLoaderConfig, + ], + }, + output: { + filename: WEBCHAT_FILENAME, + library: BOTONIC_LIBRARY_NAME, + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBCHAT), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + WEBCHAT_PUSHER_KEY: JSON.stringify(process.env.WEBCHAT_PUSHER_KEY), + }), + ], + } +} + +function botonicWebviewsConfig(mode) { + return { + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'web', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.WEBVIEWS), + output: { + filename: 'webviews.js', + library: 'BotonicWebview', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: WEBVIEWS_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(path.join('..', ASSETS_DIRNAME)), + stylesLoaderConfig, + ], + }, + resolve: resolveConfig, + plugins: [ + new HtmlWebpackPlugin({ + template: path.resolve(BOTONIC_PATH, 'src', TEMPLATES.WEBVIEWS), + filename: 'index.html', + }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: true, + IS_NODE: false, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + ], + } +} + +function botonicNodeConfig(mode) { + return { + context: ROOT, + optimization: optimizationConfig, + mode: mode, + devtool: sourceMap(mode), + target: 'node', + entry: path.resolve(WEBPACK_ENTRIES_DIRNAME, WEBPACK_ENTRIES.NODE), + resolve: resolveConfig, + output: { + filename: 'bot.js', + library: 'bot', + libraryTarget: UMD_LIBRARY_TARGET, + libraryExport: 'app', + path: OUTPUT_PATH, + }, + module: { + rules: [ + babelLoaderConfig, + fileLoaderConfig(ASSETS_DIRNAME), + nullLoaderConfig, + ], + }, + plugins: [ + new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['dist'] }), + imageminPlugin, + new webpack.DefinePlugin({ + IS_BROWSER: false, + IS_NODE: true, + HUBTYPE_API_URL: JSON.stringify(process.env.HUBTYPE_API_URL), + }), + new CopyPlugin({ + patterns: [ + { + from: INTENT_CLASSIFICATION_MODELS_PATH, + to: INTENTS_ASSETS_MODELS_PATH, + }, + ], + }), + ], + } +} + +module.exports = function (env, argv) { + if (env.target === BOTONIC_TARGETS.ALL) { + return [ + botonicNodeConfig(argv.mode), + botonicWebviewsConfig(argv.mode), + botonicWebchatConfig(argv.mode), + ] + } else if (env.target === BOTONIC_TARGETS.DEV) { + return [botonicDevConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.NODE) { + return [botonicNodeConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBVIEWS) { + return [botonicWebviewsConfig(argv.mode)] + } else if (env.target === BOTONIC_TARGETS.WEBCHAT) { + return [botonicWebchatConfig(argv.mode)] + } else { + return null + } +} diff --git a/examples/tutorial/.gitignore b/examples/tutorial/.gitignore new file mode 100644 index 0000000000..6e4c9c0cca --- /dev/null +++ b/examples/tutorial/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +package-lock.json +.DS_Store \ No newline at end of file diff --git a/examples/tutorial/.prettierrc b/examples/tutorial/.prettierrc new file mode 100644 index 0000000000..05c968c026 --- /dev/null +++ b/examples/tutorial/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "trailingComma": "all", + "singleQuote": true +} diff --git a/examples/tutorial/babel.config.js b/examples/tutorial/babel.config.js new file mode 100644 index 0000000000..ce0c91d3ff --- /dev/null +++ b/examples/tutorial/babel.config.js @@ -0,0 +1,29 @@ +/* + * This babel configuration is used along with Jest for execute tests, + * do not modify to avoid conflicts with webpack.config.js. + */ + +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + [ + '@babel/react', + { + targets: { + node: 'current', + }, + }, + ], + ], + plugins: [ + require('@babel/plugin-transform-modules-commonjs'), + require('@babel/plugin-transform-runtime'), + ], +} diff --git a/examples/tutorial/esbuild-config.ts b/examples/tutorial/esbuild-config.ts new file mode 100644 index 0000000000..7d27dc8186 --- /dev/null +++ b/examples/tutorial/esbuild-config.ts @@ -0,0 +1,156 @@ +import fs, { readFileSync, writeFileSync } from 'fs' +import path, { join } from 'path' +import esbuild from 'esbuild' +import { sassPlugin } from 'esbuild-sass-plugin' +import inlineImage from 'esbuild-plugin-inline-image' +import imageminPlugin from 'esbuild-plugin-imagemin' +import { htmlPlugin } from '@craftamap/esbuild-plugin-html' + +process.env.NODE_ENV = 'production' + +const distPath = join(__dirname, 'dist') +if (!fs.existsSync(distPath)) { + fs.mkdirSync(distPath, { recursive: true }) +} else fs.rmSync(distPath, { recursive: true, force: true }) + +const nodeEntryPoint = './esbuild-entries/node-entry.js' +const nodeOutputFile = './dist/bot.js' + +const nodeBundle: esbuild.BuildOptions = { + entryPoints: [nodeEntryPoint], + platform: 'node', + outfile: nodeOutputFile, + bundle: true, + minify: true, + sourcemap: true, + keepNames: true, + metafile: true, + format: 'cjs', + external: ['esbuild'], + loader: { + '.js': 'jsx', + }, + assetNames: 'assets/[name]-[hash]', + plugins: [inlineImage(), sassPlugin()], + target: 'ES2018', + treeShaking: true, +} + +const webchatEntryPoint = './esbuild-entries/webchat-entry.js' +const webchatOutputFile = './dist/webchat.botonic.js' + +const webchatBundle: esbuild.BuildOptions = { + entryPoints: [webchatEntryPoint], + platform: 'browser', + outfile: webchatOutputFile, + bundle: true, + minify: true, + sourcemap: false, + keepNames: true, + format: 'iife', + metafile: true, + globalName: 'Botonic', + external: ['esbuild'], + loader: { + '.js': 'jsx', + '.ts': 'tsx', + }, + define: { global: 'window' }, + assetNames: 'assets/[name]-[hash]', + plugins: [imageminPlugin(), inlineImage(), sassPlugin({ type: 'style' })], + treeShaking: true, +} + +const webviewsEntryPoint = './esbuild-entries/webviews-entry.js' + +const BOTONIC_PATH = path.resolve( + __dirname, + 'node_modules', + '@botonic', + 'react', +) +const TEMPLATES = { + WEBCHAT: 'webchat.template.html', + WEBVIEWS: 'webview.template.html', +} +const WEBVIEW_TEMPLATE_PATH = path.resolve( + BOTONIC_PATH, + 'src', + TEMPLATES.WEBVIEWS, +) + +const webviewsBundle: esbuild.BuildOptions = { + entryPoints: [{ in: webviewsEntryPoint, out: 'webviews' }], + platform: 'browser', + outdir: './dist/webviews', + bundle: true, + minify: true, + keepNames: true, + sourcemap: true, + format: 'iife', + globalName: 'BotonicWebview', + external: ['esbuild'], + loader: { + '.js': 'jsx', + '.ts': 'tsx', + }, + treeShaking: true, + metafile: true, + define: { global: 'window' }, + assetNames: '../assets/[name]-[hash]', + plugins: [ + imageminPlugin(), + inlineImage(), + sassPlugin({ type: 'style' }), + htmlPlugin({ + files: [ + { + entryPoints: [webchatEntryPoint], + filename: 'index.html', + scriptLoading: 'defer', + extraScripts: [ + { + src: 'webviews.js', + attrs: { + defer: '', + }, + }, + ], + htmlTemplate: readFileSync(WEBVIEW_TEMPLATE_PATH, { + encoding: 'utf-8', + }), + }, + ], + }), + ], +} + +async function botonicBundle() { + const nodeBundleResult = await esbuild + .build(nodeBundle) + .catch(() => process.exit(1)) + + fs.writeFileSync( + 'meta-node-bundle.json', + JSON.stringify(nodeBundleResult.metafile), + ) + + const webchatBundleResult = await esbuild + .build(webchatBundle) + .catch(() => process.exit(1)) + + fs.writeFileSync( + 'meta-webchat-bundle.json', + JSON.stringify(webchatBundleResult.metafile), + ) + const webviewsBundleResult = await esbuild + .build(webviewsBundle) + .catch(() => process.exit(1)) + + fs.writeFileSync( + 'meta-webviews-bundle.json', + JSON.stringify(webviewsBundleResult.metafile), + ) +} + +botonicBundle() diff --git a/examples/tutorial/esbuild-entries/node-entry.js b/examples/tutorial/esbuild-entries/node-entry.js new file mode 100644 index 0000000000..fc054ca0b2 --- /dev/null +++ b/examples/tutorial/esbuild-entries/node-entry.js @@ -0,0 +1,8 @@ +import { NodeApp } from '@botonic/react/lib/esm' + +import { config } from '../src' +import { locales } from '../src/locales' +import { plugins } from '../src/plugins' +import { routes } from '../src/routes' + +module.exports = new NodeApp({ routes, locales, plugins, ...config }) diff --git a/examples/tutorial/esbuild-entries/webchat-entry.js b/examples/tutorial/esbuild-entries/webchat-entry.js new file mode 100644 index 0000000000..2d402547f2 --- /dev/null +++ b/examples/tutorial/esbuild-entries/webchat-entry.js @@ -0,0 +1,5 @@ +import { WebchatApp } from '@botonic/react/lib/esm' + +import { webchat } from '../src/webchat' + +module.exports = new WebchatApp(webchat) diff --git a/examples/tutorial/esbuild-entries/webviews-entry.js b/examples/tutorial/esbuild-entries/webviews-entry.js new file mode 100644 index 0000000000..428585f83c --- /dev/null +++ b/examples/tutorial/esbuild-entries/webviews-entry.js @@ -0,0 +1,6 @@ +import { WebviewApp } from '@botonic/react' + +import { locales } from '../src/locales' +import { webviews } from '../src/webviews' + +module.exports = new WebviewApp({ webviews, locales }) diff --git a/examples/tutorial/jest.config.js b/examples/tutorial/jest.config.js new file mode 100644 index 0000000000..6b56f40225 --- /dev/null +++ b/examples/tutorial/jest.config.js @@ -0,0 +1,17 @@ +const path = require('path') + +module.exports = { + rootDir: 'tests', + transform: { + '^.+\\.jsx?$': [ + 'babel-jest', + { configFile: path.resolve(__dirname, 'babel.config.js') }, + ], + }, + transformIgnorePatterns: ['/node_modules/(?!@botonic).+\\.(js|jsx|ts|tsx)$'], + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.js', + '\\.(scss|css|less)$': '/__mocks__/styleMock.js', + }, +} diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json new file mode 100644 index 0000000000..8d6f3f6179 --- /dev/null +++ b/examples/tutorial/package.json @@ -0,0 +1,30 @@ +{ + "name": "@botonic/example-tutorial", + "version": "0.25.0-beta.0", + "scripts": { + "analyze": "esbuild-visualizer --metadata ./meta.json", + "build:esbuild": "rm -rf ./dist; ts-node ./esbuild-config.ts", + "build": "rm -rf ./dist; webpack --env target=all --mode=production", + "start": "rm -rf ./dist; webpack-dev-server --env target=dev --mode=development", + "start:esbuild:in-progress": "rm -rf ./dist; esbuild ./esbuild-entries/webchat-entry.js --bundle --outdir=dist --serve", + "deploy": "botonic deploy -c build", + "test": "jest" + }, + "dependencies": { + "@babel/runtime": "^7.23.9", + "@botonic/react": "0.25.0-beta.0", + "core-js": "^3.36.0" + }, + "devDependencies": { + "esbuild-plugin-imagemin": "^1.0.1", + "esbuild-plugin-inline-image": "0.0.9", + "esbuild-sass-plugin": "^2.16.1", + "esbuild-visualizer": "^0.4.1", + "esbuild": "^0.19.4", + "@craftamap/esbuild-plugin-html": "^0.5.0", + "@botonic/dx": "0.25.0-beta.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/examples/tutorial/src/actions/404.jsx b/examples/tutorial/src/actions/404.jsx new file mode 100644 index 0000000000..006a1d43c1 --- /dev/null +++ b/examples/tutorial/src/actions/404.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return Please, type "start" to start the tutorial. + } +} diff --git a/examples/tutorial/src/actions/age.jsx b/examples/tutorial/src/actions/age.jsx new file mode 100644 index 0000000000..661e0512dd --- /dev/null +++ b/examples/tutorial/src/actions/age.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return I know your age, and it's {this.context.params.age} + } +} diff --git a/examples/tutorial/src/actions/buttons.jsx b/examples/tutorial/src/actions/buttons.jsx new file mode 100644 index 0000000000..b0de2abfb0 --- /dev/null +++ b/examples/tutorial/src/actions/buttons.jsx @@ -0,0 +1,27 @@ +import { Button, Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + <> + + Here I display two types of buttons, the first one is a URL button and + the second is a payload button: + + + + + Clicking on a button with url will just open that URL in the browser. + Clicking on a button with payload will send an input of type + "postback" with that payload. You can find more information about how + this buttons look in Facebook Messenger here: + https://developers.facebook.com/docs/messenger-platform/send-messages/buttons#postback + + + Now, you can type 'webviews' and see how enjoyable they are. + + + ) + } +} diff --git a/examples/tutorial/src/actions/bye.jsx b/examples/tutorial/src/actions/bye.jsx new file mode 100644 index 0000000000..84a4863093 --- /dev/null +++ b/examples/tutorial/src/actions/bye.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return Bye bye! 👋 + } +} diff --git a/examples/tutorial/src/actions/carousel.jsx b/examples/tutorial/src/actions/carousel.jsx new file mode 100644 index 0000000000..d45729d33c --- /dev/null +++ b/examples/tutorial/src/actions/carousel.jsx @@ -0,0 +1,65 @@ +import { + Button, + Carousel, + Element, + Pic, + Subtitle, + Text, + Title, +} from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + const movies = [ + { + name: 'Pulp Fiction', + desc: 'Le Big Mac', + url: 'https://www.imdb.com/title/tt0110912', + pic: + 'https://ia.media-imdb.com/images/M/MV5BMTkxMTA5OTAzMl5BMl5BanBnXkFtZTgwNjA5MDc3NjE@._V1_SY1000_CR0,0,673,1000_AL_.jpg', + }, + { + name: 'The Big Lebowski', + desc: 'Fuck it Dude', + url: 'https://www.imdb.com/title/tt0118715', + pic: 'https://upload.wikimedia.org/wikipedia/en/a/a7/Snatch_ver4.jpg', + }, + { + name: 'Snatch', + desc: 'Five minutes, Turkish', + url: 'https://www.imdb.com/title/tt0208092', + pic: + 'https://nebula.wsimg.com/obj/NzQ3QUYxQzZBNzE4NjNFRTc1MTU6NmM4YjgzZWVlZTE2MGMzM2RkMTdlZjdjNGUyZmFhNDE6Ojo6OjA=', + }, + ] + return ( + <> + + Great! Here we can see a carousel. It's a Facebook Messenger + component, and it's a group of elements which consists of an image, a + title, a subtitle and a group of buttons. You can get more information + here: + https://developers.facebook.com/docs/messenger-platform/send-messages/template/generic?locale=en_US#carousel + + + {movies.map((e, i) => ( + + + {e.name} + {e.desc} + + + ))} + + + I could spend a long time talking about Botonic's features, but I + think that's enough for now. Feel free to read through the code to + learn how to integrate NLP capabilities and use all kind of rich + messages. + + Now, please, type 'end'. + + ) + } +} diff --git a/examples/tutorial/src/actions/closed_webview.jsx b/examples/tutorial/src/actions/closed_webview.jsx new file mode 100644 index 0000000000..d12e2d24d7 --- /dev/null +++ b/examples/tutorial/src/actions/closed_webview.jsx @@ -0,0 +1,19 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + <> + This webview has been closed! + + I could spend a long time talking about Botonic's features, but I + think that's enough for now. Feel free to read through the code to + learn how to integrate NLP capabilities and use all kind of rich + messages. + + Now, please, type 'end'. + + ) + } +} diff --git a/examples/tutorial/src/actions/end.jsx b/examples/tutorial/src/actions/end.jsx new file mode 100644 index 0000000000..a930443e1a --- /dev/null +++ b/examples/tutorial/src/actions/end.jsx @@ -0,0 +1,16 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return ( + <> + That's it! You just finished this Tutorial!!🎉 + + Next, go back to the Getting Started Tutorial to learn how to create + your first bot action + + + ) + } +} diff --git a/examples/tutorial/src/actions/funny.jsx b/examples/tutorial/src/actions/funny.jsx new file mode 100644 index 0000000000..4d037a279e --- /dev/null +++ b/examples/tutorial/src/actions/funny.jsx @@ -0,0 +1,8 @@ +import { Text } from '@botonic/react' +import React from 'react' + +export default class extends React.Component { + render() { + return I know dude 😂 😂 😂 + } +} diff --git a/examples/tutorial/src/actions/media.jsx b/examples/tutorial/src/actions/media.jsx new file mode 100644 index 0000000000..05cc430229 --- /dev/null +++ b/examples/tutorial/src/actions/media.jsx @@ -0,0 +1,28 @@ +import { + Audio, + Button, + Document, + Image, + Location, + Text, + Video, +} from '@botonic/react' +import React from 'react' +export default class extends React.Component { + render() { + return ( + <> + Hey! What a nice pic! Thanks 😊 + + Let me share some files with you:{' '} + + + +