Skip to content
This repository has been archived by the owner on Feb 5, 2025. It is now read-only.

Commit

Permalink
feat: new voiceflow chat - initial commit (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
gillyb authored Sep 20, 2024
1 parent f9dbb16 commit 9198d6b
Show file tree
Hide file tree
Showing 200 changed files with 7,733 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/chat/.babelrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"]
}
3 changes: 3 additions & 0 deletions packages/chat/.dependency-cruiser.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createConfig } from '@voiceflow/dependency-cruiser-config';

export default createConfig({ allowTypeCycles: true });
1 change: 1 addition & 0 deletions packages/chat/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/test-results/
5 changes: 5 additions & 0 deletions packages/chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

9 changes: 9 additions & 0 deletions packages/chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# chat

## 🚧 WIP - Under Construction 🚧

This is going to be a totally redesigned and renewed version of the `react-chat` package.
It is definitely not ready to be used for any use-case.
Do not use this.

More details will come...
8 changes: 8 additions & 0 deletions packages/chat/__mocks__/@voiceflow/stitches-react.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { vi } from 'vitest';

export const createStitches = vi.fn().mockReturnValue({
styled: vi.fn().mockImplementation((el) => el),
keyframes: vi.fn(),
});

export const keyframes = vi.fn();
3 changes: 3 additions & 0 deletions packages/chat/chromatic.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"outputDir": "./storybook-static"
}
5 changes: 5 additions & 0 deletions packages/chat/config/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import '@testing-library/jest-dom/vitest';

import { vi } from 'vitest';

vi.mock('@voiceflow/stitches-react');
42 changes: 42 additions & 0 deletions packages/chat/e2e/embedded.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<title>Embedded Mode</title>
<style>
body {
background-color: #f9f9f9;
}

#flat-chat {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
}
</style>
</head>

<body>
<div id="flat-chat"></div>

<script type="text/javascript">
(function (d, t) {
var v = d.createElement(t),
s = d.getElementsByTagName(t)[0];
v.onload = function () {
window.voiceflow.chat.load({
verify: { projectID: 'projectID' },
render: {
mode: 'embedded',
target: document.getElementById('flat-chat'),
},
});
};
v.src = '../dist/bundle.mjs';
v.type = 'text/javascript';
s.parentNode.insertBefore(v, s);
})(document, 'script');
</script>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/chat/e2e/embedded.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { expect, test } from '@playwright/test';

test('renders embedded webchat and starts automatically', async ({ page }) => {
await page.goto('embedded');

const chat = page.locator('.vfrc-chat');
await chat.waitFor({ state: 'visible' });
expect(chat).toBeInViewport();
page.locator('.vfrc-footer .vfrc-button').click();

await page.locator('.vfrc-chat-input').waitFor({ state: 'visible' });
});
122 changes: 122 additions & 0 deletions packages/chat/e2e/extensions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!doctype html>
<html>
<head>
<title>Embedded Mode</title>
<style>
body {
background-color: #f9f9f9;
}

#voiceflow-chat-frame {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
}

#order-status {
position: absolute;
visibility: hidden;
left: 50%;
top: 8px;
transform: translateX(-50%);
padding: 8px;
border-radius: 10px;
background-color: orangered;
z-index: 10;

font-family: 'Open Sans', sans-serif;
font-size: 20px;
font-weight: 400;
}
</style>
</head>

<body>
<span id="order-status" data-testid="status"></span>
<div id="voiceflow-chat-frame"></div>
<template id="complex-form">
<form>
<input name="name" placeholder="What is your name?" />
<fieldset>
<legend>What kind of hair do you have?</legend>
<div>
<input type="radio" name="hair" id="straight" value="straight" checked /><label for="straight"
>Straight</label
>
</div>
<div><input type="radio" name="hair" id="curly" value="curly" /><label for="curly">Curly</label></div>
<div><input type="radio" name="hair" id="wavy" value="wavy" /><label for="wavy">Wavy</label></div>
</fieldset>
<button>submit</button>
</form>
</template>

<script type="text/javascript">
(function (d, t) {
var v = d.createElement(t),
s = d.getElementsByTagName(t)[0];
v.onload = () => {
window.voiceflow.chat.load({
verify: { projectID: 'projectID' },
render: { mode: 'embedded' },
autostart: true,
assistant: {
extensions: [
{
name: 'order_tracker',
type: 'effect',
match: ({ trace }) => trace.type === 'update_order_status',
effect({ trace }) {
const element = document.getElementById('order-status');
const status = trace.payload;

element.style.visibility = 'visible';
element.innerText = status;
},
},
{
name: 'onboarding_form',
type: 'response',
match: ({ trace }) => trace.type === 'onboarding',
render({ trace, element }) {
const template = document.getElementById('complex-form').content.cloneNode(true);
const id = 'onboarding-form-' + Date.now();
template.firstElementChild.id = id;

element.appendChild(template);

const form = element.querySelector(`#${id}`);
form.addEventListener('submit', async (event) => {
event.preventDefault();

await window.voiceflow.chat.interact({
type: 'submit',
payload: {
name: form.elements.name.value,
hair: form.elements.hair.value,
},
});
while (form.firstChild) {
form.removeChild(form.firstChild);
}

const confirmation = document.createElement('em');
confirmation.innerText = `submitted ✅`;

form.appendChild(confirmation);
});
},
},
],
},
});
};
v.src = '../dist/bundle.mjs';
v.type = 'text/javascript';
s.parentNode.insertBefore(v, s);
})(document, 'script');
</script>
</body>
</html>
118 changes: 118 additions & 0 deletions packages/chat/e2e/extensions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { expect, test } from '@playwright/test';

