├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── docs
├── ai-assistants-light.png
└── ai-assistants.png
├── examples
└── next-js-example
│ ├── .eslintrc.json
│ ├── README.md
│ ├── next.config.mjs
│ ├── package-lock.json
│ ├── package.json
│ ├── postcss.config.js
│ ├── project.json
│ ├── public
│ ├── next.svg
│ └── vercel.svg
│ ├── src
│ ├── app
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── token
│ │ │ └── route.ts
│ └── components
│ │ └── ExampleForm.tsx
│ ├── tailwind.config.ts
│ └── tsconfig.json
├── nx.json
├── package-lock.json
├── package.json
├── packages
├── assistants-react
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── components
│ │ │ ├── AssistantChat.tsx
│ │ │ ├── AssistantChatDialog.tsx
│ │ │ ├── AssistantChatLogProps.tsx
│ │ │ ├── ChatMessageWrapper.tsx
│ │ │ ├── TypingIndicator.tsx
│ │ │ └── chat-composer-plugins
│ │ │ │ ├── EnterKeySubmitPlugin.tsx
│ │ │ │ └── SendButtonPlugin.tsx
│ │ ├── hooks
│ │ │ └── useAssistant.ts
│ │ └── index.ts
│ ├── tests
│ │ └── index.test.ts
│ └── tsconfig.json
└── assistants
│ ├── README.md
│ ├── package.json
│ ├── src
│ └── index.ts
│ ├── tests
│ └── index.test.ts
│ └── tsconfig.json
├── tools
└── build.js
└── vitest.workspace.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 |
133 | # dependencies
134 | /node_modules
135 | /.pnp
136 | .pnp.js
137 | .yarn/install-state.gz
138 |
139 | # testing
140 | /coverage
141 |
142 | # next.js
143 | .next/
144 | out/
145 |
146 | # production
147 | /build
148 |
149 | # misc
150 | .DS_Store
151 | *.pem
152 |
153 | # debug
154 | npm-debug.log*
155 | yarn-debug.log*
156 | yarn-error.log*
157 |
158 | # local env files
159 | .env*.local
160 |
161 | # vercel
162 | .vercel
163 |
164 | # typescript
165 | *.tsbuildinfo
166 | next-env.d.ts
167 |
168 | .nx/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "tabWidth": 2
4 | }
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Twilio Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |


