├── .gitignore ├── README.md ├── api ├── .env.sample ├── Dockerfile ├── justfile ├── requirements.txt └── src │ ├── __init__.py │ ├── config.py │ ├── db_pg.py │ ├── domain.py │ ├── main.py │ └── utils.py ├── docker-compose.yml ├── frontend ├── Dockerfile ├── justfile ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── ChatDetails.js │ ├── ChatDetailsHeader.js │ ├── ChatList.js │ ├── MessageInput.js │ ├── RasaAdminAPI.js │ ├── config.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js └── yarn.lock └── screenshot.png /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | frontend/build 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rasa Admin web-app (humble Rasa-X replacement) 2 | 3 | A simple Rasa backoffice web-app for tracking your Rasa bot 4 | conversations with users.. And more. 5 | 6 | ![screenshot](https://raw.githubusercontent.com/nesterapp/rasa-admin/main/screenshot.png) 7 | 8 | ### Features 9 | - Watch Bot<->users conversations 10 | - Send a message to user ( as Bot ) 11 | 12 | ### Built with 13 | - Frontend: React JS 14 | - API Backend: FastAPI, Pydantic, asyncpg 15 | 16 | ## Prerequisites 17 | Your Rasa should have an active PostgreSQL tracker store, 18 | or, if you wish to install one, see this [guide](https://rasa.com/docs/rasa/tracker-stores/). 19 | Use the same DB connection details for the next step. 20 | 21 | > Tested only with PostgreSQL. 22 | 23 | ## Installation steps summary 24 | 1. Install and run the backend API service (python). 25 | 2. Install and run the ReactJS app. 26 | > All calls to tracker store database and to your Rasa server are handled by 27 | the backend api. 28 | 29 | ## 1. Install and run a local backend API service 30 | 31 | ### Install API 32 | ```sh 33 | cd api 34 | pyenv virtualenv 3.11.3 rasa-admin-api 35 | pyenv activate rasa-admin-api 36 | pyenv local rasa-admin-api 37 | pip install -e . 38 | ``` 39 | 40 | ### Configure 41 | Copy .env.sample to .env, 42 | then edit to provide PostgreSQL and RASA server details. 43 | ```sh 44 | cd api 45 | cp .env.sample .env 46 | vim .env 47 | ``` 48 | 49 | ### Run API 50 | If you have [just](https://just.systems/) tool installed: 51 | 52 | ```sh 53 | just run 54 | ``` 55 | 56 | Or, if not: 57 | 58 | ```sh 59 | uvicorn src.main:app --reload --port 5000 60 | ``` 61 | 62 | ## 2. Install and run local ReactJS app 63 | ```sh 64 | cd frontend 65 | yarn install 66 | yarn start 67 | ``` 68 | 69 | Open your browser http://localhost:3000 70 | 71 | ### Upcoming 72 | - Human hand-off — ability to pause and resume bot-user conversation during hand-off. 73 | - Auto refresh 74 | 75 | Feel free to suggest features or submit PRs! 76 | 77 | ## License 78 | MIT License 79 | 80 | Nester (c) 2024 81 | -------------------------------------------------------------------------------- /api/.env.sample: -------------------------------------------------------------------------------- 1 | # Hostname or ip of your Rasa PostgreSQL server 2 | POSTGRES_HOST = 'localhost' 3 | # TCP Port of your Rasa PostgresSQL server 4 | POSTGRES_PORT = '5432' 5 | # Rasa database name 6 | POSTGRES_DB_NAME = 'rasa' 7 | # Rasa database user and password 8 | POSTGRES_USER = 'postgres' 9 | POSTGRES_PASSWORD = 'postgres' 10 | 11 | # --- Optionals (for sending messages to users) --- 12 | # Rasa API server URL 13 | RASA_SERVER_API_URL = "http://localhost:5005" 14 | # Rasa API JWT secret key (see https://rasa.com/docs/rasa/next/http-api/) 15 | RASA_SERVER_API_JWT_SECRET_KEY = "" 16 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11.3-slim 2 | 3 | WORKDIR /code/api 4 | 5 | COPY ./requirements.txt ./ 6 | 7 | RUN pip install --no-cache-dir --upgrade -r requirements.txt 8 | 9 | COPY ./src src 10 | COPY .env .env 11 | 12 | CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "5000"] 13 | -------------------------------------------------------------------------------- /api/justfile: -------------------------------------------------------------------------------- 1 | # API justfile 2 | 3 | PACKAGE := 'src.main' 4 | 5 | [private] 6 | default: 7 | @just --list --unsorted 8 | 9 | run: 10 | uvicorn {{ PACKAGE }}:app --reload --port 5000 11 | 12 | install: venv 13 | pip install -e . 14 | 15 | install-dev: venv 16 | pip install -e .[test] 17 | 18 | venv: 19 | -pyenv virtualenv 3.11.3 rasa-admin-api 20 | pyenv local rasa-admin-api 21 | -pyenv activate rasa-admin-api 22 | -------------------------------------------------------------------------------- /api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.97.0 2 | httpx==0.24.1 3 | pydantic==1.10.8 4 | python-dotenv==1.0.0 5 | uvicorn==0.22.0 6 | asyncpg==0.27.0 7 | requests==2.31.0 8 | PyJWT==2.8.0 9 | -------------------------------------------------------------------------------- /api/src/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | rasa-admin-api namespace module 3 | """ 4 | # -*- coding: utf-8 -*- 5 | 6 | __doc__ = "rasa-admin-api" 7 | __license__ = "Copyright" 8 | __version__ = "0.1.0" 9 | __author__ = "" 10 | __email__ = "" 11 | __url__ = "http://github.com/nesterapp/rasa-admin" 12 | -------------------------------------------------------------------------------- /api/src/config.py: -------------------------------------------------------------------------------- 1 | # rasa-admin api config 2 | # --- 3 | import pathlib 4 | 5 | project_root = pathlib.Path(__file__).parent.parent.resolve() 6 | 7 | UVICORN_LOGGING_LEVEL = 'DEBUG' # 'INFO' / 'WARNING' / 'ERROR' / 'DEBUG' 8 | -------------------------------------------------------------------------------- /api/src/db_pg.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | from asyncpg import Pool 3 | from dotenv import load_dotenv 4 | import os 5 | 6 | class PostgresDB: 7 | def __init__(self): 8 | self.pool: Pool = None 9 | self.load_env() 10 | self.dsn = self.build_dsn() 11 | 12 | def load_env(self): 13 | # dotenv_path = ".env" 14 | load_dotenv() 15 | 16 | def build_dsn(self): 17 | db_host = os.getenv("POSTGRES_HOST") 18 | db_port = os.getenv("POSTGRES_PORT") 19 | db_name = os.getenv("POSTGRES_DB_NAME") 20 | db_user = os.getenv("POSTGRES_USER") 21 | db_password = os.getenv("POSTGRES_PASSWORD") 22 | return f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" 23 | 24 | async def connect(self): 25 | self.pool = await asyncpg.create_pool(dsn=self.dsn) 26 | 27 | async def disconnect(self): 28 | await self.pool.close() 29 | 30 | async def execute(self, query: str, *args): 31 | connection: asyncpg.Connection 32 | async with self.pool.acquire() as connection: 33 | await connection.execute(query, *args) 34 | 35 | async def execute_transaction(self, queries): 36 | connection: asyncpg.Connection 37 | async with self.pool.acquire() as connection: 38 | async with connection.transaction(): 39 | for query, *args in queries: 40 | await connection.execute(query, *args) 41 | 42 | async def fetchrow(self, query: str, *args) -> asyncpg.Record: 43 | connection: asyncpg.Connection 44 | async with self.pool.acquire() as connection: 45 | result = await connection.fetchrow(query, *args) 46 | return result 47 | 48 | async def fetch(self, query: str, *args) -> list[asyncpg.Record]: 49 | connection: asyncpg.Connection 50 | async with self.pool.acquire() as connection: 51 | result = await connection.fetch(query, *args) 52 | return result 53 | -------------------------------------------------------------------------------- /api/src/domain.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional 3 | 4 | from pydantic import BaseModel, validator 5 | 6 | class EventData(BaseModel): 7 | event: str 8 | text: Optional[str] 9 | 10 | class Event(BaseModel): 11 | id: int 12 | sender_id: str 13 | type_name: str 14 | timestamp: float 15 | intent_name: Optional[str] 16 | action_name: Optional[str] 17 | data: EventData 18 | 19 | @validator('data', pre=True) 20 | def data_str_to_dict(cls, v): 21 | if isinstance(v, dict): 22 | return v 23 | elif isinstance(v, str): 24 | try: 25 | d = json.loads(v) 26 | except: 27 | raise ValueError('Failed to convert data text to dictionary') 28 | return d 29 | return v 30 | 31 | class ChatHeader(BaseModel): 32 | sender_id: str 33 | timestamp: float 34 | 35 | class Chat(BaseModel): 36 | sender_id: str 37 | events: list[Event] 38 | 39 | class MessagePayload(BaseModel): 40 | text: str -------------------------------------------------------------------------------- /api/src/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import os 4 | 5 | from contextlib import asynccontextmanager 6 | from fastapi import FastAPI, Depends, HTTPException 7 | from fastapi.middleware.cors import CORSMiddleware 8 | from dotenv import load_dotenv 9 | 10 | from src import config 11 | from src.db_pg import PostgresDB 12 | from src.domain import Chat, ChatHeader, Event, MessagePayload 13 | from src.utils import encode_jwt_token 14 | 15 | logger = logging.getLogger('uvicorn') 16 | logger.setLevel(config.UVICORN_LOGGING_LEVEL) 17 | load_dotenv() 18 | 19 | @asynccontextmanager 20 | async def lifespan(app: FastAPI): 21 | db = PostgresDB() 22 | await db.connect() 23 | app.state.database = db 24 | 25 | yield 26 | # Anything after yield is called at shutdown 27 | await app.state.database.disconnect() 28 | 29 | 30 | app = FastAPI(lifespan=lifespan) 31 | 32 | origins = [ 33 | "http://localhost:8000", 34 | "http://localhost:8080", 35 | "http://localhost:3000", 36 | "http://127.0.0.1:3000", 37 | ] 38 | 39 | app.add_middleware( 40 | CORSMiddleware, 41 | allow_origins=origins, 42 | allow_credentials=True, 43 | allow_methods=["*"], 44 | allow_headers=["*"], 45 | ) 46 | 47 | def get_database() -> PostgresDB: 48 | return app.state.database 49 | 50 | 51 | @app.get("/_status/healthz") 52 | async def root(): 53 | return {"message": "OK"} 54 | 55 | 56 | @app.get("/chats", response_model=list[ChatHeader]) 57 | async def get_chats(db: PostgresDB = Depends(get_database)): 58 | """Get all chat headers, not including messages (only sender_id and timestamp)""" 59 | 60 | rows = await db.fetch( 61 | """ 62 | SELECT sender_id, 63 | MIN("timestamp") AS timestamp 64 | FROM events 65 | GROUP BY sender_id 66 | ORDER BY timestamp DESC; 67 | """ 68 | ) 69 | chats = [ChatHeader(**row) for row in rows] 70 | return chats 71 | 72 | 73 | @app.get("/chats/{sender_id}", response_model=Chat) 74 | async def get_chat_details(sender_id: str, db: PostgresDB = Depends(get_database)) -> Chat: 75 | """Get specific chat with all of it's related events 76 | 77 | Args: 78 | sender_id 79 | """ 80 | rows = await db.fetch( 81 | """ 82 | SELECT 83 | id, sender_id, type_name, 84 | timestamp, intent_name, 85 | action_name, (data::jsonb) AS data 86 | FROM events 87 | WHERE sender_id = $1 88 | ORDER BY id desc 89 | """ 90 | , 91 | sender_id 92 | ) 93 | if not rows: 94 | raise HTTPException(status_code=404, detail="Conversation not found") 95 | 96 | events = [Event(**row) for row in rows] 97 | chat = Chat(sender_id=sender_id, events=events) 98 | return chat 99 | 100 | 101 | @app.post("/chats/{sender_id}/message") 102 | async def send_message( 103 | sender_id: str, 104 | message: MessagePayload 105 | ): 106 | """Send a text message from Rasa Bot to a user via channel 107 | 108 | Args: 109 | sender_id: the recipient id for the message 110 | message: the text message to send. i.e payload { "text": "foobar" } 111 | 112 | see: https://rasa.com/docs/rasa/pages/http-api#operation/addConversationTrackerEvents 113 | """ 114 | 115 | rasa_url = os.getenv("RASA_SERVER_API_URL") 116 | jwt_secret = os.getenv("RASA_SERVER_API_JWT_SECRET_KEY") 117 | assert rasa_url, jwt_secret 118 | 119 | url = f"{rasa_url}/conversations/{sender_id}/tracker/events" 120 | 121 | query_params = { 122 | "output_channel": "telegram", # TODO: why failing to send with "latest" 123 | "execute_side_effects": "true" 124 | } 125 | 126 | payload = { 127 | "event": "bot", 128 | "text": message.text 129 | } 130 | 131 | if not jwt_secret: 132 | raise HTTPException(status_code=400, detail="JWT secret is missing") 133 | jwt_token = encode_jwt_token(jwt_secret) 134 | headers = { 135 | "Authorization": f"Bearer {jwt_token}", 136 | "Content-Type": "application/json" 137 | } 138 | 139 | response = requests.post(url, params=query_params, json=payload, headers=headers) 140 | 141 | if response.status_code == 200: 142 | return {"message": "Message sent successfully"} 143 | else: 144 | return {"message": "Failed to send message"} 145 | -------------------------------------------------------------------------------- /api/src/utils.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | import os 3 | 4 | def encode_jwt_token(jwt_secret_key: str) -> str: 5 | 6 | payload = { 7 | "user": { 8 | "role": "admin" 9 | } 10 | } 11 | 12 | return jwt.encode(payload, jwt_secret_key, algorithm="HS256") 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.3" 3 | 4 | services: 5 | api: 6 | image: rasa-admin/api:latest 7 | build: 8 | context: ./api 9 | ports: 10 | - 5000:5000 11 | networks: 12 | - frontend 13 | - backend 14 | frontend: 15 | image: rasa-admin/frontend:latest 16 | build: 17 | context: ./frontend 18 | ports: 19 | - 3000:3000 20 | networks: 21 | - frontend 22 | 23 | networks: 24 | frontend: 25 | backend: 26 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Client base image 2 | FROM node:21.7.0-alpine as base 3 | WORKDIR /app 4 | COPY package.json yarn.lock ./ 5 | 6 | ENV NODE_ENV production 7 | 8 | RUN yarn install --frozen-lockfile --production && \ 9 | yarn cache clean 10 | RUN yarn global add serve 11 | 12 | COPY public ./public 13 | COPY src ./src 14 | RUN yarn build 15 | 16 | CMD [ "serve", "-s", "./build" ] 17 | -------------------------------------------------------------------------------- /frontend/justfile: -------------------------------------------------------------------------------- 1 | [private] 2 | default: 3 | @just --list --unsorted 4 | 5 | dev: 6 | yarn start 7 | 8 | build: 9 | yarn build 10 | 11 | test: 12 | yarn test -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rasa-admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.17.0", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "axios": "^1.5.0", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "react-scripts": "5.0.1", 13 | "web-vitals": "^2.1.4" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesterapp/rasa-admin/e21f9d983b26887b1e332acdcf6e38b9915c8704/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesterapp/rasa-admin/e21f9d983b26887b1e332acdcf6e38b9915c8704/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesterapp/rasa-admin/e21f9d983b26887b1e332acdcf6e38b9915c8704/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | /* ChatApp.css */ 2 | .chat-app { 3 | display: flex; 4 | height: 100vh; 5 | overflow: hidden; 6 | } 7 | 8 | .chat-list { 9 | width: 17%; 10 | border-right: 1px solid #ccc; 11 | padding: 20px; 12 | box-sizing: border-box; 13 | background-color: #f5f5f5; 14 | overflow-y: auto; 15 | } 16 | 17 | .chat-list h2 { 18 | font-size: 1.2em; 19 | margin-bottom: 15px; 20 | } 21 | 22 | .chat-list ul { 23 | list-style: none; 24 | padding: 0; 25 | margin: 0; 26 | } 27 | 28 | .chat-list li { 29 | padding: 10px; 30 | cursor: pointer; 31 | transition: background-color 0.3s ease; 32 | border-radius: 5px; 33 | } 34 | 35 | .chat-list li:hover { 36 | background-color: #ddd; 37 | } 38 | 39 | /* Add more space between list items */ 40 | .chat-list li + li { 41 | margin-top: 5px; 42 | } 43 | 44 | /* Style selected item */ 45 | .chat-list li.selected { 46 | background-color: #007bff; 47 | color: #fff; 48 | } 49 | 50 | .middle-section { 51 | display: flex; 52 | flex-direction: column; 53 | flex: 1; 54 | overflow-y: auto; 55 | } 56 | 57 | .chat-details { 58 | flex: 1; 59 | padding: 20px; 60 | box-sizing: border-box; 61 | overflow-y: auto; 62 | position: relative; /* Add relative positioning to the container */ 63 | } 64 | 65 | .chat-details-header { 66 | position: sticky; 67 | top: 0; 68 | background-color: #fff; 69 | z-index: 1; 70 | padding: 10px 20px; /* Add padding for the header */ 71 | display: flex; 72 | flex-direction: column; /* Set flex-direction to column */ 73 | justify-content: center; /* Center items horizontally */ 74 | align-items: flex-start; /* Center items vertically */ 75 | border-bottom: 1px solid #ccc; /* Add a border at the bottom */ 76 | } 77 | 78 | .messages { 79 | display: flex; 80 | flex-direction: column; 81 | } 82 | 83 | .timestamp { 84 | font-size: 12px; 85 | margin-bottom: 0px; 86 | } 87 | 88 | .timestamp.bot { 89 | color: #808080; 90 | } 91 | 92 | .timestamp.user { 93 | color: #d8d8d8; 94 | } 95 | 96 | .message { 97 | display: flex; 98 | flex-direction: column; 99 | margin: 10px 0; 100 | padding: 10px; 101 | border-radius: 20px; 102 | } 103 | 104 | .message-body { 105 | font-size: 14px; 106 | padding: 0px; 107 | max-width: 100%; 108 | } 109 | 110 | .message-body.bot { 111 | color: #060505; 112 | } 113 | 114 | .message-body.user { 115 | color: #fff; 116 | } 117 | 118 | .message.bot { 119 | align-self: flex-start; 120 | background-color: #eaecf4; /* Bot bubble color */ 121 | } 122 | 123 | .message.user { 124 | align-self: flex-end; 125 | background-color: #007bff; /* User bubble color */ 126 | } 127 | 128 | /* Message Input box */ 129 | .message-input { 130 | background-color: #fff; 131 | border-top: 1px solid #ccc; 132 | border-bottom: 1px solid #ccc; 133 | padding: 20px; 134 | flex-shrink: 0; 135 | display: flex; 136 | align-items: center; /* Align items vertically centered */ 137 | } 138 | 139 | .message-input input { 140 | flex-grow: 1; /* Take available horizontal space */ 141 | padding: 10px; 142 | border: 1px solid #ccc; 143 | border-radius: 5px; 144 | margin-right: 10px; 145 | } 146 | 147 | .message-input button { 148 | width: 70px; 149 | padding: 10px 0; 150 | background-color: #007bff; 151 | color: #fff; 152 | border: none; 153 | border-radius: 5px; 154 | cursor: pointer; 155 | flex-shrink: 0; 156 | } 157 | 158 | .right-panel { 159 | width: 20%; 160 | border-left: 1px solid #ccc; 161 | padding: 20px; 162 | box-sizing: border-box; 163 | background-color: #f5f5f5; 164 | overflow-y: auto; 165 | } 166 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ChatList from './ChatList'; 3 | import ChatDetails from './ChatDetails'; 4 | import ChatDetailsHeader from './ChatDetailsHeader'; 5 | import MessageInput from './MessageInput'; 6 | import RasaAdminAPI from './RasaAdminAPI'; 7 | import './App.css'; 8 | 9 | 10 | function App() { 11 | const [selectedChat, setSelectedChat] = useState(undefined); 12 | const [selectedChatDetails, setSelectedChatDetails] = useState(undefined); 13 | const [chats, setChats ] = useState([]); 14 | 15 | const handleChatClick = (selectedChat) => { 16 | setSelectedChat(selectedChat); 17 | setSelectedChatDetails(undefined); 18 | 19 | RasaAdminAPI.getChat(selectedChat.sender_id).then(data => { 20 | setSelectedChatDetails(data); 21 | }); 22 | }; 23 | 24 | const handleSendMessage = (text) => { 25 | console.log(`send message: ${text} to ${selectedChat.sender_id}`); 26 | RasaAdminAPI.sendMessage(selectedChat.sender_id, text); 27 | }; 28 | 29 | useEffect(() => { 30 | // Fetch chat collection 31 | RasaAdminAPI.getChats().then(data => setChats(data)); 32 | }, []); 33 | 34 | return ( 35 |
36 | 37 | {/* Render ConversationList component */} 38 | 42 | 43 |
44 | 45 | {/* Render ChatDetails component */} 46 | 47 | {selectedChatDetails && } 48 |
49 | 50 |
51 | 52 |
53 | ); 54 | } 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /frontend/src/ChatDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ALLOWED_TYPES = ['user', 'bot']; // action, slot, user_featurization 4 | 5 | function formatTimestamp(timestamp) { 6 | const date = new Date(timestamp * 1000); 7 | return date.toLocaleString(); 8 | } 9 | 10 | function ChatDetails({ chat }) { 11 | 12 | const filter_events = (chat) => { 13 | const filteredEvents = chat.events.filter(event => ALLOWED_TYPES.includes(event.type_name)); 14 | return filteredEvents.slice().reverse() 15 | } 16 | 17 | return ( 18 |
19 | {chat ? ( 20 |
21 | {filter_events(chat).map((event, index) => ( 22 |
23 |

24 | {formatTimestamp(event.timestamp)} 25 |

26 |

27 | {event.data.text} 28 |

29 |
30 | ))} 31 |
32 | ) : ( 33 |

Select a chat

34 | )} 35 |
36 | ); 37 | } 38 | 39 | export default ChatDetails; 40 | -------------------------------------------------------------------------------- /frontend/src/ChatDetailsHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function ChatDetailsHeader({ chatHeader }) { 4 | const senderId = chatHeader?.sender_id 5 | return ( 6 |
7 |

Chat

8 |

{senderId ? `Sender ID: ${senderId}` : 'Select a chat'}

9 |
10 | ); 11 | } 12 | 13 | export default ChatDetailsHeader; 14 | -------------------------------------------------------------------------------- /frontend/src/ChatList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function ChatList({ chats, selectedChat, onSelectChat }) { 4 | 5 | return ( 6 |
7 |

Chats

8 | 19 |
20 | );} 21 | 22 | export default ChatList; 23 | -------------------------------------------------------------------------------- /frontend/src/MessageInput.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function MessageInput({ onSendMessage }) { 4 | const [message, setMessage] = useState(''); 5 | 6 | const handleInputChange = (event) => { 7 | setMessage(event.target.value); 8 | }; 9 | 10 | const handleSendClick = () => { 11 | if (message.trim() !== '') { 12 | onSendMessage(message); 13 | setMessage(''); 14 | } 15 | }; 16 | 17 | return ( 18 |
19 | 25 | 26 |
27 | ); 28 | } 29 | 30 | export default MessageInput; 31 | -------------------------------------------------------------------------------- /frontend/src/RasaAdminAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { API_URL } from './config' 3 | 4 | class RasaAdminAPI { 5 | 6 | static getChats = async () => { 7 | let response = await axios.get(`${API_URL}/chats`) 8 | const { data } = response; 9 | return data 10 | } 11 | 12 | static getChat = async (sender_id) => { 13 | let response = await axios.get(`${API_URL}/chats/${sender_id}`) 14 | const { data } = response; 15 | return data 16 | } 17 | 18 | static sendMessage = async (sender_id, text) => { 19 | await axios.post(`${API_URL}/chats/${sender_id}/message`, 20 | { text }, 21 | { 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | } 26 | ); 27 | }; 28 | } 29 | 30 | export default RasaAdminAPI -------------------------------------------------------------------------------- /frontend/src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * rasa-admin frontend config 3 | * @module config 4 | */ 5 | const NODE_ENV = process.env.NODE_ENV || 'development'; 6 | const IS_PROD = NODE_ENV === 'production'; 7 | 8 | const 9 | API_URL = IS_PROD ? 'http://host.docker.internal:5000' : 'http://localhost:5000' 10 | 11 | export { 12 | API_URL 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nesterapp/rasa-admin/e21f9d983b26887b1e332acdcf6e38b9915c8704/screenshot.png --------------------------------------------------------------------------------