import { slateMessage } from './utils';

const RUNTIME_URL = 'https://general-runtime.voiceflow.com/public/projectID/state/user/*/interact';

test('trigger effect extension on incoming trace', async ({ page }) => {
const systemMessages = [
'Welcome to the pizza palace!',
'What kind of pizza do you want?',
'One cheese pizza coming right up',
];
const userMessages = ['I want to order a pizza', 'Cheese please'];
const traceType = 'update_order_status';
let count = 0;

// eslint-disable-next-line consistent-return
await page.route(RUNTIME_URL, async (route) => {
count++;

switch (count) {
case 1:
return route.fulfill({
json: {
trace: [{ type: traceType, payload: 'idle' }, slateMessage(systemMessages[0])],
},
});

case 2:
return route.fulfill({
json: {
trace: [{ type: traceType, payload: 'in progress' }, slateMessage(systemMessages[1])],
},
});

case 3:
return route.fulfill({
json: {
trace: [{ type: traceType, payload: 'ordered' }, slateMessage(systemMessages[2])],
},
});

default:
}
});

await page.goto('extensions');

const chat = page.locator('.vfrc-chat');
await chat.waitFor({ state: 'visible' });
expect(chat).toBeInViewport();

await page.locator('[data-testid="status"]', { hasText: 'idle' }).waitFor({ state: 'visible' });
await page.locator('.vfrc-message', { hasText: systemMessages[0] }).waitFor({ state: 'visible' });

const input = page.locator('.vfrc-chat-input textarea');
await input.waitFor({ state: 'visible' });
await input.fill(userMessages[0]);

const submit = page.locator('.vfrc-chat-input .vfrc-bubble');
await submit.click();

await page.locator('.vfrc-message', { hasText: userMessages[0] }).waitFor({ state: 'visible' });
await page.locator('.vfrc-message', { hasText: systemMessages[1] }).waitFor({ state: 'visible' });
await page.locator('[data-testid="status"]', { hasText: 'in progress' }).waitFor({ state: 'visible' });

await input.fill(userMessages[1]);
await submit.click();

await page.locator('.vfrc-message', { hasText: userMessages[1] }).waitFor({ state: 'visible' });
await page.locator('.vfrc-message', { hasText: systemMessages[2] }).waitFor({ state: 'visible' });
await page.locator('[data-testid="status"]', { hasText: 'ordered' }).waitFor({ state: 'visible' });
});

test('render response extension from incoming trace', async ({ page }) => {
let count = 0;

await page.route(RUNTIME_URL, (route) => {
count++;

switch (count) {
case 1:
return route.fulfill({
json: {
trace: [slateMessage("Welcome to Sal's Salon! Tell me about yourself."), { type: 'onboarding' }],
},
});
case 2:
default:
expect(route.request().postDataJSON()).toEqual({
action: {
type: 'submit',
payload: { name: 'Alex', hair: 'curly' },
},
});

return route.fulfill({ json: { trace: [] } });
}
});

await page.goto('extensions');

const chat = page.locator('.vfrc-chat');
await chat.waitFor({ state: 'visible' });
expect(chat).toBeInViewport();

await page.locator('.vfrc-message').waitFor({ state: 'visible' });

const extensionMessage = page.locator('.vfrc-message--extension-onboarding_form');
await extensionMessage.waitFor({ state: 'visible' });

await extensionMessage.locator('[name="name"]').fill('Alex');
await extensionMessage.locator('[name="hair"][id="curly"]').click();
await extensionMessage.getByRole('button').click();
await page
.locator('.vfrc-message--extension-onboarding_form', { hasText: 'submitted ✅' })
.waitFor({ state: 'visible' });
});
29 changes: 29 additions & 0 deletions packages/chat/e2e/overlay.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!doctype html>
<html>
<head>
<title>Overlay mode</title>
<style>
body {
background-color: #f9f9f9;
}
</style>
</head>

<body>
<script type="text/javascript">
(function (d, t) {
var v = d.createElement(t),
s = d.getElementsByTagName(t)[0];
v.onload = function () {
window.voiceflow.chat.load({
verify: { projectID: 'projectID' },
render: { mode: 'overlay' },
});
};
v.src = '../dist/bundle.mjs';
v.type = 'text/javascript';
s.parentNode.insertBefore(v, s);
})(document, 'script');
</script>
</body>
</html>
Loading

0 comments on commit 9198d6b

Please sign in to comment.