2 | Twilio AI Assistants — JavaScript & React SDK
3 |
4 | > [!NOTE]
5 | > Twilio AI Assistants is a [Twilio Alpha](https://twilioalpha.com) project that is currently in Developer Preview. If you would like to try AI Assistants, [join the waitlist](https://twilioalpha.com/ai-assistants).
6 |
7 | ## Packages
8 |
9 | - [`@twilio-alpha/assistants-react`](./packages/assistants-react/) — Library to add an AI Assistant to your React application
10 | - [`@twilio-alpha/assistants`](./packages/assistants/) — Base JavaScript library to bring your own framework
11 |
12 | ## Examples
13 |
14 | - [Next.js demo](./examples/assistant-js-example/src/app/page.tsx);
15 |
16 | ## Contributing
17 |
18 | This repo uses npm workspaces.
19 |
--------------------------------------------------------------------------------
/docs/ai-assistants-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio-labs/ai-assistants-js/e32f67e0a4ea7a0082589bcc3f76b101c0d82ee3/docs/ai-assistants-light.png
--------------------------------------------------------------------------------
/docs/ai-assistants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio-labs/ai-assistants-js/e32f67e0a4ea7a0082589bcc3f76b101c0d82ee3/docs/ai-assistants.png
--------------------------------------------------------------------------------
/examples/next-js-example/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/next-js-example/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/examples/next-js-example/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | async headers() {
4 | return [
5 | {
6 | // matching all API routes
7 | source: "/token",
8 | headers: [
9 | { key: "Access-Control-Allow-Credentials", value: "true" },
10 | { key: "Access-Control-Allow-Origin", value: "*" },
11 | {
12 | key: "Access-Control-Allow-Methods",
13 | value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
14 | },
15 | {
16 | key: "Access-Control-Allow-Headers",
17 | value:
18 | "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
19 | },
20 | ],
21 | },
22 | ];
23 | },
24 | };
25 |
26 | export default nextConfig;
27 |
--------------------------------------------------------------------------------
/examples/next-js-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-js-example",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@twilio-alpha/assistants-react": "^0.0.1",
13 | "next": "14.1.3",
14 | "react": "^18.2",
15 | "react-dom": "^18.2",
16 | "twilio": "^5.0.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20",
20 | "@types/react": "^18",
21 | "@types/react-dom": "^18",
22 | "autoprefixer": "^10.0.1",
23 | "eslint": "^8",
24 | "eslint-config-next": "14.1.3",
25 | "postcss": "^8",
26 | "tailwindcss": "^3.3.0",
27 | "typescript": "^5"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/examples/next-js-example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/examples/next-js-example/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
3 | "name": "next-js-example",
4 | "projectType": "application",
5 | "sourceRoot": "examples/next-js-example",
6 | "targets": {
7 | "watch": {
8 | "executor": "nx:run-commands",
9 | "options": {
10 | "commands": [
11 | "next dev",
12 | "nx watch --projects=next-js-example --includeDependentProjects -- nx run-many -t build -p \\$NX_PROJECT_NAME --exclude=next-js-example"
13 | ],
14 | "cwd": "examples/next-js-example",
15 | "parallel": true
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/next-js-example/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/next-js-example/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/next-js-example/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twilio-labs/ai-assistants-js/e32f67e0a4ea7a0082589bcc3f76b101c0d82ee3/examples/next-js-example/src/app/favicon.ico
--------------------------------------------------------------------------------
/examples/next-js-example/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/examples/next-js-example/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/next-js-example/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ExampleForm, ExampleFormData } from "@/components/ExampleForm";
4 | import { AssistantChat } from "@twilio-alpha/assistants-react";
5 | import { useEffect, useState } from "react";
6 |
7 | export default function Home() {
8 | const [token, setToken] = useState("");
9 | const [conversationSid, setConversationSid] = useState();
10 | const [formData, setFormData] = useState({
11 | firstName: "",
12 | lastName: "",
13 | email: "",
14 | interest: "",
15 | company: "",
16 | });
17 |
18 | useEffect(() => {
19 | setConversationSid(localStorage.getItem("CONVERSATIONS_SID") || undefined);
20 | fetch("/token")
21 | .then((resp) => resp.json())
22 | .then(({ token }) => {
23 | setToken(token);
24 | });
25 | }, []);
26 |
27 | function saveConversationSid(sid: string) {
28 | localStorage.setItem("CONVERSATIONS_SID", sid);
29 | }
30 |
31 | function fillForm(data: ExampleFormData) {
32 | console.log("new data", data);
33 | setFormData(data);
34 | }
35 |
36 | return (
37 |
38 |
Assistants Demo
39 |
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/examples/next-js-example/src/app/token/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import * as Twilio from "twilio";
3 |
4 | export async function GET(req: NextRequest) {
5 | if (process.env.NODE_ENV === "production") {
6 | return NextResponse.json({ error: "not for production" }, { status: 401 });
7 | }
8 |
9 | if (
10 | typeof process.env.TWILIO_ACCOUNT_SID !== "string" ||
11 | typeof process.env.TWILIO_API_KEY_SID !== "string" ||
12 | typeof process.env.TWILIO_API_KEY_SECRET !== "string"
13 | ) {
14 | return NextResponse.json({ error: "Missing credentials" }, { status: 500 });
15 | }
16 |
17 | let AccessToken = Twilio.jwt.AccessToken;
18 | let token = new AccessToken(
19 | process.env.TWILIO_ACCOUNT_SID,
20 | process.env.TWILIO_API_KEY_SID,
21 | process.env.TWILIO_API_KEY_SECRET,
22 | {
23 | identity: "email:demo-chat@example.com",
24 | ttl: 3600,
25 | }
26 | );
27 |
28 | let grant = new AccessToken.ChatGrant({
29 | serviceSid: process.env.SERVICE_SID,
30 | });
31 | token.addGrant(grant);
32 | return NextResponse.json({ token: token.toJwt() });
33 | }
34 |
--------------------------------------------------------------------------------
/examples/next-js-example/src/components/ExampleForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export type ExampleFormData = {
4 | firstName: string;
5 | lastName: string;
6 | email: string;
7 | interest: string;
8 | company: string;
9 | };
10 |
11 | export type ExampleFormProps = {
12 | data: ExampleFormData;
13 | };
14 |
15 | export function ExampleForm({ data }: ExampleFormProps) {
16 | return (
17 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/examples/next-js-example/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/examples/next-js-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/nx/schemas/nx-schema.json",
3 | "targetDefaults": {
4 | "dev": {
5 | "cache": true,
6 | "dependsOn": ["^build"]
7 | },
8 | "build": {
9 | "cache": true,
10 | "dependsOn": ["^build"]
11 | },
12 | "test": {
13 | "cache": true,
14 | "dependsOn": ["^build"]
15 | },
16 | "lib-pack": {
17 | "dependsOn": ["^clean", "^build"]
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "assistants",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "vitest",
8 | "start:nextjs": "npm run dev -w next-js-example",
9 | "lib-pack": "nx run-many -t lib-pack --exclude=next-js-example"
10 | },
11 | "author": "",
12 | "license": "MIT",
13 | "workspaces": [
14 | "packages/*",
15 | "examples/*"
16 | ],
17 | "devDependencies": {
18 | "nx": "^18.2.3",
19 | "react": "18.2",
20 | "react-dom": "18.2",
21 | "rimraf": "^5.0.5",
22 | "vitest": "^1.4.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/assistants-react/README.md:
--------------------------------------------------------------------------------
1 | # Twilio AI Assistants - React SDK
2 |
3 | > [!NOTE]
4 | > Twilio AI Assistants is a [Twilio Alpha](https://twilioalpha.com) project that is currently in Developer Preview. If you would like to try AI Assistants, [join the waitlist](https://twilioalpha.com/ai-assistants).
5 |
6 | ## Installation
7 |
8 | > [!NOTE]
9 | > Requires `react` & `react-dom` to be installed
10 |
11 | ```bash
12 | npm install @twilio-alpha/assistants-react @twilio/conversations
13 | ```
14 |
15 | ## Usage
16 |
17 | ### Basic usage
18 |
19 | ```jsx
20 | import { AssistantChat } from "@twilio-alpha/assistants-react";
21 |
22 | export function App() {
23 | return ;
24 | }
25 | ```
26 |
27 | ### Using with your own UI
28 |
29 | If you want to reuse your own UI you can use the `useAssistant` hook instead.
30 |
31 | ```jsx
32 | import { useAssistant } from "@twilio-alpha/assistants-react";
33 |
34 | export function App() {
35 | const { messages, sendMessage } = useAssistant("", {
36 | assistantSid: "",
37 | });
38 |
39 | function send(evt) {
40 | evt.preventDefault();
41 | sendMessage(evt.target.message.value);
42 | }
43 |
44 | return (
45 | <>
46 |
47 | {messages.map((message) => {
48 | return (
49 | -
50 | {message.author}: {message.body}
51 |
52 | );
53 | })}
54 |
55 |
59 | >
60 | );
61 | }
62 | ```
63 |
64 | ### Reusing existing session
65 |
66 | By default every time the `AssistantChat` component gets rendered it will create a new Twilio Conversation (aka a session). If you want to re-use the existing one between page refreshes you can use the `conversationSid` property and the `onConversationSetup` handler to persist the session. This gives you full control on how you want to manage the session. If there is no `conversationSid`, the component will automatically create one.
67 |
68 | Example code:
69 |
70 | ```jsx
71 | import { useEffect, useState } from "react";
72 |
73 | export function App() {
74 | const [conversationSid, setConversationSid] = useState();
75 |
76 | useEffect(() => {
77 | // fetches an existing conversation SID from the local storage if it exists
78 | setConversationSid(localStorage.getItem("CONVERSATIONS_SID") || undefined);
79 | }, []);
80 |
81 | function saveConversationSid(sid: string) {
82 | localStorage.setItem("CONVERSATIONS_SID", sid);
83 | }
84 |
85 | return (
86 |
93 | );
94 | }
95 | ```
96 |
97 | ## License
98 |
99 | MIT
100 |
--------------------------------------------------------------------------------
/packages/assistants-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@twilio-alpha/assistants-react",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "module": "dist/index.mjs",
8 | "files": [
9 | "README.md",
10 | "dist/"
11 | ],
12 | "scripts": {
13 | "clean": "rimraf dist/",
14 | "build": "node ../../tools/build.js src/index.ts && tsc",
15 | "test": "vitest",
16 | "lib-pack": "npm pack"
17 | },
18 | "keywords": [],
19 | "author": "Twilio Alpha ",
20 | "license": "MIT",
21 | "devDependencies": {
22 | "@types/react": "^18.2.65",
23 | "@types/react-dom": "^18.2.21",
24 | "esbuild": "^0.20.1",
25 | "esbuild-node-externals": "^1.13.0",
26 | "react": "^18.2.0",
27 | "react-dom": "^18.2.0",
28 | "typescript": "^5.4.2"
29 | },
30 | "peerDependencies": {
31 | "react": ">=18"
32 | },
33 | "dependencies": {
34 | "@twilio-alpha/assistants": "^0.0.1",
35 | "@twilio-paste/anchor": "^12.1.0",
36 | "@twilio-paste/avatar": "^9.1.0",
37 | "@twilio-paste/box": "^10.3.0",
38 | "@twilio-paste/button": "^14.1.0",
39 | "@twilio-paste/chat-composer": "^5.1.1",
40 | "@twilio-paste/chat-log": "^5.2.1",
41 | "@twilio-paste/flex": "^8.1.0",
42 | "@twilio-paste/form": "^11.1.1",
43 | "@twilio-paste/icons": "^12.3.0",
44 | "@twilio-paste/input": "^9.1.2",
45 | "@twilio-paste/lexical-library": "^4.1.0",
46 | "@twilio-paste/minimizable-dialog": "^4.1.1",
47 | "@twilio-paste/skeleton-loader": "^6.1.1",
48 | "@twilio-paste/stack": "^8.1.0",
49 | "@twilio-paste/status": "^2.1.1",
50 | "@twilio-paste/text": "^10.1.1",
51 | "@twilio-paste/theme": "^11.1.0",
52 | "react-markdown": "^9.0.1",
53 | "zod": "^3.22.4"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/AssistantChat.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { AssistantChatDialog } from "./AssistantChatDialog";
3 | import useAssistant, { UseAssistantOptions } from "../hooks/useAssistant";
4 |
5 | export interface AssistantChatProps {
6 | /**
7 | * A valid JWT with a Twilio Conversations grant
8 | * - {@link https://www.twilio.com/docs/iam/access-tokens#create-an-access-token-for-conversations | Learn more}
9 | */
10 | token: string;
11 |
12 | /**
13 | * The SID of your Twilio AI Assistant
14 | */
15 | assistantSid: string;
16 |
17 | /**
18 | * A conversation SID if you want to connect to an existing conversation
19 | */
20 | conversationSid?: string;
21 |
22 | /**
23 | * Callback for when the client has connected to a conversation.
24 | * This can be used in conjunction with the `conversationSid` property if
25 | * you want to save & reconnect to a conversation between page reloads
26 | *
27 | * @param conversationSid
28 | * @returns
29 | */
30 | onConversationSetup?: (conversationSid: string) => void;
31 |
32 | /**
33 | * Handlers for any UI Tools you want the Twilio AI Assistant to be able to use.
34 | * This requires for the UI Tools to be properly configured.
35 | * - {@link https://www.twilio.com/docs/alpha/ai-assistants/code-samples/ui-tools | Learn more about UI tools}
36 | */
37 | toolHandlers?: UseAssistantOptions["toolHandlers"];
38 | }
39 |
40 | /**
41 | * Renders a full popup chat for you AI Assistant, incl. both a button and
42 | * the chat dialog when the button gets pressed.
43 | *
44 | * @example
45 | *
46 | *
47 | * @param {AssistantChatProps} props
48 | * @returns
49 | */
50 | export function AssistantChat({
51 | token,
52 | assistantSid,
53 | toolHandlers,
54 | conversationSid,
55 | onConversationSetup,
56 | }: AssistantChatProps) {
57 | if (!token || !assistantSid) {
58 | return <>>;
59 | }
60 |
61 | const assistant = useAssistant(token, {
62 | assistantSid,
63 | toolHandlers,
64 | conversationSid,
65 | });
66 |
67 | useEffect(() => {
68 | if (
69 | conversationSid !== assistant.conversationSid &&
70 | typeof assistant.conversationSid === "string" &&
71 | typeof onConversationSetup === "function"
72 | ) {
73 | onConversationSetup(assistant.conversationSid);
74 | }
75 | }, [conversationSid, assistant.conversationSid]);
76 |
77 | return ;
78 | }
79 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/AssistantChatDialog.tsx:
--------------------------------------------------------------------------------
1 | import { Theme } from "@twilio-paste/theme";
2 | import { ChatIcon } from "@twilio-paste/icons/esm/ChatIcon";
3 | import {
4 | MinimizableDialog,
5 | MinimizableDialogButton,
6 | MinimizableDialogContainer,
7 | MinimizableDialogContent,
8 | MinimizableDialogHeader,
9 | } from "@twilio-paste/minimizable-dialog";
10 | import React, { useState } from "react";
11 | import { Flex } from "@twilio-paste/flex";
12 | import { StatusBadge, StatusBadgeVariants } from "@twilio-paste/status";
13 | import { Box } from "@twilio-paste/box";
14 | import { ConnectionState } from "@twilio/conversations";
15 | import { match } from "ts-pattern";
16 |
17 | import { ChatComposer, ChatComposerProps } from "@twilio-paste/chat-composer";
18 | import { $getRoot, ClearEditorPlugin } from "@twilio-paste/lexical-library";
19 | import { SendButtonPlugin } from "./chat-composer-plugins/SendButtonPlugin";
20 | import { EnterKeySubmitPlugin } from "./chat-composer-plugins/EnterKeySubmitPlugin";
21 | import {
22 | AssistantChatLogProps,
23 | AssistantChatLog,
24 | } from "./AssistantChatLogProps";
25 |
26 | export type AssistantChatDialogProps = {} & AssistantChatLogProps;
27 |
28 | export function AssistantChatDialog(props: AssistantChatDialogProps) {
29 | const { sendMessage, state } = props;
30 |
31 | const [messageInput, setMessageInput] = useState("");
32 |
33 | const connectionString = match(state)
34 | .with("connected", () => "ConnectivityAvailable")
35 | .with("disconnected", () => "ConnectivityOffline")
36 | .otherwise(() => "ConnectivityBusy");
37 |
38 | const isLoading = state === "connected" ? false : true;
39 |
40 | const handleComposerChange: ChatComposerProps["onChange"] = (editorState) => {
41 | editorState.read(() => {
42 | const text = $getRoot().getTextContent();
43 | setMessageInput(text);
44 | });
45 | };
46 |
47 | const submitMessage = (): void => {
48 | if (messageInput === "") return;
49 | sendMessage(messageInput);
50 | };
51 |
52 | return (
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 |
65 | Live chat
66 |
67 | {state}
68 |
69 |
70 |
71 |
72 |
73 |
84 | {
89 | throw error;
90 | },
91 | }}
92 | ariaLabel="Message"
93 | placeholder="Type here..."
94 | onChange={handleComposerChange}
95 | >
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/AssistantChatLogProps.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from "react";
2 | import { Box, BoxProps } from "@twilio-paste/box";
3 | import { ChatBookend, ChatBookendItem, ChatLog } from "@twilio-paste/chat-log";
4 | import { UseAssistantOutput } from "../hooks/useAssistant";
5 | import { ChatMessageWrapper } from "./ChatMessageWrapper";
6 | import { TypingIndicator } from "./TypingIndicator";
7 |
8 | export type AssistantChatLogProps = {
9 | /**
10 | * Enables additional debug output in the Chat UI such as the
11 | * Conversations SID and the identity of the user
12 | */
13 | _debug?: boolean;
14 | } & UseAssistantOutput &
15 | BoxProps;
16 |
17 | /**
18 | * Renders a full chat log (without input) based on the output of `useAssistant`.
19 | * To modify the container behavior you can use {@link https://paste.twilio.design/primitives/box | Twilio Paste's Box properties}
20 | *
21 | * @example Regular use
22 | * const assistant = useAssistant(...)
23 | * return
24 | *
25 | * @example Custom styling: Removing max height
26 | * return
27 | *
28 | * @param {AssistantChatLogProps} props
29 | */
30 | export function AssistantChatLog({
31 | _debug,
32 | conversationSid,
33 | identity,
34 | isTyping,
35 | messages,
36 | ...props
37 | }: AssistantChatLogProps) {
38 | const scrollerRef = useRef(null);
39 | const loggerRef = useRef(null);
40 | const [mounted, setMounted] = useState(false);
41 |
42 | useEffect(() => {
43 | setMounted(true);
44 | }, []);
45 |
46 | useEffect(() => {
47 | if (!mounted || !loggerRef.current) return;
48 | scrollerRef.current?.scrollTo({
49 | top: loggerRef.current.scrollHeight,
50 | behavior: "smooth",
51 | });
52 | }, [messages, isTyping, mounted]);
53 |
54 | return (
55 |
64 |
65 | {_debug && (
66 |
67 | SID: {conversationSid}
68 | Identity: {identity}
69 |
70 | )}
71 | {messages.map((message) => {
72 | return ;
73 | })}
74 | {isTyping ? : null}
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/ChatMessageWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | ChatBubble,
4 | ChatMessage,
5 | ChatMessageMeta,
6 | ChatMessageMetaItem,
7 | MessageVariants,
8 | } from "@twilio-paste/chat-log";
9 | import { Message } from "@twilio/conversations";
10 | import Markdown from "react-markdown";
11 | import { Box } from "@twilio-paste/box";
12 | import { Anchor } from "@twilio-paste/anchor";
13 | import { JsxRuntimeComponents } from "react-markdown/lib";
14 |
15 | export interface ChatMessageProps {
16 | message: Message;
17 | }
18 |
19 | function markdownComponents(
20 | variant: "inbound" | "outbound"
21 | ): Partial {
22 | return {
23 | a: ({ node, ...props }) => {
24 | return (
25 | // @ts-ignore
26 |
30 | );
31 | },
32 | blockquote: ({ node, ...props }) => {
33 | return (
34 | // @ts-ignore
35 |
46 | );
47 | },
48 | };
49 | }
50 |
51 | /**
52 | * Renders both an incoming our outgoing message to be used in a Twilio Paste ChatLog
53 | * Message bodies get treated as markdown using {@link https://www.npmjs.com/package/react-markdown | react-markdown}
54 | * to enable formatting.
55 | *
56 | * @param {ChatMessageProps} props
57 | */
58 | export function ChatMessageWrapper({ message }: ChatMessageProps) {
59 | const variant: MessageVariants =
60 | message.author === "system" ? "inbound" : "outbound";
61 |
62 | const timeString = message.dateUpdated
63 | ? new Intl.DateTimeFormat(undefined, { timeStyle: "short" }).format(
64 | message.dateUpdated
65 | )
66 | : "";
67 |
68 | return (
69 |
70 |
71 |
72 | {message.body}
73 |
74 |
75 |
76 | {timeString}
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/TypingIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | ChatMessage,
4 | ChatMessageMeta,
5 | ChatMessageMetaItem,
6 | } from "@twilio-paste/chat-log";
7 | import { SkeletonLoader } from "@twilio-paste/skeleton-loader";
8 | import { Stack } from "@twilio-paste/stack";
9 |
10 | /**
11 | * Typing indicator to be used in conjunction with a Twilio Paste ChatLog component
12 | */
13 | export function TypingIndicator() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Typing...
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/chat-composer-plugins/EnterKeySubmitPlugin.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | CLEAR_EDITOR_COMMAND,
4 | COMMAND_PRIORITY_HIGH,
5 | KEY_ENTER_COMMAND,
6 | useLexicalComposerContext,
7 | } from "@twilio-paste/lexical-library";
8 |
9 | /**
10 | * Plugin for the Twilio Paste ChatComposer component to handle "hit enter to send"
11 | * {@link https://paste.twilio.design/components/chat-composer#adding-interactivity-with-plugins | Twilio Paste Docs}
12 | */
13 | export const EnterKeySubmitPlugin = ({
14 | onKeyDown,
15 | }: {
16 | onKeyDown: () => void;
17 | }): null => {
18 | const [editor] = useLexicalComposerContext();
19 |
20 | const handleEnterKey = React.useCallback(
21 | (event: KeyboardEvent) => {
22 | const { shiftKey, ctrlKey } = event;
23 | if (shiftKey || ctrlKey) return false;
24 | event.preventDefault();
25 | event.stopPropagation();
26 | onKeyDown();
27 | editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
28 | return true;
29 | },
30 | [editor, onKeyDown]
31 | );
32 |
33 | React.useEffect(() => {
34 | return editor.registerCommand(
35 | KEY_ENTER_COMMAND,
36 | handleEnterKey,
37 | COMMAND_PRIORITY_HIGH
38 | );
39 | }, [editor, handleEnterKey]);
40 | return null;
41 | };
42 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/components/chat-composer-plugins/SendButtonPlugin.tsx:
--------------------------------------------------------------------------------
1 | import { SendIcon } from "@twilio-paste/icons/esm/SendIcon";
2 | import React from "react";
3 | import { Box } from "@twilio-paste/box";
4 | import {
5 | CLEAR_EDITOR_COMMAND,
6 | useLexicalComposerContext,
7 | } from "@twilio-paste/lexical-library";
8 | import { Button } from "@twilio-paste/button";
9 |
10 | /**
11 | * Component rendering a send button for the Twilio Paste ChatComposer component
12 | * {@link https://paste.twilio.design/components/chat-composer#adding-interactivity-with-plugins | Twilio Paste Docs}
13 | */
14 | export const SendButtonPlugin = ({
15 | onClick,
16 | }: {
17 | onClick: () => void;
18 | }): JSX.Element => {
19 | const [editor] = useLexicalComposerContext();
20 |
21 | const handleSend = (): void => {
22 | onClick();
23 | editor.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
24 | };
25 |
26 | return (
27 |
28 |
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/hooks/useAssistant.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionState, Message } from "@twilio/conversations";
2 | import { useEffect, useRef, useState } from "react";
3 |
4 | import Assistant, { AssistantOptions } from "@twilio-alpha/assistants";
5 |
6 | export type UseAssistantOptions = Partial & {
7 | /**
8 | * The SID of your Twilio AI Assistant
9 | */
10 | assistantSid: string;
11 |
12 | /**
13 | * A conversation SID if you want to connect to an existing conversation
14 | */
15 | conversationSid?: string;
16 |
17 | /**
18 | * Handlers for any UI Tools you want the Twilio AI Assistant to be able to use.
19 | * This requires for the UI Tools to be properly configured.
20 | * - {@link https://www.twilio.com/docs/alpha/ai-assistants/code-samples/ui-tools | Learn more about UI tools}
21 | */
22 | toolHandlers?: {
23 | [key: string]: (data: any) => void;
24 | };
25 | };
26 |
27 | export type UseAssistantOutput = {
28 | /**
29 | * A list of all the messages in the conversation
30 | */
31 | messages: Message[];
32 |
33 | /**
34 | * Connection state to the conversation
35 | */
36 | state: ConnectionState;
37 |
38 | /**
39 | * Function to send a new message to the AI Assistant / Conversation
40 | *
41 | * @param message The new message to send
42 | * @returns success
43 | */
44 | sendMessage: (message: string) => Promise;
45 |
46 | /**
47 | *
48 | */
49 | conversationSid?: string;
50 |
51 | /**
52 | * Indicates when the AI Assistant is generating a new response.
53 | * Only works in combination with the {@link https://www.twilio.com/docs/alpha/ai-assistants/code-samples/conversations | AI Assistants Conversations integration}
54 | */
55 | isTyping: boolean;
56 |
57 | /**
58 | * The Twilio Conversations identity of the user that's logged in
59 | */
60 | identity?: string;
61 | };
62 |
63 | export function useAssistant(
64 | /**
65 | * A valid JWT with a Twilio Conversations grant
66 | * - {@link https://www.twilio.com/docs/iam/access-tokens#create-an-access-token-for-conversations | Learn more}
67 | */
68 | token: string,
69 | options: UseAssistantOptions
70 | ): UseAssistantOutput {
71 | const assistantClient = useRef();
72 | const [messages, setMessages] = useState([]);
73 | const [state, setConnectionState] = useState("unknown");
74 | const [conversationSid, setConversationSid] = useState();
75 | const [identity, setIdentity] = useState();
76 | const [isTyping, setIsTyping] = useState(false);
77 |
78 | useEffect(() => {
79 | if (typeof token !== "string" || token.length === 0) {
80 | return;
81 | }
82 |
83 | assistantClient.current = new Assistant(token, {});
84 | assistantClient.current.on("messagesChanged", (messages) => {
85 | setMessages(messages);
86 | });
87 | assistantClient.current.on("statusChanged", (state) => {
88 | setConnectionState(state);
89 | });
90 | assistantClient.current.on(
91 | "joinedConversation",
92 | ({ conversationSid, identity }) => {
93 | setConversationSid(conversationSid);
94 | setIdentity(identity);
95 | }
96 | );
97 | assistantClient.current.on("assistantTypingStarted", () => {
98 | setIsTyping(true);
99 | });
100 | assistantClient.current.on("assistantTypingEnded", () => {
101 | setIsTyping(false);
102 | });
103 | assistantClient.current.on("uiToolTriggered", (payload) => {
104 | if (
105 | options.toolHandlers &&
106 | typeof options.toolHandlers[payload.name] === "function"
107 | ) {
108 | options.toolHandlers[payload.name](payload.data);
109 | }
110 | });
111 |
112 | return () => {
113 | assistantClient.current?.removeAllListeners();
114 | assistantClient.current?.destroy();
115 | assistantClient.current = undefined;
116 | };
117 | }, [token, options.conversationSid]);
118 |
119 | useEffect(() => {
120 | if (
121 | state === "connected" &&
122 | options.assistantSid &&
123 | assistantClient.current &&
124 | !assistantClient.current.conversation
125 | ) {
126 | assistantClient.current.start(
127 | options.assistantSid,
128 | options.conversationSid
129 | );
130 | }
131 | }, [state, options.assistantSid]);
132 |
133 | async function sendMessage(message: string) {
134 | return assistantClient.current?.sendMessage(message) || false;
135 | }
136 |
137 | return { state, messages, sendMessage, conversationSid, isTyping, identity };
138 | }
139 |
140 | export default useAssistant;
141 |
--------------------------------------------------------------------------------
/packages/assistants-react/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./components/AssistantChatDialog";
2 | export * from "./components/AssistantChat";
3 | export * from "./hooks/useAssistant";
4 |
--------------------------------------------------------------------------------
/packages/assistants-react/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, expectTypeOf, test } from "vitest";
2 | import { FC } from "react";
3 | import { useAssistant, AssistantChat, AssistantChatProps } from "../src/index";
4 |
5 | // TODO: add more tests
6 |
7 | test("exports right items", () => {
8 | expectTypeOf(useAssistant).toBeCallableWith("", {
9 | assistantSid: "",
10 | });
11 | expectTypeOf(AssistantChat).toMatchTypeOf>();
12 | });
13 |
--------------------------------------------------------------------------------
/packages/assistants-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "target": "esnext",
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "declaration": true,
8 | "strict": true,
9 | "moduleResolution": "node",
10 | "jsx": "react",
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "emitDeclarationOnly": true,
14 | "outDir": "dist",
15 | "rootDir": "src"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/assistants/README.md:
--------------------------------------------------------------------------------
1 | # Twilio AI Assistants - JavaScript SDK
2 |
3 | > [!NOTE]
4 | > Twilio AI Assistants is a [Twilio Alpha](https://twilioalpha.com) project that is currently in Developer Preview. If you would like to try AI Assistants, [join the waitlist](https://twilioalpha.com/ai-assistants).
5 |
6 | ## Installation
7 |
8 | ```bash
9 | npm install @twilio-alpha/assistants @twilio/conversations
10 | ```
11 |
12 | ## Usage
13 |
14 | ```js
15 | import Assistant from "@twilio-alpha/assistants-react";
16 |
17 | const assistant = new Assistant("");
18 | let messages = await assistant.start();
19 | assistant.on("messagesChanged", (newMessages) => {
20 | messages = newMessages;
21 | });
22 | assistant.sendMessage("Ahoy");
23 | ```
24 |
25 | ## License
26 |
27 | MIT
28 |
--------------------------------------------------------------------------------
/packages/assistants/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@twilio-alpha/assistants",
3 | "version": "0.0.1",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "module": "dist/index.mjs",
8 | "files": [
9 | "dist/"
10 | ],
11 | "scripts": {
12 | "clean": "rimraf dist",
13 | "build": "node ../../tools/build.js src/index.ts && tsc",
14 | "test": "vitest",
15 | "lib-pack": "npm pack"
16 | },
17 | "author": "Twilio Alpha ",
18 | "license": "MIT",
19 | "dependencies": {
20 | "events": "^3.3.0",
21 | "ts-pattern": "^5.0.8",
22 | "zod": "^3.22.4"
23 | },
24 | "devDependencies": {
25 | "@twilio/conversations": "^2.5.0",
26 | "@types/events": "^3.0.3"
27 | },
28 | "peerDependencies": {
29 | "@twilio/conversations": "^2.5.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/assistants/src/index.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from "events";
2 | import {
3 | ConnectionState,
4 | Conversation,
5 | Client as ConversationClient,
6 | ConversationUpdateReason,
7 | JSONObject,
8 | Message,
9 | MessageUpdateReason,
10 | } from "@twilio/conversations";
11 | import { match } from "ts-pattern";
12 | import z from "zod";
13 |
14 | export type AssistantOptions = {};
15 |
16 | export const UiToolData = z.object({
17 | name: z.string(),
18 | data: z.record(z.any()),
19 | });
20 |
21 | export type UiToolData = z.infer;
22 |
23 | export type JoinedConversationEventData = {
24 | conversationSid: string;
25 | identity: string;
26 | };
27 |
28 | export declare interface Assistant extends EventEmitter {
29 | destroy(): void;
30 | emit(event: "messagesChanged", data: Message[]): any;
31 | on(event: "messagesChanged", handler: (state: Message[]) => void): this;
32 | emit(event: "statusChanged", data: ConnectionState): any;
33 | on(event: "statusChanged", handler: (state: ConnectionState) => void): this;
34 | emit(event: "joinedConversation", data: JoinedConversationEventData): any;
35 | on(
36 | event: "joinedConversation",
37 | handler: (data: JoinedConversationEventData) => void
38 | ): this;
39 | emit(event: "assistantTypingStarted"): any;
40 | on(event: "assistantTypingStarted", handler: () => void): this;
41 | emit(event: "assistantTypingEnded"): any;
42 | on(event: "assistantTypingEnded", handler: () => void): this;
43 | emit(event: "uiToolTriggered", data: UiToolData): any;
44 | on(event: "uiToolTriggered", handler: (data: UiToolData) => void): this;
45 | on(event: string, handler: Function): this;
46 | }
47 |
48 | export class Assistant extends EventEmitter {
49 | private conversationsClient: ConversationClient;
50 | conversation?: Conversation;
51 | private options: AssistantOptions;
52 | private messages: Message[] = [];
53 |
54 | constructor(token: string, options: AssistantOptions = {}) {
55 | super();
56 | this.options = options;
57 | this.conversationsClient = new ConversationClient(token);
58 | this.conversationsClient.on(
59 | "connectionStateChanged",
60 | this.handleConnectionStateChanged.bind(this)
61 | );
62 | }
63 |
64 | private handleConnectionStateChanged(state: ConnectionState) {
65 | this.emit("statusChanged", state);
66 | return this;
67 | }
68 |
69 | destroy() {
70 | this.conversation?.removeAllListeners();
71 | this.conversationsClient.shutdown();
72 | }
73 |
74 | updateToken(token: string) {
75 | this.conversationsClient.updateToken(token);
76 | }
77 |
78 | async start(
79 | assistantSid: string,
80 | conversationSid?: string
81 | ): Promise {
82 | this.conversation = await match(conversationSid)
83 | .with(undefined, () =>
84 | this.conversationsClient.createConversation({
85 | attributes: {
86 | assistantSid: assistantSid,
87 | },
88 | friendlyName: `Assistant Conversation ${
89 | this.conversationsClient.user.identity
90 | } - ${new Date().toISOString()}`,
91 | })
92 | )
93 | .otherwise((conversationSid) =>
94 | this.conversationsClient.getConversationBySid(conversationSid)
95 | );
96 |
97 | try {
98 | await this.conversation.join();
99 | this.emit("joinedConversation", {
100 | conversationSid: this.conversation.sid,
101 | identity: this.conversationsClient.user.identity,
102 | });
103 | } catch (err) {
104 | if (this.conversation.state?.current === "active") {
105 | this.emit("joinedConversation", {
106 | conversationSid: this.conversation.sid,
107 | identity: this.conversationsClient.user.identity,
108 | });
109 | }
110 | }
111 | this.conversation.addListener(
112 | "messageAdded",
113 | this.handleMessageAdded.bind(this)
114 | );
115 | this.conversation.addListener(
116 | "messageUpdated",
117 | this.handleMessageUpdated.bind(this)
118 | );
119 | this.conversation.addListener(
120 | "messageRemoved",
121 | this.handleMessageRemoved.bind(this)
122 | );
123 | this.conversation.addListener(
124 | "updated",
125 | this.handleConversationUpdate.bind(this)
126 | );
127 | const messages = await this.conversation.getMessages();
128 | this.messages = messages.items;
129 | this.emit("messagesChanged", this.messages);
130 | return this.messages;
131 | }
132 |
133 | async sendMessage(message: string) {
134 | if (this.conversation) {
135 | await this.conversation.sendMessage(message);
136 | return true;
137 | }
138 | return false;
139 | }
140 |
141 | private handleConversationUpdate(data: {
142 | conversation: Conversation;
143 | updateReasons: ConversationUpdateReason[];
144 | }) {
145 | if (data.updateReasons.includes("attributes")) {
146 | if (
147 | typeof data.conversation.attributes === "object" &&
148 | data.conversation.attributes !== null &&
149 | !Array.isArray(data.conversation.attributes) &&
150 | typeof data.conversation.attributes.assistantIsTyping === "boolean"
151 | ) {
152 | if (data.conversation.attributes.assistantIsTyping) {
153 | this.emit("assistantTypingStarted");
154 | } else {
155 | this.emit("assistantTypingEnded");
156 | }
157 | }
158 | }
159 | }
160 |
161 | private handleMessageAdded(message: Message) {
162 | if (
163 | typeof message.attributes === "object" &&
164 | message.attributes !== null &&
165 | !Array.isArray(message.attributes) &&
166 | message.attributes.assistantMessageType === "ui-tool"
167 | ) {
168 | if (message.body) {
169 | try {
170 | const toolData = UiToolData.parse(JSON.parse(message.body));
171 | this.emit("uiToolTriggered", toolData);
172 | } catch (err) {
173 | console.error(err);
174 | }
175 | }
176 | return;
177 | }
178 | this.messages = [...this.messages, message];
179 | this.emit("messagesChanged", this.messages);
180 | }
181 |
182 | private handleMessageUpdated(data: {
183 | message: Message;
184 | updateReasons: MessageUpdateReason[];
185 | }) {
186 | this.messages = this.messages.map((msg) =>
187 | msg.sid === data.message.sid ? data.message : msg
188 | );
189 | this.emit("messagesChanged", this.messages);
190 | }
191 |
192 | private handleMessageRemoved(message: Message) {
193 | this.messages = this.messages.filter((msg) => msg.sid !== message.sid);
194 | }
195 | }
196 |
197 | export default Assistant;
198 |
--------------------------------------------------------------------------------
/packages/assistants/tests/index.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, expectTypeOf, test } from "vitest";
2 | import Assistants from "../src/index";
3 |
4 | // TODO: add more tests
5 |
6 | test("exports Assistants as a class", () => {
7 | expectTypeOf(Assistants).toBeConstructibleWith("", {
8 | assistantSid: "",
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/packages/assistants/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src"],
3 | "compilerOptions": {
4 | "module": "esnext",
5 | "target": "esnext",
6 | "lib": ["dom", "dom.iterable", "esnext"],
7 | "declaration": true,
8 | "strict": true,
9 | "moduleResolution": "node",
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "emitDeclarationOnly": true,
13 | "outDir": "dist",
14 | "rootDir": "src"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tools/build.js:
--------------------------------------------------------------------------------
1 | const esbuild = require("esbuild");
2 | const { nodeExternalsPlugin } = require("esbuild-node-externals");
3 | const { argv, cwd } = require("node:process");
4 | const { join } = require("node:path");
5 |
6 | let [_nodeFilePath, _program, ...entryPoints] = argv;
7 |
8 | let generateUmd = false;
9 | if (entryPoints.includes("--umd")) {
10 | generateUmd = true;
11 | entryPoints = entryPoints
12 | .map((x) => (x === "--umd" ? undefined : x))
13 | .filter((x) => !!x);
14 | }
15 |
16 | function buildOne(mode, fileName, overrideOptions = {}) {
17 | return esbuild.build({
18 | color: true,
19 | entryPoints: entryPoints,
20 | outfile: join(cwd(), "dist", fileName),
21 | bundle: true,
22 | minifyIdentifiers: false,
23 | minifySyntax: true,
24 | minifyWhitespace: true,
25 | treeShaking: true,
26 | platform: "browser",
27 | format: mode,
28 | /**
29 | * From docs:
30 | * The main fields setting is set to main,module. This means tree shaking
31 | * will likely not happen for packages that provide both module and main
32 | * since tree shaking works with ECMAScript modules but not with CommonJS
33 | * modules.
34 | * Unfortunately some packages incorrectly treat module as meaning
35 | * "browser code" instead of "ECMAScript module code" so this default
36 | * behavior is required for compatibility. You can manually configure the
37 | * main fields setting to module,main if you want to enable tree shaking
38 | * and know it is safe to do so.
39 | */
40 | mainFields: ["module", "main", "browser"],
41 | target: ["chrome100", "firefox100", "safari14", "edge100", "node18.16.0"],
42 | external: ["react", "react-dom", "@twilio/conversations"],
43 | ...overrideOptions,
44 | });
45 | }
46 |
47 | async function build() {
48 | await buildOne("esm", "index.mjs");
49 | await buildOne("cjs", "index.js");
50 | if (generateUmd) {
51 | await buildOne("iife", "ai-assistants.bundled.js", {
52 | external: undefined,
53 | globalName: "Twilio.Alpha.AiAssistants",
54 | target: ["chrome100", "firefox100", "safari14", "edge100"],
55 | });
56 | }
57 | }
58 |
59 | build().catch(console.error);
60 |
--------------------------------------------------------------------------------
/vitest.workspace.ts:
--------------------------------------------------------------------------------
1 | export default ["packages/*"];
2 |
--------------------------------------------------------------------------------