Finalização da Implementaçao do dialogflow

pull/20/head
adriano 2022-11-08 17:23:13 -03:00
parent 9bf1850405
commit 1bdd4adaba
54 changed files with 2076 additions and 421 deletions

View File

@ -15,6 +15,9 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@google-cloud/dialogflow": "^4.6.0",
"actions-on-google": "^3.0.0",
"axios": "^0.27.2",
"@sentry/node": "^5.29.2", "@sentry/node": "^5.29.2",
"@types/pino": "^6.3.4", "@types/pino": "^6.3.4",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View File

@ -13,10 +13,12 @@ import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl"; import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
import AppError from "../errors/AppError"; import AppError from "../errors/AppError";
import { searchContactCache } from '../helpers/ContactsCache' import { searchContactCache } from '../helpers/ContactsCache'
import { off } from "process"; import { off } from "process";
import GetContactService from "../services/ContactServices/GetContactService";
import ToggleUseQueuesContactService from "../services/ContactServices/ToggleUseQueuesContactService";
import ToggleUseDialogflowContactService from "../services/ContactServices/ToggleUseDialogflowContactService";
type IndexQuery = { type IndexQuery = {
@ -32,9 +34,15 @@ interface ContactData {
name: string; name: string;
number: string; number: string;
email?: string; email?: string;
useDialogflow: boolean;
extraInfo?: ExtraInfo[]; extraInfo?: ExtraInfo[];
} }
type IndexGetContactQuery = {
name: string;
number: string;
};
export const index = async (req: Request, res: Response): Promise<Response> => { export const index = async (req: Request, res: Response): Promise<Response> => {
let { searchParam, pageNumber } = req.query as IndexQuery; let { searchParam, pageNumber } = req.query as IndexQuery;
@ -68,6 +76,17 @@ export const index = async (req: Request, res: Response): Promise<Response> => {
return res.json({ contacts, count, hasMore }); return res.json({ contacts, count, hasMore });
}; };
export const getContact = async (req: Request, res: Response): Promise<Response> => {
const { name, number } = req.body as IndexGetContactQuery;
const contact = await GetContactService({
name,
number
});
return res.status(200).json(contact);
};
export const store = async (req: Request, res: Response): Promise<Response> => { export const store = async (req: Request, res: Response): Promise<Response> => {
const newContact: ContactData = req.body; const newContact: ContactData = req.body;
newContact.number = newContact.number.replace("-", "").replace(" ", ""); newContact.number = newContact.number.replace("-", "").replace(" ", "");
@ -94,11 +113,13 @@ export const store = async (req: Request, res: Response): Promise<Response> => {
let number = validNumber let number = validNumber
let email = newContact.email let email = newContact.email
let extraInfo = newContact.extraInfo let extraInfo = newContact.extraInfo
let useDialogflow = newContact.useDialogflow
const contact = await CreateContactService({ const contact = await CreateContactService({
name, name,
number, number,
email, email,
useDialogflow,
extraInfo, extraInfo,
profilePicUrl profilePicUrl
}); });
@ -173,6 +194,40 @@ export const remove = async (
}; };
export const toggleUseQueue = async (
req: Request,
res: Response
): Promise<Response> => {
const { contactId } = req.params;
const contact = await ToggleUseQueuesContactService({ contactId });
const io = getIO();
io.emit("contact", {
action: "update",
contact
});
return res.status(200).json(contact);
};
export const toggleUseDialogflow = async (
req: Request,
res: Response
): Promise<Response> => {
const { contactId } = req.params;
const contact = await ToggleUseDialogflowContactService({ contactId });
const io = getIO();
io.emit("contact", {
action: "update",
contact
});
return res.status(200).json(contact);
};
export const contacsBulkInsertOnQueue = async (req: Request, res: Response): Promise<Response> => { export const contacsBulkInsertOnQueue = async (req: Request, res: Response): Promise<Response> => {

View File

@ -0,0 +1,85 @@
import { Request, Response } from "express";
import { getIO } from "../libs/socket";
import CreateDialogflowService from "../services/DialogflowServices/CreateDialogflowService";
import DeleteDialogflowService from "../services/DialogflowServices/DeleteDialogflowService";
import ListDialogflowsService from "../services/DialogflowServices/ListDialogflowService";
import ShowDialogflowService from "../services/DialogflowServices/ShowDialogflowService";
import TestSessionDialogflowService from "../services/DialogflowServices/TestSessionDialogflowService";
import UpdateDialogflowService from "../services/DialogflowServices/UpdateDialogflowService";
export const index = async (req: Request, res: Response): Promise<Response> => {
const dialogflows = await ListDialogflowsService();
return res.status(200).json(dialogflows);
};
export const store = async (req: Request, res: Response): Promise<Response> => {
const { name, projectName, jsonContent, language } = req.body;
const dialogflow = await CreateDialogflowService({ name, projectName, jsonContent, language });
const io = getIO();
io.emit("dialogflow", {
action: "update",
dialogflow
});
return res.status(200).json(dialogflow);
};
export const show = async (req: Request, res: Response): Promise<Response> => {
const { dialogflowId } = req.params;
const dialogflow = await ShowDialogflowService(dialogflowId);
return res.status(200).json(dialogflow);
};
export const update = async (
req: Request,
res: Response
): Promise<Response> => {
const { dialogflowId } = req.params;
const dialogflowData = req.body;
const dialogflow = await UpdateDialogflowService({dialogflowData, dialogflowId });
const io = getIO();
io.emit("dialogflow", {
action: "update",
dialogflow
});
return res.status(201).json(dialogflow);
};
export const remove = async (
req: Request,
res: Response
): Promise<Response> => {
const { dialogflowId } = req.params;
await DeleteDialogflowService(dialogflowId);
const io = getIO();
io.emit("dialogflow", {
action: "delete",
dialogflowId: +dialogflowId
});
return res.status(200).send();
};
export const testSession = async (req: Request, res: Response): Promise<Response> => {
const { projectName, jsonContent, language } = req.body;
const response = await TestSessionDialogflowService({ projectName, jsonContent, language });
const io = getIO();
io.emit("dialogflow", {
action: "testSession",
response
});
return res.status(200).json(response);
};

View File

@ -14,6 +14,8 @@ import QuickAnswer from "../models/QuickAnswer";
import SchedulingNotify from "../models/SchedulingNotify"; import SchedulingNotify from "../models/SchedulingNotify";
import StatusChatEnd from "../models/StatusChatEnd"; import StatusChatEnd from "../models/StatusChatEnd";
import UserOnlineTime from "../models/UserOnlineTime"; import UserOnlineTime from "../models/UserOnlineTime";
import Dialogflow from "../models/Dialogflow";
// eslint-disable-next-line // eslint-disable-next-line
const dbConfig = require("../config/database"); const dbConfig = require("../config/database");
// import dbConfig from "../config/database"; // import dbConfig from "../config/database";
@ -36,6 +38,7 @@ const models = [
SchedulingNotify, SchedulingNotify,
StatusChatEnd, StatusChatEnd,
UserOnlineTime, UserOnlineTime,
Dialogflow,
]; ];
sequelize.addModels(models); sequelize.addModels(models);

View File

@ -0,0 +1,44 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.createTable("Dialogflows", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
projectName: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
jsonContent: {
type: DataTypes.TEXT,
allowNull: false,
},
language: {
type: DataTypes.STRING,
allowNull: false,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.dropTable("Dialogflows");
}
};

View File

@ -0,0 +1,16 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Queues", "dialogflowId", {
type: DataTypes.INTEGER,
references: { model: "Dialogflows", key: "id" },
onUpdate: "CASCADE",
onDelete: "SET NULL"
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Queues", "dialogflowId");
}
};

View File

@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Contacts", "useDialogflow", {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Contacts", "useDialogflow");
}
};

View File

@ -0,0 +1,15 @@
import { QueryInterface, DataTypes } from "sequelize";
module.exports = {
up: (queryInterface: QueryInterface) => {
return queryInterface.addColumn("Contacts", "useQueues", {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
});
},
down: (queryInterface: QueryInterface) => {
return queryInterface.removeColumn("Contacts", "useQueues");
}
};

View File

@ -1,16 +1,44 @@
const fsPromises = require("fs/promises"); const fsPromises = require("fs/promises");
const fs = require('fs') import dir from 'path';
import fs from 'fs';
import os from 'os';
import ListUsersService from "../services/UserServices/ListUsersService" import ListUsersService from "../services/UserServices/ListUsersService"
const _botIsOnQueue = async (botName: string) => { const _botIsOnQueue = async (botName: string) => {
const { users, count, hasMore } = await ListUsersService({searchParam:`${botName}`,pageNumber:1}); const botInfoFile = dir.join(os.tmpdir(), `botInfo.json`);
console.log('The bot botInfoFile: ', botInfoFile)
try {
if (fs.existsSync(botInfoFile)) {
console.log('botInfo.json file exists');
const botInfo = fs.readFileSync(botInfoFile, {encoding:'utf8', flag:'r'});
return JSON.parse(botInfo)
} else {
console.log('botInfo.json file not found!');
}
} catch (error) {
console.log('There was an error on try to read the botInfo.json file: ',error)
}
console.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ')
const { users, count, hasMore } = await ListUsersService({ searchParam: `${botName}`, pageNumber: 1 });
let botIsOnQueue = false let botIsOnQueue = false
let userIdBot = null let userIdBot = null
let queueId = null let queueId = null
if(users.length > 0){ if (users.length > 0) {
try { try {
@ -19,19 +47,24 @@ const _botIsOnQueue = async (botName: string) => {
userIdBot = Object(users)[0].id userIdBot = Object(users)[0].id
botIsOnQueue = true botIsOnQueue = true
}catch(err){ fs.writeFileSync(botInfoFile, JSON.stringify({ userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue }), "utf8");
console.log('O usuário botqueue não está em nenhuma fila err: ',err) } catch (err) {
console.log('O usuário botqueue não está em nenhuma fila err: ', err)
} }
} }
else{ else {
console.log('Usuário botqueue não existe!') console.log('Usuário botqueue não existe!')
fs.writeFileSync(botInfoFile, JSON.stringify({ isOnQueue: false, botQueueId: 0, userIdBot: 0 }), "utf8");
} }
return { userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue } return { userIdBot: userIdBot, botQueueId: queueId, isOnQueue: botIsOnQueue }
} }
export default _botIsOnQueue; export default _botIsOnQueue;

View File

@ -0,0 +1,33 @@
const fsPromises = require("fs/promises");
const fs = require('fs')
import axios from 'axios';
import * as https from "https";
const endPointQuery = async (url: string) => {
let response:any = null
try {
const httpsAgent = new https.Agent({ rejectUnauthorized: false, });
// const url = 'https://sos.espacolaser.com.br/api/whatsapp/ticket/R32656'
response = await axios.get(url, {
httpsAgent,
headers: {
'x-access-token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOnsiaWQiOjEsInJvbGUiOiJjbGllbnQiLCJob3N0Ijoic29zLmVzcGFjb2xhc2VyLmNvbS5iciIsInRlbmFudCI6ImVzcGFjb2xhc2VyIiwibmFtZSI6IlNFTlNSLklUIiwiY29tcGFueSI6eyJpZCI6NDR9fSwiZGF0ZSI6MTY2MTI2MjY0MywiaWF0IjoxNjYxMjYyNjQzLCJleHAiOjE3NDc2NjI2NDN9.zf91OmRs4_C7B8OlVpLLrQMiRBYc7edP4qAdH_hqxpk',
'Origin': 'espacolaser'
}
});
console.log(`TEST URL CLIENT GET ROUTE: ${url} | STATUS CODE: ${response.status}`);
} catch (error) {
console.error(error);
}
return response
}
export default endPointQuery;

View File

@ -0,0 +1,20 @@
import dir from 'path';
import fs from 'fs';
import os from 'os';
const deleteFileFromTMP = (name_ext: string) =>{
const botInfoFile = dir.join(os.tmpdir(), name_ext);
try {
fs.unlinkSync(botInfoFile);
console.log(`${name_ext} file deleted!`);
} catch (error) {
console.log(`Can't delete ${name_ext} file: `,error)
}
}
export default deleteFileFromTMP;

View File

@ -41,6 +41,14 @@ class Contact extends Model<Contact> {
@Column @Column
isGroup: boolean; isGroup: boolean;
@Default(true)
@Column
useQueues: boolean;
@Default(true)
@Column
useDialogflow: boolean;
@CreatedAt @CreatedAt
createdAt: Date; createdAt: Date;

View File

@ -0,0 +1,45 @@
import {
Table,
Column,
CreatedAt,
UpdatedAt,
Model,
DataType,
PrimaryKey,
HasMany,
AutoIncrement
} from "sequelize-typescript";
import Queue from "./Queue";
@Table
class Dialogflow extends Model<Dialogflow> {
@PrimaryKey
@AutoIncrement
@Column
id: number;
@Column(DataType.TEXT)
name: string;
@Column(DataType.TEXT)
projectName: string;
@Column(DataType.TEXT)
jsonContent: string;
@Column(DataType.TEXT)
language: string;
@CreatedAt
@Column(DataType.DATE(6))
createdAt: Date;
@UpdatedAt
@Column(DataType.DATE(6))
updatedAt: Date;
@HasMany(() => Queue)
queues: Queue[]
}
export default Dialogflow;

View File

@ -8,13 +8,16 @@ import {
AutoIncrement, AutoIncrement,
AllowNull, AllowNull,
Unique, Unique,
BelongsToMany BelongsToMany,
BelongsTo,
ForeignKey
} from "sequelize-typescript"; } from "sequelize-typescript";
import User from "./User"; import User from "./User";
import UserQueue from "./UserQueue"; import UserQueue from "./UserQueue";
import Whatsapp from "./Whatsapp"; import Whatsapp from "./Whatsapp";
import WhatsappQueue from "./WhatsappQueue"; import WhatsappQueue from "./WhatsappQueue";
import Dialogflow from "./Dialogflow";
@Table @Table
class Queue extends Model<Queue> { class Queue extends Model<Queue> {
@ -36,6 +39,13 @@ class Queue extends Model<Queue> {
@Column @Column
greetingMessage: string; greetingMessage: string;
@ForeignKey(() => Dialogflow)
@Column
dialogflowId: number;
@BelongsTo(() => Dialogflow)
dialogflow: Dialogflow;
@CreatedAt @CreatedAt
createdAt: Date; createdAt: Date;

View File

@ -18,6 +18,10 @@ contactRoutes.post("/contacts", isAuth, ContactController.store);
contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update); contactRoutes.put("/contacts/:contactId", isAuth, ContactController.update);
contactRoutes.put("/contacts/toggleUseQueues/:contactId", isAuth, ContactController.toggleUseQueue);
contactRoutes.put("/contacts/toggleUseDialogflow/:contactId", isAuth, ContactController.toggleUseDialogflow);
contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove); contactRoutes.delete("/contacts/:contactId", isAuth, ContactController.remove);
export default contactRoutes; export default contactRoutes;

View File

@ -0,0 +1,20 @@
import { Router } from "express";
import isAuth from "../middleware/isAuth";
import * as DialogflowController from "../controllers/DialogflowController";
const dialogflowRoutes = Router();
dialogflowRoutes.get("/dialogflow", isAuth, DialogflowController.index);
dialogflowRoutes.post("/dialogflow", isAuth, DialogflowController.store);
dialogflowRoutes.get("/dialogflow/:dialogflowId", isAuth, DialogflowController.show);
dialogflowRoutes.put("/dialogflow/:dialogflowId", isAuth, DialogflowController.update);
dialogflowRoutes.delete("/dialogflow/:dialogflowId", isAuth, DialogflowController.remove);
dialogflowRoutes.post("/dialogflow/testsession", isAuth, DialogflowController.testSession);
export default dialogflowRoutes;

View File

@ -13,6 +13,7 @@ import quickAnswerRoutes from "./quickAnswerRoutes";
import reportRoutes from "./reportRoutes"; import reportRoutes from "./reportRoutes";
import schedulingNotifiyRoutes from "./SchedulingNotifyRoutes"; import schedulingNotifiyRoutes from "./SchedulingNotifyRoutes";
import statusChatEndRoutes from "./statusChatEndRoutes"; import statusChatEndRoutes from "./statusChatEndRoutes";
import dialogflowRoutes from "./dialogflowRoutes";
const routes = Router(); const routes = Router();
@ -31,5 +32,6 @@ routes.use(quickAnswerRoutes);
routes.use(schedulingNotifiyRoutes); routes.use(schedulingNotifiyRoutes);
routes.use(reportRoutes); routes.use(reportRoutes);
routes.use(statusChatEndRoutes); routes.use(statusChatEndRoutes);
routes.use(dialogflowRoutes);
export default routes; export default routes;

View File

@ -3,6 +3,7 @@ import Contact from "../../models/Contact";
import { createOrUpdateContactCache } from '../../helpers/ContactsCache' import { createOrUpdateContactCache } from '../../helpers/ContactsCache'
interface ExtraInfo { interface ExtraInfo {
name: string; name: string;
value: string; value: string;
@ -12,6 +13,7 @@ interface Request {
name: string; name: string;
number: string; number: string;
email?: string; email?: string;
useDialogflow?: boolean;
profilePicUrl?: string; profilePicUrl?: string;
extraInfo?: ExtraInfo[]; extraInfo?: ExtraInfo[];
} }
@ -20,6 +22,7 @@ const CreateContactService = async ({
name, name,
number, number,
email = "", email = "",
useDialogflow,
extraInfo = [] extraInfo = []
}: Request): Promise<Contact> => { }: Request): Promise<Contact> => {
const numberExists = await Contact.findOne({ const numberExists = await Contact.findOne({
@ -35,6 +38,7 @@ const CreateContactService = async ({
name, name,
number, number,
email, email,
useDialogflow,
extraInfo extraInfo
}, },
{ {
@ -47,7 +51,6 @@ const CreateContactService = async ({
await createOrUpdateContactCache(`contact:${contact.id}`, {id: contact.id, name, number, profilePicUrl:'', isGroup:'false', extraInfo, email }) await createOrUpdateContactCache(`contact:${contact.id}`, {id: contact.id, name, number, profilePicUrl:'', isGroup:'false', extraInfo, email })
// //
return contact; return contact;
}; };

View File

@ -0,0 +1,38 @@
import AppError from "../../errors/AppError";
import Contact from "../../models/Contact";
import CreateContactService from "./CreateContactService";
interface ExtraInfo {
name: string;
value: string;
}
interface Request {
name: string;
number: string;
email?: string;
profilePicUrl?: string;
extraInfo?: ExtraInfo[];
}
const GetContactService = async ({ name, number }: Request): Promise<Contact> => {
const numberExists = await Contact.findOne({
where: { number }
});
if (!numberExists) {
const contact = await CreateContactService({
name,
number,
})
if (contact == null)
throw new AppError("CONTACT_NOT_FOUND")
else
return contact
}
return numberExists
};
export default GetContactService;

View File

@ -0,0 +1,34 @@
import AppError from "../../errors/AppError";
import Contact from "../../models/Contact";
interface Request {
contactId: string;
}
const ToggleUseDialogflowContactService = async ({
contactId
}: Request): Promise<Contact> => {
const contact = await Contact.findOne({
where: { id: contactId },
attributes: ["id", "useDialogflow"]
});
if (!contact) {
throw new AppError("ERR_NO_CONTACT_FOUND", 404);
}
const useDialogflow = contact.useDialogflow ? false : true;
await contact.update({
useDialogflow
});
await contact.reload({
attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"],
include: ["extraInfo"]
});
return contact;
};
export default ToggleUseDialogflowContactService;

View File

@ -0,0 +1,34 @@
import AppError from "../../errors/AppError";
import Contact from "../../models/Contact";
interface Request {
contactId: string;
}
const ToggleUseQueuesContactService = async ({
contactId
}: Request): Promise<Contact> => {
const contact = await Contact.findOne({
where: { id: contactId },
attributes: ["id", "useQueues"]
});
if (!contact) {
throw new AppError("ERR_NO_CONTACT_FOUND", 404);
}
const useQueues = contact.useQueues ? false : true;
await contact.update({
useQueues
});
await contact.reload({
attributes: ["id", "name", "number", "email", "profilePicUrl", "useQueues", "useDialogflow"],
include: ["extraInfo"]
});
return contact;
};
export default ToggleUseQueuesContactService;

View File

@ -0,0 +1,76 @@
import * as Yup from "yup";
import AppError from "../../errors/AppError";
import Dialogflow from "../../models/Dialogflow";
interface Request {
name: string;
projectName: string;
jsonContent: string;
language: string;
}
const CreateDialogflowService = async ({
name,
projectName,
jsonContent,
language
}: Request): Promise<Dialogflow> => {
const schema = Yup.object().shape({
name: Yup.string()
.required()
.min(2)
.test(
"Check-name",
"This DialogFlow name is already used.",
async value => {
if (!value) return false;
const nameExists = await Dialogflow.findOne({
where: { name: value }
});
return !nameExists;
}
),
projectName: Yup.string()
.required()
.min(2)
.test(
"Check-name",
"This DialogFlow projectName is already used.",
async value => {
if (!value) return false;
const nameExists = await Dialogflow.findOne({
where: { projectName: value }
});
return !nameExists;
}
),
jsonContent: Yup.string()
.required()
,
language: Yup.string()
.required()
.min(2)
});
try {
await schema.validate({ name, projectName, jsonContent, language });
} catch (err) {
throw new AppError(err.message);
}
const dialogflow = await Dialogflow.create(
{
name,
projectName,
jsonContent,
language
}
);
return dialogflow;
};
export default CreateDialogflowService;

View File

@ -0,0 +1,33 @@
import { SessionsClient } from "@google-cloud/dialogflow";
import Dialogflow from "../../models/Dialogflow";
import dir from 'path';
import fs from 'fs';
import os from 'os';
import { logger } from "../../utils/logger";
const sessions : Map<number, SessionsClient> = new Map<number, SessionsClient>();
const createDialogflowSession = async (id:number, projectName:string, jsonContent:string) : Promise<SessionsClient | undefined> => {
if(sessions.has(id)) {
return sessions.get(id);
}
const keyFilename = dir.join(os.tmpdir(), `whaticket_${id}.json`);
console.log('keyFilename: ',keyFilename)
logger.info(`Openig new dialogflow session #${projectName} in '${keyFilename}'`)
await fs.writeFileSync(keyFilename, jsonContent);
const session = new SessionsClient({ keyFilename });
sessions.set(id, session);
return session;
}
const createDialogflowSessionWithModel = async (model: Dialogflow) : Promise<SessionsClient | undefined> => {
return createDialogflowSession(model.id, model.projectName, model.jsonContent);
}
export { createDialogflowSession, createDialogflowSessionWithModel };

View File

@ -0,0 +1,16 @@
import Dialogflow from "../../models/Dialogflow";
import AppError from "../../errors/AppError";
const DeleteDialogflowService = async (id: string): Promise<void> => {
const dialogflow = await Dialogflow.findOne({
where: { id }
});
if (!dialogflow) {
throw new AppError("ERR_NO_DIALOG_FOUND", 404);
}
await dialogflow.destroy();
};
export default DeleteDialogflowService;

View File

@ -0,0 +1,9 @@
import DialogFLow from "../../models/Dialogflow";
const ListDialogflowService = async (): Promise<DialogFLow[]> => {
const dialogFLows = await DialogFLow.findAll();
return dialogFLows;
};
export default ListDialogflowService;

View File

@ -0,0 +1,64 @@
import * as Sentry from "@sentry/node";
import { SessionsClient } from "@google-cloud/dialogflow";
import { logger } from "../../utils/logger";
async function detectIntent(
sessionClient:SessionsClient,
projectId:string,
sessionId:string,
query:string,
languageCode:string
) {
const sessionPath = sessionClient.projectAgentSessionPath(
projectId,
sessionId
);
const request = {
session: sessionPath,
queryInput: {
text: {
text: query,
languageCode: languageCode,
},
},
};
const responses = await sessionClient.detectIntent(request);
return responses[0];
}
async function queryDialogFlow(
sessionClient:SessionsClient,
projectId:string,
sessionId:string,
query:string,
languageCode:string
) : Promise<any | null> {
let intentResponse;
try {
intentResponse = await detectIntent(
sessionClient,
projectId,
sessionId,
query,
languageCode
);
const responses = intentResponse?.queryResult?.fulfillmentMessages;
if (responses?.length === 0) {
return null;
} else {
return responses;
}
} catch (error) {
Sentry.captureException(error);
logger.error(`Error handling whatsapp message: Err: ${error}`);
}
return null;
}
export {queryDialogFlow}

View File

@ -0,0 +1,15 @@
import Dialogflow from "../../models/Dialogflow";
import AppError from "../../errors/AppError";
const ShowDialogflowService = async (id: string | number): Promise<Dialogflow> => {
const dialogflow = await Dialogflow.findByPk(id);
if (!dialogflow) {
throw new AppError("ERR_NO_DIALOG_FOUND", 404);
}
return dialogflow;
};
export default ShowDialogflowService;

View File

@ -0,0 +1,70 @@
import * as Yup from "yup";
import AppError from "../../errors/AppError";
import { queryDialogFlow } from "./QueryDialogflow";
import { createDialogflowSession } from "./CreateSessionDialogflow";
interface Request {
projectName: string;
jsonContent: string;
language: string;
}
interface Response {
messages: string[];
}
const TestDialogflowSession = async ({
projectName,
jsonContent,
language
}: Request): Promise<Response | null> => {
const schema = Yup.object().shape({
projectName: Yup.string()
.required()
.min(2),
jsonContent: Yup.string()
.required(),
language: Yup.string()
.required()
.min(2)
});
try {
await schema.validate({ projectName, jsonContent, language });
} catch (err) {
throw new AppError(err.message);
}
const session = await createDialogflowSession(999, projectName, jsonContent);
if (!session) {
throw new AppError("ERR_TEST_SESSION_DIALOG", 400);
}
let dialogFlowReply = await queryDialogFlow(
session,
projectName,
"TestSeesion",
"Ola",
language,
);
await session.close();
if (!dialogFlowReply) {
throw new AppError("ERR_TEST_REPLY_DIALOG", 400);
}
const messages = [];
for (let message of dialogFlowReply) {
messages.push(message.text.text[0]);
}
return { messages };
}
export default TestDialogflowSession;

View File

@ -0,0 +1,55 @@
import * as Yup from "yup";
import AppError from "../../errors/AppError";
import Dialogflow from "../../models/Dialogflow";
import ShowDialogflowService from "./ShowDialogflowService";
interface DialogflowData {
name?: string;
projectName?: string;
jsonContent?: string;
language?: string;
}
interface Request {
dialogflowData: DialogflowData;
dialogflowId: string;
}
const UpdateDialogflowService = async ({
dialogflowData,
dialogflowId
}: Request): Promise<Dialogflow> => {
const schema = Yup.object().shape({
name: Yup.string().min(2),
projectName: Yup.string().min(2),
jsonContent: Yup.string().min(2),
language: Yup.string().min(2)
});
const {
name,
projectName,
jsonContent,
language
} = dialogflowData;
try {
await schema.validate({ name, projectName, jsonContent, language });
} catch (err) {
throw new AppError(err.message);
}
const dialogflow = await ShowDialogflowService(dialogflowId);
await dialogflow.update({
name,
projectName,
jsonContent,
language
});
return dialogflow;
};
export default UpdateDialogflowService;

View File

@ -1,5 +1,6 @@
import * as Yup from "yup"; import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
interface QueueData { interface QueueData {
@ -61,6 +62,8 @@ const CreateQueueService = async (queueData: QueueData): Promise<Queue> => {
const queue = await Queue.create(queueData); const queue = await Queue.create(queueData);
deleteFileFromTMP(`botInfo.json`)
return queue; return queue;
}; };

View File

@ -5,6 +5,7 @@ import UserQueue from "../../models/UserQueue";
import ListTicketsServiceCache from "../TicketServices/ListTicketServiceCache"; import ListTicketsServiceCache from "../TicketServices/ListTicketServiceCache";
import { deleteTicketsFieldsCache } from '../../helpers/TicketCache' import { deleteTicketsFieldsCache } from '../../helpers/TicketCache'
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
const DeleteQueueService = async (queueId: number | string): Promise<void> => { const DeleteQueueService = async (queueId: number | string): Promise<void> => {
@ -29,6 +30,8 @@ const DeleteQueueService = async (queueId: number | string): Promise<void> => {
} }
await queue.destroy(); await queue.destroy();
deleteFileFromTMP(`botInfo.json`)
}; };
export default DeleteQueueService; export default DeleteQueueService;

View File

@ -1,6 +1,7 @@
import { Op } from "sequelize"; import { Op } from "sequelize";
import * as Yup from "yup"; import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
import Queue from "../../models/Queue"; import Queue from "../../models/Queue";
import ShowQueueService from "./ShowQueueService"; import ShowQueueService from "./ShowQueueService";
@ -67,6 +68,8 @@ const UpdateQueueService = async (
await queue.update(queueData); await queue.update(queueData);
deleteFileFromTMP(`botInfo.json`)
return queue; return queue;
}; };

View File

@ -26,8 +26,8 @@ const FindOrCreateTicketService = async (
//Habilitar esse caso queira usar o bot //Habilitar esse caso queira usar o bot
// const botInfo = await BotIsOnQueue('botqueue') const botInfo = await BotIsOnQueue('botqueue')
const botInfo = { isOnQueue: false } // const botInfo = { isOnQueue: false }

View File

@ -10,7 +10,7 @@ const ShowTicketService = async (id: string | number): Promise<Ticket> => {
{ {
model: Contact, model: Contact,
as: "contact", as: "contact",
attributes: ["id", "name", "number", "profilePicUrl"], attributes: ["id", "name", "number", "profilePicUrl", "useDialogflow", "useQueues"],
include: ["extraInfo"] include: ["extraInfo"]
}, },
{ {
@ -21,7 +21,8 @@ const ShowTicketService = async (id: string | number): Promise<Ticket> => {
{ {
model: Queue, model: Queue,
as: "queue", as: "queue",
attributes: ["id", "name", "color"] attributes: ["id", "name", "color"],
include: ["dialogflow"]
} }
] ]
}); });

View File

@ -4,6 +4,8 @@ import AppError from "../../errors/AppError";
import { SerializeUser } from "../../helpers/SerializeUser"; import { SerializeUser } from "../../helpers/SerializeUser";
import User from "../../models/User"; import User from "../../models/User";
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
interface Request { interface Request {
email: string; email: string;
password: string; password: string;
@ -78,6 +80,8 @@ const CreateUserService = async ({
const serializedUser = SerializeUser(user); const serializedUser = SerializeUser(user);
deleteFileFromTMP(`botInfo.json`)
return serializedUser; return serializedUser;
}; };

View File

@ -3,6 +3,7 @@ import AppError from "../../errors/AppError";
import Ticket from "../../models/Ticket"; import Ticket from "../../models/Ticket";
import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus"; import UpdateDeletedUserOpenTicketsStatus from "../../helpers/UpdateDeletedUserOpenTicketsStatus";
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
const DeleteUserService = async (id: string | number): Promise<void> => { const DeleteUserService = async (id: string | number): Promise<void> => {
const user = await User.findOne({ const user = await User.findOne({
@ -24,6 +25,8 @@ const DeleteUserService = async (id: string | number): Promise<void> => {
await user.destroy(); await user.destroy();
deleteFileFromTMP(`botInfo.json`)
}; };
export default DeleteUserService; export default DeleteUserService;

View File

@ -3,6 +3,8 @@ import * as Yup from "yup";
import AppError from "../../errors/AppError"; import AppError from "../../errors/AppError";
import ShowUserService from "./ShowUserService"; import ShowUserService from "./ShowUserService";
import deleteFileFromTMP from "../../helpers/deleteFileFromTMP";
interface UserData { interface UserData {
email?: string; email?: string;
password?: string; password?: string;
@ -67,6 +69,8 @@ const UpdateUserService = async ({
queues: user.queues queues: user.queues
}; };
deleteFileFromTMP(`botInfo.json`)
return serializedUser; return serializedUser;
}; };

View File

@ -0,0 +1,15 @@
const data:any[] = [
{
"id":"1",
"action":"transfer_to_attendant",
},
{
"id":"2",
"action":"request_endpoint",
},
]
export default data;

View File

@ -16,7 +16,9 @@ import {
Contact as WbotContact, Contact as WbotContact,
Message as WbotMessage, Message as WbotMessage,
MessageAck, MessageAck,
Client Client,
Chat,
MessageMedia
} from "whatsapp-web.js"; } from "whatsapp-web.js";
import Contact from "../../models/Contact"; import Contact from "../../models/Contact";
@ -44,17 +46,23 @@ import { StartWhatsAppSession } from "../../services/WbotServices/StartWhatsAppS
import { removeWbot } from '../../libs/wbot' import { removeWbot } from '../../libs/wbot'
import { restartWhatsSession } from "../../helpers/RestartWhatsSession"; import { restartWhatsSession } from "../../helpers/RestartWhatsSession";
// test del
import data_ura from './ura' import data_ura from './ura'
import msg_client_transfer from './ura_msg_transfer' import msg_client_transfer from './ura_msg_transfer'
import final_message from "./ura_final_message"; import final_message from "./ura_final_message";
import SendWhatsAppMessage from "./SendWhatsAppMessage"; import SendWhatsAppMessage from "./SendWhatsAppMessage";
import Whatsapp from "../../models/Whatsapp"; import Whatsapp from "../../models/Whatsapp";
import { splitDateTime } from "../../helpers/SplitDateTime"; import { splitDateTime } from "../../helpers/SplitDateTime";
//
import { queryDialogFlow } from "../DialogflowServices/QueryDialogflow";
import { createDialogflowSessionWithModel } from "../DialogflowServices/CreateSessionDialogflow";
import bot_actions from './BotActions'
import ShowTicketService from "../TicketServices/ShowTicketService";
import { updateTicketCacheByTicketId } from '../../helpers/TicketCache' import { updateTicketCacheByTicketId } from '../../helpers/TicketCache'
import endPointQuery from "../../helpers/EndpointQuery";
interface Session extends Client { interface Session extends Client {
@ -171,6 +179,171 @@ const verifyMessage = async (
}; };
async function sendDelayedMessages(wbot: Session, ticket: Ticket, contact: Contact, message: string) {
const body = message.replace(/\\n/g, '\n');
if (body.search('dialog_actions') != -1) {
let msgAction = botMsgActions(body)
console.log('gggggggggggggggggggggggggggggggggg msgAction: ', msgAction)
if (msgAction.actions[0] == 'request_endpoint') {
const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody);
await verifyMessage(sentMessage, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
// const url = 'https://sos.espacolaser.com.br/api/whatsapps/ticket/R32656'
const endPointResponse = await endPointQuery(msgAction.actions[1])
if (endPointResponse) {
const response = Object.entries(endPointResponse.data);
let msg_endpoint_response = ''
for (let i = 0; i < response.length; i++) {
msg_endpoint_response += `*${response[i][0]}*: ${response[i][1]}\n`
}
if (endPointResponse.data.status == 'EM ATENDIMENTO') {
// const msg = await wbot.sendMessage(`${contact.number}@c.us`, `Seu chamado está em atendimento pelo analista ${endPointResponse.data.tecnico} + Última informação do CHAT
// Verificar pelo “ID do solicitante” se existe chamado na HIT -> Se houver, informar status (Vamos alinhar os detalhes)`);
// const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Seu chamado está em atendimento\n\n *Analista:* ${endPointResponse.data.tecnico}\n *Chat:* ${endPointResponse.data.chat ? endPointResponse.data.chat : ""}\n\n_Digite *0* para voltar ao menu principal._`);
const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Seu chamado está em atendimento\n\n${msg_endpoint_response}\n_Digite *0* para voltar ao menu principal._`);
await verifyMessage(msg, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
}
else if (endPointResponse.data.categoria == 'ELOS' || (endPointResponse.data.subcategoria == 'VENDA' || endPointResponse.data.subcategoria == 'INDISPONIBILIDADE')) {
// const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Vi que está com um problema no ELOS\n\n *Status:* ${endPointResponse.data.status}\n\nSe seu caso for urgente para concluir uma venda, digite “URGENTE”\n_Digite *0* para voltar ao menu principal._`);
const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n Vi que está com um problema no ELOS\n\n${msg_endpoint_response}\nSe seu caso for urgente para concluir uma venda, digite “URGENTE”\n_Digite *0* para voltar ao menu principal._`);
await verifyMessage(msg, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
}
else if ((endPointResponse.data.categoria == 'INFRAESTRUTURA' || endPointResponse.data.subcategoria == 'INTERNET' ||
endPointResponse.data.terceiro_nivel == 'QUEDA TOTAL' || endPointResponse.data.terceiro_nivel == 'PROBLEMA DE LENTIDÃO') ||
(endPointResponse.data.terceiro_nivel == 'PROBLEMA DE LENTIDÃO' || endPointResponse.data.terceiro_nivel == 'ABERTO')) {
const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n${msg_endpoint_response}\n Estamos direcionando seu atendimento para o Suporte. Em breve você será atendido por um de nossos atendentes!`);
await transferTicket(0, wbot, ticket, contact)
}
else {
// const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n *Status:* ${endPointResponse.data.status}\n *Data:* ${endPointResponse.data.data_chat ? endPointResponse.data.data_chat : ""}\n *Hora:* ${endPointResponse.data.hora_chat ? endPointResponse.data.hora_chat : ""} \n\n Por favor, aguarde atendimento e acompanhe sua solicitação no SOS.\n_Digite *0* para voltar ao menu principal._`);
const msg = await wbot.sendMessage(`${contact.number}@c.us`, `*Situação do chamado ${extractCallCode(msgAction.msgBody)}:*\n\n${msg_endpoint_response}\n Por favor, aguarde atendimento e acompanhe sua solicitação no SOS.\n_Digite *0* para voltar ao menu principal._`);
await verifyMessage(msg, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
}
}
else {
botSendMessage(ticket, contact, wbot, `Desculpe, nao foi possível realizar a consulta!\n _Digite *0* para voltar ao menu principal._`)
}
} else if (msgAction.actions[0] == 'queue_transfer') {
console.log('>>>>>>>>>>>>>>> msgAction: ', msgAction, ' | msgAction.actions[1]: ', msgAction.actions[1])
const msg = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody);
await verifyMessage(msg, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
await transferTicket(+msgAction.actions[1], wbot, ticket, contact)
}
else if (msgAction.actions[0] == 'send_file'){
const sourcePath = path.join(__dirname,`../../../public/bot`)
const msg = await wbot.sendMessage(`${contact.number}@c.us`, msgAction.msgBody);
await verifyMessage(msg, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
await botSendMedia(ticket,contact,wbot,sourcePath, msgAction.actions[1])
}
}
else {
// const linesOfBody = body.split('\n');
const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, body);
await verifyMessage(sentMessage, ticket, contact);
await new Promise(f => setTimeout(f, 1000));
// for(let message of linesOfBody) {
// const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, message);
// await verifyMessage(sentMessage, ticket, contact);
// await new Promise(f => setTimeout(f, 1000));
// }
}
}
const extractCallCode = (str: string) => {
if (str.includes('*') && str.indexOf('*') < str.lastIndexOf('*')) {
return (str.substring(str.indexOf('*'), str.lastIndexOf('*') + 1)).split('*').join('')
}
return ''
}
const sendDialogflowAwswer = async (
wbot: Session,
ticket: Ticket,
msg: WbotMessage,
contact: Contact,
chat: Chat
) => {
const session = await createDialogflowSessionWithModel(ticket.queue.dialogflow);
if (session === undefined) {
return;
}
wbot.sendPresenceAvailable();
// console.log('typeof(msg.type): ', typeof (msg.type), ' | msg.type: ', msg.type)
if (msg.type != 'chat') {
botSendMessage(ticket, contact, wbot, `Desculpe, nao compreendi!\nEnvie apenas texto quando estiver interagindo com o bot!\n _Digite *0* para voltar ao menu principal._`)
return
}
if (msg.type == 'chat' && String(msg.body).length > 120) {
botSendMessage(ticket, contact, wbot, `Desculpe, nao compreendi!\nTexto acima de 120 caracteres!\n _Digite *0* para voltar ao menu principal._`)
return
}
let dialogFlowReply = await queryDialogFlow(
session,
ticket.queue.dialogflow.projectName,
msg.from,
msg.body,
ticket.queue.dialogflow.language
);
if (dialogFlowReply === null) {
return;
}
chat.sendStateTyping();
await new Promise(f => setTimeout(f, 1000));
for (let message of dialogFlowReply) {
await sendDelayedMessages(wbot, ticket, contact, message.text.text[0]);
}
}
const verifyQueue = async ( const verifyQueue = async (
wbot: Session, wbot: Session,
@ -195,8 +368,8 @@ const verifyQueue = async (
let choosenQueue = null let choosenQueue = null
//Habilitar esse caso queira usar o bot //Habilitar esse caso queira usar o bot
// const botInfo = await BotIsOnQueue('botqueue') const botInfo = await BotIsOnQueue('botqueue')
const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 } // const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 }
if (botInfo.isOnQueue) { if (botInfo.isOnQueue) {
@ -247,7 +420,12 @@ const verifyQueue = async (
ticketId: ticket.id ticketId: ticket.id
}); });
data_ura.forEach((s, index) => { botOptions += `*${index + 1}* - ${s.option}\n` }); const _ticket = await ShowTicketService(ticket.id);
const chat = await msg.getChat();
await sendDialogflowAwswer(wbot, _ticket, msg, contact, chat);
return
} }
// //
@ -302,6 +480,27 @@ const verifyQueue = async (
} }
}; };
const transferTicket = async (queueIndex: number, wbot: Session, ticket: Ticket, contact: Contact) => {
const botInfo = await BotIsOnQueue('botqueue')
const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId)
const queues = queuesWhatsGreetingMessage.queues
await botTransferTicket(queues[queueIndex], ticket, contact, wbot)
}
const botMsgActions = (params: string) => {
let lstActions = params.split('dialog_actions=')
let bodyMsg = lstActions[0].trim()
let actions = lstActions[1].split("=")
return { msgBody: bodyMsg, 'actions': [actions[0].trim(), actions[1].trim()] };
}
const isValidMsg = (msg: WbotMessage): boolean => { const isValidMsg = (msg: WbotMessage): boolean => {
if (msg.from === "status@broadcast") return false; if (msg.from === "status@broadcast") return false;
if ( if (
@ -341,6 +540,37 @@ const botTransferTicket = async (queues: Queue, ticket: Ticket, contact: Contact
} }
const botSendMedia = async (ticket: Ticket, contact: Contact, wbot: Session, mediaPath: string, fileNameExtension: string) => {
const debouncedSentMessage = debounce(
async () => {
// const sentMessage = await wbot.sendMessage(`${ticket.contact.number}@${ticket.isGroup ? "g" : "c"}.us`, newMedia, { sendAudioAsVoice: true });
const newMedia = MessageMedia.fromFilePath(`${mediaPath}/${fileNameExtension}`);
const sentMessage = await wbot.sendMessage(`${contact.number}@c.us`, newMedia, {caption: 'this is my caption'});
// client.sendMessage("xxxxxxxx@c.us", media, {caption: "some caption"}); })();
// client.sendMessage(msg.from, attachmentData, { caption: 'Here\'s your requested media.' });
await ticket.update({ lastMessage: fileNameExtension });
verifyMessage(sentMessage, ticket, contact);
},
3000,
ticket.id
);
debouncedSentMessage();
}
const botSendMessage = (ticket: Ticket, contact: Contact, wbot: Session, msg: string) => { const botSendMessage = (ticket: Ticket, contact: Contact, wbot: Session, msg: string) => {
@ -469,307 +699,14 @@ const handleMessage = async (
// O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente // O bot interage com o cliente e encaminha o atendimento para fila de atendende quando o usuário escolhe a opção falar com atendente
//Habilitar esse caso queira usar o bot //Habilitar esse caso queira usar o bot
// const botInfo = await BotIsOnQueue('botqueue') const botInfo = await BotIsOnQueue('botqueue')
const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 } // const botInfo = { isOnQueue: false, botQueueId: 0, userIdBot: 0 }
if (botInfo.isOnQueue && !msg.fromMe && ticket.userId == botInfo.userIdBot) { if (botInfo.isOnQueue && !msg.fromMe && ticket.userId == botInfo.userIdBot) {
await sendDialogflowAwswer(wbot, ticket, msg, contact, chat);
if (msg.body === '0') {
const queue = await ShowQueueService(ticket.queue.id);
const greetingMessage = `\u200e${queue.greetingMessage}`;
let options = "";
data_ura.forEach((s, index) => { options += `*${index + 1}* - ${s.option}\n` });
botSendMessage(ticket, contact, wbot, `${greetingMessage}\n\n${options}\n${final_message.msg}`)
}
else {
// Pega as ultimas 9 opções numericas digitadas pelo cliente em orde DESC
// Consulta apenas mensagens do usuári
let lastOption = ''
let ura_length = data_ura.length
let indexAttendant = data_ura.findIndex((u) => u.atendente)
let opt_user_attendant = '-1'
if (indexAttendant != -1) {
opt_user_attendant = data_ura[indexAttendant].id
}
//
let ticket_message = await ShowTicketMessage(ticket.id, true, ura_length, `^[0-${ura_length}}]$`);
if (ticket_message.length > 1) {
lastOption = ticket_message[1].body
const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId)
const queues = queuesWhatsGreetingMessage.queues
if (queues.length > 1) {
const index_opt_user_attendant = ticket_message.findIndex((q) => q.body == opt_user_attendant)
const index0 = ticket_message.findIndex((q) => q.body == '0')
if (index_opt_user_attendant != -1) {
if (index0 > -1 && index0 < index_opt_user_attendant) {
lastOption = ''
}
else {
lastOption = opt_user_attendant
}
}
} }
}
//
//
// È numero
if (!Number.isNaN(Number(msg.body.trim())) && (+msg.body >= 0 && +msg.body <= data_ura.length)) {
const indexUra = data_ura.findIndex((ura) => ura.id == msg.body.trim())
if (indexUra != -1) {
if (data_ura[indexUra].id != opt_user_attendant && lastOption != opt_user_attendant) {
// test del
let next = true
let indexAux = ticket_message.findIndex((e) => e.body == '0')
let listMessage = null
if (indexAux != -1) {
listMessage = ticket_message.slice(0, indexAux)
}
else {
listMessage = ticket_message
}
let id = ''
let subUra = null
if (listMessage.length > 1) {
id = listMessage[listMessage.length - 1].body
subUra = data_ura.filter((e) => e.id == id)[0]
if (subUra && (!subUra.subOptions || subUra.subOptions.length == 0)) {
listMessage.pop()
}
}
if (listMessage.length > 1) {
id = listMessage[listMessage.length - 1].body
subUra = data_ura.filter((e) => e.id == id)[0]
if (subUra.subOptions && subUra.subOptions.length > 0) {
if (!Number.isNaN(Number(msg.body.trim())) && (+msg.body >= 0 && +msg.body <= subUra.subOptions?.length) && subUra.subOptions) {
if (subUra.subOptions[+msg.body - 1].responseToClient) {
botSendMessage(ticket, contact, wbot, `*${subUra.option}*\n\n${subUra.subOptions[+msg.body - 1].responseToClient}`)
}
else {
botSendMessage(ticket, contact, wbot, `*${subUra.option}*\n\n${subUra.subOptions[+msg.body - 1].subOpt}`)
}
const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId)
const queues = queuesWhatsGreetingMessage.queues
if (queues.length > 0) {
await botTransferTicket(queues[0], ticket, contact, wbot)
}
else {
console.log('NO QUEUE!')
}
}
else {
let options = "";
let subOptions: any[] = subUra.subOptions
subOptions?.forEach((s, index) => { options += `*${index + 1}* - ${s.subOpt}\n` });
botSendMessage(ticket, contact, wbot, `*${subUra.option}*\n\nDigite um número válido disponível no menu de opções de atendimento abaixo: \n${options}\n\n*0* - Voltar ao menu principal`)
}
next = false
}
}
//
if (next) {
if (data_ura[indexUra].subOptions && data_ura[indexUra].subOptions.length > 0) {
let options = "";
let option = data_ura[indexUra].option
let subOptions: any[] = data_ura[indexUra].subOptions
let description = data_ura[indexUra].description
subOptions?.forEach((s, index) => { options += `*${index + 1}* - ${s.subOpt}\n` });
const body = `\u200e${description}:\n${options}`
botSendMessage(ticket, contact, wbot, `*${option}*\n\n${body}\n\n *0* - Voltar ao menu principal`)
}
else {
//test del deletar isso (Usar somente na hit)
if (data_ura[indexUra].closeChat) {
const { ticket: res } = await UpdateTicketService({
ticketData: { 'status': 'closed', 'userId': botInfo.userIdBot }, ticketId: ticket.id
});
///////////////////////////////
const whatsapp = await ShowWhatsAppService(ticket.whatsappId);
const { farewellMessage } = whatsapp;
if (farewellMessage) {
await SendWhatsAppMessage({ body: farewellMessage, ticket: res });
}
///////////////////////////////
}
else {
botSendMessage(ticket, contact, wbot, `${data_ura[indexUra].description}\n\n *0* - Voltar ao menu principal`)
}
//
// botSendMessage(ticket, contact, wbot, `${data_ura[indexUra].description}\n\n *0* - Voltar ao menu principal`)
}
}
}
else if (data_ura[indexUra].id == opt_user_attendant) {
const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId)
const queues = queuesWhatsGreetingMessage.queues
// Se fila for maior que 1 exibi as opções fila para atendimento humano
if (queues.length > 1) {
let options = "";
queues.forEach((queue, index) => {
options += `*${index + 1}* - ${queue.name}\n`;
});
const body = `\u200eSelecione uma das opções de atendimento abaixo:\n${options}`;
botSendMessage(ticket, contact, wbot, body)
} // Para situações onde há apenas uma fila com exclusão da fila do bot, já direciona o cliente para essa fila de atendimento humano
else if (queues.length == 1) {
await botTransferTicket(queues[0], ticket, contact, wbot)
botSendMessage(ticket, contact, wbot, `${msg_client_transfer.msg}`)
}
}
else if (lastOption == opt_user_attendant) {
const queuesWhatsGreetingMessage = await queuesOutBot(wbot, botInfo.botQueueId)
const queues = queuesWhatsGreetingMessage.queues
// É numero
if (!Number.isNaN(Number(msg.body.trim())) && (+msg.body >= 0 && +msg.body <= queues.length)) {
await botTransferTicket(queues[+msg.body - 1], ticket, contact, wbot)
botSendMessage(ticket, contact, wbot, `${msg_client_transfer.msg}`)
}
else {
botSendMessage(ticket, contact, wbot, `Digite um número válido disponível no menu de opções de atendimento\n\n*0* - Voltar ao menu principal`)
}
}
}
}
else {
// É numero
if (!Number.isNaN(Number(msg.body.trim()))) {
botSendMessage(ticket, contact, wbot, `Opção numérica inválida!\nDigite um dos números mostrados no menu de opções\n\n*0* - Voltar ao menu principal`)
}
else {
botSendMessage(ticket, contact, wbot, `Digite um número válido disponível no menu de opções\n\n*0* - Voltar ao menu principal`)
}
}
}
}
//
// test del
// if (msg.body.trim() == 'broken') { // if (msg.body.trim() == 'broken') {
// throw new Error('Throw makes it go boom!') // throw new Error('Throw makes it go boom!')

View File

@ -73,6 +73,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => {
name: "", name: "",
number: "", number: "",
email: "", email: "",
useDialogflow: true,
}; };
const [contact, setContact] = useState(initialState); const [contact, setContact] = useState(initialState);

View File

@ -0,0 +1,285 @@
import React, { useState, useEffect } from "react";
import * as Yup from "yup";
import { Formik, Form, Field } from "formik";
import { toast } from "react-toastify";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
CircularProgress,
Select,
InputLabel,
MenuItem,
FormControl,
TextField,
} from '@material-ui/core'
import { makeStyles } from "@material-ui/core/styles";
import { green } from "@material-ui/core/colors";
import { i18n } from "../../translate/i18n";
import api from "../../services/api";
import toastError from "../../errors/toastError";
const useStyles = makeStyles(theme => ({
root: {
display: "flex",
flexWrap: "wrap",
},
textField: {
marginRight: theme.spacing(1),
flex: 1,
},
btnWrapper: {
position: "relative",
},
buttonProgress: {
color: green[500],
position: "absolute",
top: "50%",
left: "50%",
marginTop: -12,
marginLeft: -12,
},
btnLeft: {
display: "flex",
marginRight: "auto",
marginLeft: 12,
},
formControl: {
margin: theme.spacing(1),
minWidth: 240,
},
colorAdorment: {
width: 20,
height: 20,
},
}));
const DialogflowSchema = Yup.object().shape({
name: Yup.string()
.min(2, "Too Short!")
.max(50, "Too Long!")
.required("Required"),
projectName: Yup.string().min(3, "Too Short!").max(100, "Too Long!").required(),
jsonContent: Yup.string().min(3, "Too Short!").required(),
language: Yup.string().min(2, "Too Short!").max(50, "Too Long!").required(),
});
const DialogflowModal = ({ open, onClose, dialogflowId }) => {
const classes = useStyles();
const initialState = {
name: "",
projectName: "",
jsonContent: "",
language: "",
};
const [dialogflow, setDialogflow] = useState(initialState);
useEffect(() => {
(async () => {
if (!dialogflowId) return;
try {
const { data } = await api.get(`/dialogflow/${dialogflowId}`);
setDialogflow(prevState => {
return { ...prevState, ...data };
});
} catch (err) {
toastError(err);
}
})();
return () => {
setDialogflow({
name: "",
projectName: "",
jsonContent: "",
language: "",
});
};
}, [dialogflowId, open]);
const handleClose = () => {
onClose();
setDialogflow(initialState);
};
const handleTestSession = async (event, values) => {
try {
const {projectName, jsonContent, language } = values
await api.post(`/dialogflow/testSession`, {projectName, jsonContent, language });
toast.success( i18n.t("dialogflowModal.messages.testSuccess") );
} catch (err) {
toastError(err);
}
};
const handleSaveDialogflow = async values => {
try {
console.log(values)
if (dialogflowId) {
await api.put(`/dialogflow/${dialogflowId}`, values);
toast.success( i18n.t("dialogflowModal.messages.editSuccess") );
} else {
await api.post("/dialogflow", values);
toast.success( i18n.t("dialogflowModal.messages.addSuccess") );
}
handleClose();
} catch (err) {
toastError(err);
}
};
return (
<div className={classes.root}>
<Dialog open={open} onClose={handleClose} scroll="paper">
<DialogTitle>
{dialogflowId
? `${i18n.t("dialogflowModal.title.edit")}`
: `${i18n.t("dialogflowModal.title.add")}`}
</DialogTitle>
<Formik
initialValues={dialogflow}
enableReinitialize={true}
validationSchema={DialogflowSchema}
onSubmit={(values, actions, event) => {
setTimeout(() => {
console.log(actions);
handleSaveDialogflow(values);
actions.setSubmitting(false);
}, 400);
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<DialogContent dividers>
<Field
as={TextField}
label={i18n.t("dialogflowModal.form.name")}
autoFocus
name="name"
error={touched.name && Boolean(errors.name)}
helperText={touched.name && errors.name}
variant="outlined"
margin="dense"
className={classes.textField}
/>
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
>
<InputLabel id="language-selection-input-label">
{i18n.t("dialogflowModal.form.language")}
</InputLabel>
<Field
as={Select}
label={i18n.t("dialogflowModal.form.language")}
name="language"
labelId="profile-selection-label"
error={touched.language && Boolean(errors.language)}
helperText={touched.language && errors.language}
id="language-selection"
required
>
<MenuItem value="pt-BR">Portugues</MenuItem>
<MenuItem value="en">Inglês</MenuItem>
<MenuItem value="es">Espanhol</MenuItem>
</Field>
</FormControl>
<div>
<Field
as={TextField}
label={i18n.t("dialogflowModal.form.projectName")}
name="projectName"
error={touched.projectName && Boolean(errors.projectName)}
helperText={touched.projectName && errors.projectName}
fullWidth
variant="outlined"
margin="dense"
/>
</div>
<div>
<Field
as={TextField}
label={i18n.t("dialogflowModal.form.jsonContent")}
type="jsonContent"
multiline
//inputRef={greetingRef}
rows={5}
fullWidth
name="jsonContent"
error={
touched.jsonContent && Boolean(errors.jsonContent)
}
helperText={
touched.jsonContent && errors.jsonContent
}
variant="outlined"
margin="dense"
/>
</div>
</DialogContent>
<DialogActions>
<Button
//type="submit"
onClick={(e) => handleTestSession(e, values)}
color="inherit"
disabled={isSubmitting}
name="testSession"
variant="outlined"
className={classes.btnLeft}
>
{i18n.t("dialogflowModal.buttons.test")}
</Button>
<Button
onClick={handleClose}
color="secondary"
disabled={isSubmitting}
variant="outlined"
>
{i18n.t("dialogflowModal.buttons.cancel")}
</Button>
<Button
type="submit"
color="primary"
disabled={isSubmitting}
variant="contained"
className={classes.btnWrapper}
>
{dialogflowId
? `${i18n.t("dialogflowModal.buttons.okEdit")}`
: `${i18n.t("dialogflowModal.buttons.okAdd")}`}
{isSubmitting && (
<CircularProgress
size={24}
className={classes.buttonProgress}
/>
)}
</Button>
</DialogActions>
</Form>
)}
</Formik>
</Dialog>
</div>
);
};
export default DialogflowModal;

View File

@ -13,14 +13,23 @@ import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent"; import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle"; import DialogTitle from "@material-ui/core/DialogTitle";
import CircularProgress from "@material-ui/core/CircularProgress"; import CircularProgress from "@material-ui/core/CircularProgress";
import {
FormControl,
InputLabel,
Select,
MenuItem,
IconButton,
InputAdornment
} from "@material-ui/core"
import { i18n } from "../../translate/i18n"; import { i18n } from "../../translate/i18n";
import api from "../../services/api"; import api from "../../services/api";
import toastError from "../../errors/toastError"; import toastError from "../../errors/toastError";
import ColorPicker from "../ColorPicker"; import ColorPicker from "../ColorPicker";
import { IconButton, InputAdornment } from "@material-ui/core";
import { Colorize } from "@material-ui/icons"; import { Colorize } from "@material-ui/icons";
import useDialogflows from "../../hooks/useDialogflows";
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
@ -45,8 +54,8 @@ const useStyles = makeStyles(theme => ({
marginLeft: -12, marginLeft: -12,
}, },
formControl: { formControl: {
margin: theme.spacing(1), marginRight: theme.spacing(1),
minWidth: 120, minWidth: 200,
}, },
colorAdorment: { colorAdorment: {
width: 20, width: 20,
@ -61,6 +70,7 @@ const QueueSchema = Yup.object().shape({
.required("Required"), .required("Required"),
color: Yup.string().min(3, "Too Short!").max(9, "Too Long!").required(), color: Yup.string().min(3, "Too Short!").max(9, "Too Long!").required(),
greetingMessage: Yup.string(), greetingMessage: Yup.string(),
dialogflowId: Yup.number(),
}); });
const QueueModal = ({ open, onClose, queueId }) => { const QueueModal = ({ open, onClose, queueId }) => {
@ -70,10 +80,14 @@ const QueueModal = ({ open, onClose, queueId }) => {
name: "", name: "",
color: "", color: "",
greetingMessage: "", greetingMessage: "",
dialogflowId: "",
}; };
const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false); const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false);
const [queue, setQueue] = useState(initialState); const [queue, setQueue] = useState(initialState);
const [dialogflows, setDialogflows] = useState([]);
const { findAll: findAllDialogflows } = useDialogflows();
const greetingRef = useRef(); const greetingRef = useRef();
useEffect(() => { useEffect(() => {
@ -81,6 +95,10 @@ const QueueModal = ({ open, onClose, queueId }) => {
if (!queueId) return; if (!queueId) return;
try { try {
const { data } = await api.get(`/queue/${queueId}`); const { data } = await api.get(`/queue/${queueId}`);
if(data.dialogflowId === null) {
data.dialogflowId = "";
}
setQueue(prevState => { setQueue(prevState => {
return { ...prevState, ...data }; return { ...prevState, ...data };
}); });
@ -94,10 +112,20 @@ const QueueModal = ({ open, onClose, queueId }) => {
name: "", name: "",
color: "", color: "",
greetingMessage: "", greetingMessage: "",
dialogflowId: "",
}); });
}; };
}, [queueId, open]); }, [queueId, open]);
useEffect(() => {
const loadDialogflows = async () => {
const list = await findAllDialogflows();
setDialogflows(list);
}
loadDialogflows();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const handleClose = () => { const handleClose = () => {
onClose(); onClose();
setQueue(initialState); setQueue(initialState);
@ -105,6 +133,10 @@ const QueueModal = ({ open, onClose, queueId }) => {
const handleSaveQueue = async values => { const handleSaveQueue = async values => {
try { try {
if(values.dialogflowId === "") {
values.dialogflowId = null;
}
if (queueId) { if (queueId) {
await api.put(`/queue/${queueId}`, values); await api.put(`/queue/${queueId}`, values);
} else { } else {
@ -213,6 +245,27 @@ const QueueModal = ({ open, onClose, queueId }) => {
margin="dense" margin="dense"
/> />
</div> </div>
<FormControl
variant="outlined"
className={classes.formControl}
margin="dense"
>
<InputLabel id="dialogflow-selection-input-label">
{i18n.t("queueModal.form.dialogflow")}
</InputLabel>
<Field
as={Select}
label={i18n.t("queueModal.form.dialogflow")}
name="dialogflowId"
labelId="dialogflow-selection-label"
id="dialogflow-selection"
>
<MenuItem value={''}>&nbsp;</MenuItem>
{dialogflows.map((dialogflow) => (
<MenuItem key={dialogflow.id} value={dialogflow.id}>{dialogflow.name}</MenuItem>
))}
</Field>
</FormControl>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button

View File

@ -34,6 +34,7 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
const history = useHistory(); const history = useHistory();
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [useDialogflow, setUseDialogflow] = useState(ticket.contact.useDialogflow);
const ticketOptionsMenuOpen = Boolean(anchorEl); const ticketOptionsMenuOpen = Boolean(anchorEl);
const { user } = useContext(AuthContext); const { user } = useContext(AuthContext);
@ -50,9 +51,9 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
const chatEndVal = (data) => { const chatEndVal = (data) => {
if(data){ if (data) {
data = {...data, 'ticketId': ticket.id} data = { ...data, 'ticketId': ticket.id }
@ -75,12 +76,12 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
}; };
const handleUpdateTicketStatus = async (e, status, userId, schedulingData={}) => { const handleUpdateTicketStatus = async (e, status, userId, schedulingData = {}) => {
setLoading(true); setLoading(true);
try { try {
if(status==='closed'){ if (status === 'closed') {
if (tabOption === 'search') { if (tabOption === 'search') {
setTabOption('open') setTabOption('open')
@ -93,7 +94,7 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
}); });
} }
else{ else {
if (tabOption === 'search') { if (tabOption === 'search') {
setTabOption('open') setTabOption('open')
@ -119,11 +120,21 @@ const TicketActionButtons = ({ ticket, statusChatEnd }) => {
toastError(err); toastError(err);
} }
}; };
const handleContactToggleUseDialogflow = async () => {
setLoading(true);
try {
const contact = await api.put(`/contacts/toggleUseDialogflow/${ticket.contact.id}`);
setUseDialogflow(contact.data.useDialogflow);
setLoading(false);
} catch (err) {
setLoading(false);
toastError(err);
}
};
return ( return (
<div className={classes.actionButtons}> <div className={classes.actionButtons}>
{ticket.status === "closed" && ( {ticket.status === "closed" && (

View File

@ -0,0 +1,16 @@
function getConfig(name, defaultValue=null) {
// If inside a docker container, use window.ENV
if( window.ENV !== undefined ) {
return window.ENV[name] || defaultValue;
}
return process.env[name] || defaultValue;
}
export function getBackendUrl() {
return getConfig('REACT_APP_BACKEND_URL');
}
export function getHoursCloseTicketsAuto() {
return getConfig('REACT_APP_HOURS_CLOSE_TICKETS_AUTO');
}

View File

@ -0,0 +1,12 @@
import api from "../../services/api";
const useDialogflows = () => {
const findAll = async () => {
const { data } = await api.get("/dialogflow");
return data;
}
return { findAll };
};
export default useDialogflows;

View File

@ -1,6 +1,7 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import DeviceHubOutlined from "@material-ui/icons/DeviceHubOutlined"
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon"; import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText"; import ListItemText from "@material-ui/core/ListItemText";
@ -130,11 +131,18 @@ const MainListItems = (props) => {
role={user.profile} role={user.profile}
perform="settings-view:show" perform="settings-view:show"
yes={() => ( yes={() => (
<>
<ListItemLink <ListItemLink
to="/settings" to="/settings"
primary={i18n.t("mainDrawer.listItems.settings")} primary={i18n.t("mainDrawer.listItems.settings")}
icon={<SettingsOutlinedIcon />} icon={<SettingsOutlinedIcon />}
/> />
<ListItemLink
to="/Dialogflows"
primary={i18n.t("mainDrawer.listItems.dialogflow")}
icon={<DeviceHubOutlined />}
/>
</>
)} )}
/> />
</> </>

View File

@ -0,0 +1,221 @@
import { format, parseISO } from "date-fns";
import React, { useReducer, useState } from "react";
import {
Button,
IconButton,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
TableHead,
TableRow
} from "@material-ui/core";
import { DeleteOutline, Edit } from "@material-ui/icons";
import { toast } from "react-toastify";
import ConfirmationModal from "../../components/ConfirmationModal";
import DialogflowModal from "../../components/DialogflowModal";
import MainContainer from "../../components/MainContainer";
import MainHeader from "../../components/MainHeader";
import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper";
import TableRowSkeleton from "../../components/TableRowSkeleton";
import Title from "../../components/Title";
import toastError from "../../errors/toastError";
import api from "../../services/api";
import { i18n } from "../../translate/i18n";
import useLoadData from "../Queues/useLoadData.js";
import useSocket from "../Queues/useSocket";
const useStyles = makeStyles((theme) => ({
mainPaper: {
flex: 1,
padding: theme.spacing(1),
overflowY: "scroll",
...theme.scrollbarStyles,
},
customTableCell: {
display: "flex",
alignItems: "center",
justifyContent: "center",
},
}));
const reducer = (state, action) => {
if (action.type === "LOAD_DIALOGFLOWS") {
const dialoflows = action.payload;
const newDialogflows = [];
dialoflows.forEach((dialogflow) => {
const dialogflowIndex = state.findIndex((q) => q.id === dialogflow.id);
if (dialogflowIndex !== -1) {
state[dialogflowIndex] = dialogflow;
} else {
newDialogflows.push(dialogflow);
}
});
return [...state, ...newDialogflows];
}
if (action.type === "UPDATE_DIALOGFLOWS") {
const dialogflow = action.payload;
const dialogflowIndex = state.findIndex((u) => u.id === dialogflow.id);
if (dialogflowIndex !== -1) {
state[dialogflowIndex] = dialogflow;
return [...state];
} else {
return [dialogflow, ...state];
}
}
if (action.type === "DELETE_DIALOGFLOW") {
const dialogflowId = action.payload;
const dialogflowIndex = state.findIndex((q) => q.id === dialogflowId);
if (dialogflowIndex !== -1) {
state.splice(dialogflowIndex, 1);
}
return [...state];
}
if (action.type === "RESET") {
return [];
}
};
const Dialogflows = () => {
const classes = useStyles();
const [dialogflows, dispatch] = useReducer(reducer, []);
const [loading, setLoading] = useState(false);
const [dialogflowModalOpen, setDialogflowModalOpen] = useState(false);
const [selectedDialogflow, setSelectedDialogflow] = useState(null);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
useLoadData(setLoading, dispatch, '/dialogflow', 'LOAD_DIALOGFLOWS');
useSocket(dispatch, 'dialogflow', 'UPDATE_DIALOGFLOWS', 'DELETE_DIALOGFLOW', 'dialogflow', 'dialogflowId');
const handleOpenDialogflowModal = () => {
setDialogflowModalOpen(true);
setSelectedDialogflow(null);
};
const handleCloseDialogflowModal = () => {
setDialogflowModalOpen(false);
setSelectedDialogflow(null);
};
const handleEditDialogflow = (dialogflow) => {
setSelectedDialogflow(dialogflow);
setDialogflowModalOpen(true);
};
const handleCloseConfirmationModal = () => {
setConfirmModalOpen(false);
setSelectedDialogflow(null);
};
const handleDeleteDialogflow = async (dialogflowId) => {
try {
await api.delete(`/dialogflow/${dialogflowId}`);
toast.success(i18n.t("Dialogflow deleted successfully!"));
} catch (err) {
toastError(err);
}
setSelectedDialogflow(null);
};
return (
<MainContainer>
<ConfirmationModal
title={
selectedDialogflow &&
`${i18n.t("dialogflows.confirmationModal.deleteTitle")} ${
selectedDialogflow.name
}?`
}
open={confirmModalOpen}
onClose={handleCloseConfirmationModal}
onConfirm={() => handleDeleteDialogflow(selectedDialogflow.id)}
>
{i18n.t("dialogflows.confirmationModal.deleteMessage")}
</ConfirmationModal>
<DialogflowModal
open={dialogflowModalOpen}
onClose={handleCloseDialogflowModal}
dialogflowId={selectedDialogflow?.id}
/>
<MainHeader>
<Title>{i18n.t("dialogflows.title")}</Title>
<MainHeaderButtonsWrapper>
<Button
variant="contained"
color="primary"
onClick={handleOpenDialogflowModal}
>
{i18n.t("dialogflows.buttons.add")}
</Button>
</MainHeaderButtonsWrapper>
</MainHeader>
<Paper className={classes.mainPaper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="center">
{i18n.t("dialogflows.table.name")}
</TableCell>
<TableCell align="center">
{i18n.t("dialogflows.table.projectName")}
</TableCell>
<TableCell align="center">
{i18n.t("dialogflows.table.language")}
</TableCell>
<TableCell align="center">
{i18n.t("dialogflows.table.lastUpdate")}
</TableCell>
<TableCell align="center">
{i18n.t("dialogflows.table.actions")}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<>
{dialogflows.map((dialogflow) => (
<TableRow key={dialogflow.id}>
<TableCell align="center">{dialogflow.name}</TableCell>
<TableCell align="center">{ dialogflow.projectName}</TableCell>
<TableCell align="center">{dialogflow.language}</TableCell>
<TableCell align="center">
{format(parseISO(dialogflow.updatedAt), "dd/MM/yy HH:mm")}
</TableCell>
<TableCell align="center">
<IconButton
size="small"
onClick={() => handleEditDialogflow(dialogflow)}
><Edit />
</IconButton>
<IconButton
size="small"
onClick={() => {
setSelectedDialogflow(dialogflow);
setConfirmModalOpen(true);
}}
>
<DeleteOutline />
</IconButton>
</TableCell>
</TableRow>
))}
{loading && <TableRowSkeleton columns={4} />}
</>
</TableBody>
</Table>
</Paper>
</MainContainer>
);
};
export default Dialogflows;

View File

@ -0,0 +1,22 @@
import { useEffect } from "react";
import toastError from "../../errors/toastError";
import api from "../../services/api";
const useLoadData = (setLoading, dispatch, route, dispatchType) => {
useEffect(() => {
(async () => {
setLoading(true);
try {
const { data } = await api.get(route);
dispatch({ type: dispatchType, payload: data });
setLoading(false);
} catch (err) {
toastError(err);
setLoading(false);
}
})();
}, []);
}
export default useLoadData;

View File

@ -0,0 +1,24 @@
import { useEffect } from "react";
import openSocket from "../../services/socket-io";
const useSocket = (dispatch, socketEvent, typeUpdate, typeDelete, payloadUpdate, payloadDelete) => {
useEffect(() => {
const socket = openSocket();
socket.on(socketEvent, (data) => {
if (data.action === "update" || data.action === "create") {
dispatch({ type: typeUpdate, payload: data[payloadUpdate] });
}
if (data.action === "delete") {
dispatch({ type: typeDelete, payload: data[payloadDelete] });
}
});
return () => {
socket.disconnect();
};
}, []);
}
export default useSocket;

View File

@ -21,7 +21,7 @@ import { AuthProvider } from "../context/Auth/AuthContext";
import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext";
import Route from "./Route"; import Route from "./Route";
import Dialogflows from "../pages/Dialogflow/";
const Routes = () => { const Routes = () => {
return ( return (
@ -47,6 +47,7 @@ const Routes = () => {
<Route exact path="/quickAnswers" component={QuickAnswers} isPrivate /> <Route exact path="/quickAnswers" component={QuickAnswers} isPrivate />
<Route exact path="/Settings" component={Settings} isPrivate /> <Route exact path="/Settings" component={Settings} isPrivate />
<Route exact path="/Queues" component={Queues} isPrivate /> <Route exact path="/Queues" component={Queues} isPrivate />
<Route exact path="/Dialogflows" component={Dialogflows} isPrivate />
</LoggedInLayout> </LoggedInLayout>
</WhatsAppsProvider> </WhatsAppsProvider>
</Switch> </Switch>

View File

@ -0,0 +1,8 @@
import openSocket from "socket.io-client";
import { getBackendUrl } from "../config";
function connectToSocket() {
return openSocket(getBackendUrl());
}
export default connectToSocket;

View File

@ -155,6 +155,7 @@ const messages = {
email: "Email", email: "Email",
extraName: "Field name", extraName: "Field name",
extraValue: "Value", extraValue: "Value",
dialogflow: "Dialogflow",
}, },
buttons: { buttons: {
addExtraInfo: "Add information", addExtraInfo: "Add information",
@ -188,6 +189,7 @@ const messages = {
form: { form: {
name: "Name", name: "Name",
color: "Color", color: "Color",
dialogflow: "Dialogflow",
greetingMessage: "Greeting Message", greetingMessage: "Greeting Message",
}, },
buttons: { buttons: {
@ -196,6 +198,29 @@ const messages = {
cancel: "Cancel", cancel: "Cancel",
}, },
}, },
dialogflowModal: {
title: {
add: "Add Project",
edit: "Edit Project",
},
form: {
name: "Name",
projectName: "Project Name",
language: "Language",
jsonContent: "JsonContent"
},
buttons: {
okAdd: "Add",
okEdit: "Save",
cancel: "Cancel",
test: "Bot Test",
},
messages: {
testSuccess: "Dialogflow test successfully",
addSuccess: "Dialogflow added successfully",
editSuccess: "Dialogflow edited successfully",
}
},
userModal: { userModal: {
title: { title: {
add: "Add user", add: "Add user",
@ -206,6 +231,7 @@ const messages = {
email: "Email", email: "Email",
password: "Password", password: "Password",
profile: "Profile", profile: "Profile",
whatsapp: "Default Connection",
}, },
buttons: { buttons: {
okAdd: "Add", okAdd: "Add",
@ -248,7 +274,9 @@ const messages = {
title: "Transfer Ticket", title: "Transfer Ticket",
fieldLabel: "Type to search for users", fieldLabel: "Type to search for users",
fieldQueueLabel: "Transfer to queue", fieldQueueLabel: "Transfer to queue",
fieldConnectionLabel: "Transfer to connection",
fieldQueuePlaceholder: "Please select a queue", fieldQueuePlaceholder: "Please select a queue",
fieldConnectionPlaceholder: "Please select a connection",
noOptions: "No user found with this name", noOptions: "No user found with this name",
buttons: { buttons: {
ok: "Transfer", ok: "Transfer",
@ -260,6 +288,7 @@ const messages = {
assignedHeader: "Working on", assignedHeader: "Working on",
noTicketsTitle: "Nothing here!", noTicketsTitle: "Nothing here!",
noTicketsMessage: "No tickets found with this status or search term.", noTicketsMessage: "No tickets found with this status or search term.",
connectionTitle: "Connection that is currently being used.",
buttons: { buttons: {
accept: "Accept", accept: "Accept",
}, },
@ -284,6 +313,7 @@ const messages = {
administration: "Administration", administration: "Administration",
users: "Users", users: "Users",
settings: "Settings", settings: "Settings",
dialogflow: "Dialogflow",
}, },
appBar: { appBar: {
user: { user: {
@ -312,6 +342,24 @@ const messages = {
"Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.", "Are you sure? It cannot be reverted! Tickets in this queue will still exist, but will not have any queues assigned.",
}, },
}, },
dialogflows: {
title: "Dialogflow",
table: {
name: "Name",
projectName: "Project Name",
lamguage: "Language",
lastUpdate:"Last Update",
actions: "Actions",
},
buttons: {
add: "Add Project",
},
confirmationModal: {
deleteTitle: "Delete",
deleteMessage:
"Are you sure? It cannot be reverted!",
},
},
queueSelect: { queueSelect: {
inputLabel: "Queues", inputLabel: "Queues",
}, },
@ -340,6 +388,7 @@ const messages = {
name: "Name", name: "Name",
email: "Email", email: "Email",
profile: "Profile", profile: "Profile",
whatsapp: "Default Connection",
actions: "Actions", actions: "Actions",
}, },
buttons: { buttons: {
@ -371,6 +420,7 @@ const messages = {
header: { header: {
assignedTo: "Assigned to:", assignedTo: "Assigned to:",
buttons: { buttons: {
dialogflow: "Dialogflow",
return: "Return", return: "Return",
resolve: "Resolve", resolve: "Resolve",
reopen: "Reopen", reopen: "Reopen",
@ -392,6 +442,7 @@ const messages = {
}, },
ticketOptionsMenu: { ticketOptionsMenu: {
delete: "Delete", delete: "Delete",
useQueues: "¿Usar colas?",
transfer: "Transfer", transfer: "Transfer",
confirmationModal: { confirmationModal: {
title: "Delete ticket #", title: "Delete ticket #",
@ -453,6 +504,9 @@ const messages = {
"This color is already in use, pick another one.", "This color is already in use, pick another one.",
ERR_WAPP_GREETING_REQUIRED: ERR_WAPP_GREETING_REQUIRED:
"Greeting message is required if there is more than one queue.", "Greeting message is required if there is more than one queue.",
ERR_NO_DIALOG_FOUND: "No Dialogflow found with this ID.",
ERR_TEST_SESSION_DIALOG: "Error creating DialogFlow session",
ERR_TEST_REPLY_DIALOG: "Error testing DialogFlow configuration",
}, },
}, },
}, },

View File

@ -158,6 +158,7 @@ const messages = {
email: "Correo Electrónico", email: "Correo Electrónico",
extraName: "Nombre del Campo", extraName: "Nombre del Campo",
extraValue: "Valor", extraValue: "Valor",
dialogflow: "Dialogflow",
}, },
buttons: { buttons: {
addExtraInfo: "Agregar información", addExtraInfo: "Agregar información",
@ -191,6 +192,7 @@ const messages = {
form: { form: {
name: "Nombre", name: "Nombre",
color: "Color", color: "Color",
dialogflow: "Dialogflow",
greetingMessage: "Mensaje de saludo", greetingMessage: "Mensaje de saludo",
}, },
buttons: { buttons: {
@ -199,6 +201,29 @@ const messages = {
cancel: "Cancelar", cancel: "Cancelar",
}, },
}, },
dialogflowModal: {
title: {
add: "Agregar cola",
edit: "Editar cola",
},
form: {
name: "Nombre",
projectName: "Nombre del proyecto",
language: "Idioma",
jsonContent: "JsonContent",
},
buttons: {
okAdd: "Añadir",
okEdit: "Ahorrar",
cancel: "Cancelar",
test: "Testar Bot",
},
messages: {
testSuccess: "Prueba Dialogflow con éxito",
addSuccess: "Dialogflow agregado con éxito",
editSuccess: "Dialogflow editado con éxito",
}
},
userModal: { userModal: {
title: { title: {
add: "Agregar usuario", add: "Agregar usuario",
@ -209,6 +234,7 @@ const messages = {
email: "Correo Electrónico", email: "Correo Electrónico",
password: "Contraseña", password: "Contraseña",
profile: "Perfil", profile: "Perfil",
whatsapp: "Conexión estándar",
}, },
buttons: { buttons: {
okAdd: "Agregar", okAdd: "Agregar",
@ -251,7 +277,9 @@ const messages = {
title: "Transferir Ticket", title: "Transferir Ticket",
fieldLabel: "Escriba para buscar usuarios", fieldLabel: "Escriba para buscar usuarios",
fieldQueueLabel: "Transferir a la cola", fieldQueueLabel: "Transferir a la cola",
fieldConnectionLabel: "Transferir to conexión",
fieldQueuePlaceholder: "Seleccione una cola", fieldQueuePlaceholder: "Seleccione una cola",
fieldConnectionPlaceholder: "Seleccione una conexión",
noOptions: "No se encontraron usuarios con ese nombre", noOptions: "No se encontraron usuarios con ese nombre",
buttons: { buttons: {
ok: "Transferir", ok: "Transferir",
@ -262,6 +290,7 @@ const messages = {
pendingHeader: "Cola", pendingHeader: "Cola",
assignedHeader: "Trabajando en", assignedHeader: "Trabajando en",
noTicketsTitle: "¡Nada acá!", noTicketsTitle: "¡Nada acá!",
connectionTitle: "Conexión que se está utilizando actualmente.",
noTicketsMessage: noTicketsMessage:
"No se encontraron tickets con este estado o término de búsqueda", "No se encontraron tickets con este estado o término de búsqueda",
buttons: { buttons: {
@ -280,6 +309,7 @@ const messages = {
mainDrawer: { mainDrawer: {
listItems: { listItems: {
dashboard: "Dashboard", dashboard: "Dashboard",
connections: "Conexiones", connections: "Conexiones",
tickets: "Tickets", tickets: "Tickets",
contacts: "Contactos", contacts: "Contactos",
@ -288,6 +318,7 @@ const messages = {
administration: "Administración", administration: "Administración",
users: "Usuarios", users: "Usuarios",
settings: "Configuración", settings: "Configuración",
dialogflow: "Dialogflow",
}, },
appBar: { appBar: {
user: { user: {
@ -316,6 +347,24 @@ const messages = {
"¿Estás seguro? ¡Esta acción no se puede revertir! Los tickets en esa cola seguirán existiendo, pero ya no tendrán ninguna cola asignada.", "¿Estás seguro? ¡Esta acción no se puede revertir! Los tickets en esa cola seguirán existiendo, pero ya no tendrán ninguna cola asignada.",
}, },
}, },
dialoflows: {
title: "Dialogflow",
table: {
name: "Nombre",
projectName: "Nombre del proyecto",
language: "Idioma",
lastUpdate: "Última Actualización",
actions: "Comportamiento",
},
buttons: {
add: "Agregar proyecto",
},
confirmationModal: {
deleteTitle: "Eliminar",
deleteMessage:
"¿Estás seguro? ¡Esta acción no se puede revertir!",
},
},
queueSelect: { queueSelect: {
inputLabel: "Linhas", inputLabel: "Linhas",
}, },
@ -345,6 +394,7 @@ const messages = {
name: "Nombre", name: "Nombre",
email: "Correo Electrónico", email: "Correo Electrónico",
profile: "Perfil", profile: "Perfil",
whatsapp: "Conexión estándar",
actions: "Acciones", actions: "Acciones",
}, },
buttons: { buttons: {
@ -376,6 +426,7 @@ const messages = {
header: { header: {
assignedTo: "Asignado a:", assignedTo: "Asignado a:",
buttons: { buttons: {
dialogflow: "Dialogflow",
return: "Devolver", return: "Devolver",
resolve: "Resolver", resolve: "Resolver",
reopen: "Reabrir", reopen: "Reabrir",
@ -398,6 +449,7 @@ const messages = {
}, },
ticketOptionsMenu: { ticketOptionsMenu: {
delete: "Borrar", delete: "Borrar",
useQueues: "Use Queues",
transfer: "Transferir", transfer: "Transferir",
confirmationModal: { confirmationModal: {
title: "¿Borrar ticket #", title: "¿Borrar ticket #",
@ -460,6 +512,9 @@ const messages = {
"Este color ya está en uso, elija otro.", "Este color ya está en uso, elija otro.",
ERR_WAPP_GREETING_REQUIRED: ERR_WAPP_GREETING_REQUIRED:
"El mensaje de saludo es obligatorio cuando hay más de una cola.", "El mensaje de saludo es obligatorio cuando hay más de una cola.",
ERR_NO_DIALOG_FOUND: "No se encontró Dialogflow con este ID.",
ERR_TEST_SESSION_DIALOG: "Error al crear la sesión de dialogflow",
ERR_TEST_REPLY_DIALOG: "Error al probar la configuración de DialogFlow",
}, },
}, },
}, },

View File

@ -80,8 +80,7 @@ const messages = {
qrcode: { qrcode: {
title: "Esperando leitura do QR Code", title: "Esperando leitura do QR Code",
content: content:
// "Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão", "Clique no botão 'QR CODE' e leia o QR Code com o seu celular para iniciar a sessão",
"Entre em contato com o suporte para realizar a leitura do QR code",
}, },
connected: { connected: {
title: "Conexão estabelecida!", title: "Conexão estabelecida!",
@ -158,6 +157,7 @@ const messages = {
email: "Email", email: "Email",
extraName: "Nome do campo", extraName: "Nome do campo",
extraValue: "Valor", extraValue: "Valor",
dialogflow: "Dialogflow",
}, },
buttons: { buttons: {
addExtraInfo: "Adicionar informação", addExtraInfo: "Adicionar informação",
@ -191,6 +191,7 @@ const messages = {
form: { form: {
name: "Nome", name: "Nome",
color: "Cor", color: "Cor",
dialogflow: "Dialogflow",
greetingMessage: "Mensagem de saudação", greetingMessage: "Mensagem de saudação",
}, },
buttons: { buttons: {
@ -199,6 +200,29 @@ const messages = {
cancel: "Cancelar", cancel: "Cancelar",
}, },
}, },
dialogflowModal: {
title: {
add: "Adicionar projeto",
edit: "Editar projeto",
},
form: {
name: "Nome",
projectName: "Nome do Projeto",
language: "Linguagem",
jsonContent: "JsonContent",
},
buttons: {
okAdd: "Adicionar",
okEdit: "Salvar",
cancel: "Cancelar",
test: "Testar Bot",
},
messages: {
testSuccess: "Dialogflow testado com sucesso!",
addSuccess: "Dialogflow adicionado com sucesso.",
editSuccess: "Dialogflow editado com sucesso.",
}
},
userModal: { userModal: {
title: { title: {
add: "Adicionar usuário", add: "Adicionar usuário",
@ -209,6 +233,7 @@ const messages = {
email: "Email", email: "Email",
password: "Senha", password: "Senha",
profile: "Perfil", profile: "Perfil",
whatsapp: "Conexão Padrão",
}, },
buttons: { buttons: {
okAdd: "Adicionar", okAdd: "Adicionar",
@ -241,7 +266,7 @@ const messages = {
search: { title: "Busca" }, search: { title: "Busca" },
}, },
search: { search: {
placeholder: "Busca telefone/nome", placeholder: "Buscar tickets e mensagens",
}, },
buttons: { buttons: {
showAll: "Todos", showAll: "Todos",
@ -251,7 +276,9 @@ const messages = {
title: "Transferir Ticket", title: "Transferir Ticket",
fieldLabel: "Digite para buscar usuários", fieldLabel: "Digite para buscar usuários",
fieldQueueLabel: "Transferir para fila", fieldQueueLabel: "Transferir para fila",
fieldConnectionLabel: "Transferir para conexão",
fieldQueuePlaceholder: "Selecione uma fila", fieldQueuePlaceholder: "Selecione uma fila",
fieldConnectionPlaceholder: "Selecione uma conexão",
noOptions: "Nenhum usuário encontrado com esse nome", noOptions: "Nenhum usuário encontrado com esse nome",
buttons: { buttons: {
ok: "Transferir", ok: "Transferir",
@ -264,6 +291,7 @@ const messages = {
noTicketsTitle: "Nada aqui!", noTicketsTitle: "Nada aqui!",
noTicketsMessage: noTicketsMessage:
"Nenhum ticket encontrado com esse status ou termo pesquisado", "Nenhum ticket encontrado com esse status ou termo pesquisado",
connectionTitle: "Conexão que está sendo utilizada atualmente.",
buttons: { buttons: {
accept: "Aceitar", accept: "Aceitar",
}, },
@ -288,6 +316,7 @@ const messages = {
administration: "Administração", administration: "Administração",
users: "Usuários", users: "Usuários",
settings: "Configurações", settings: "Configurações",
dialogflow: "Dialogflow",
}, },
appBar: { appBar: {
user: { user: {
@ -316,6 +345,24 @@ const messages = {
"Você tem certeza? Essa ação não pode ser revertida! Os tickets dessa fila continuarão existindo, mas não terão mais nenhuma fila atribuída.", "Você tem certeza? Essa ação não pode ser revertida! Os tickets dessa fila continuarão existindo, mas não terão mais nenhuma fila atribuída.",
}, },
}, },
dialogflows: {
title: "Dialogflow",
table: {
name: "Nome",
projectName: "Nome do Projeto",
language: "Linguagem",
lastUpdate: "Ultima atualização",
actions: "Ações",
},
buttons: {
add: "Adicionar Projeto",
},
confirmationModal: {
deleteTitle: "Excluir",
deleteMessage:
"Você tem certeza? Essa ação não pode ser revertida! e será removida das filas e conexões vinculadas",
},
},
queueSelect: { queueSelect: {
inputLabel: "Filas", inputLabel: "Filas",
}, },
@ -345,6 +392,7 @@ const messages = {
name: "Nome", name: "Nome",
email: "Email", email: "Email",
profile: "Perfil", profile: "Perfil",
whatsapp: "Conexão Padrão",
actions: "Ações", actions: "Ações",
}, },
buttons: { buttons: {
@ -376,6 +424,7 @@ const messages = {
header: { header: {
assignedTo: "Atribuído à:", assignedTo: "Atribuído à:",
buttons: { buttons: {
dialogflow: "Dialogflow",
return: "Retornar", return: "Retornar",
resolve: "Resolver", resolve: "Resolver",
reopen: "Reabrir", reopen: "Reabrir",
@ -398,6 +447,7 @@ const messages = {
}, },
ticketOptionsMenu: { ticketOptionsMenu: {
delete: "Deletar", delete: "Deletar",
useQueues: "Usar fila?",
transfer: "Transferir", transfer: "Transferir",
confirmationModal: { confirmationModal: {
title: "Deletar o ticket do contato", title: "Deletar o ticket do contato",
@ -458,6 +508,9 @@ const messages = {
"Esta cor já está em uso, escolha outra.", "Esta cor já está em uso, escolha outra.",
ERR_WAPP_GREETING_REQUIRED: ERR_WAPP_GREETING_REQUIRED:
"A mensagem de saudação é obrigatório quando há mais de uma fila.", "A mensagem de saudação é obrigatório quando há mais de uma fila.",
ERR_NO_DIALOG_FOUND: "Nenhuma Dialogflow encontrado com este ID",
ERR_TEST_SESSION_DIALOG: "Erro ao criar sessão do DialogFlow",
ERR_TEST_REPLY_DIALOG: "Erro ao testar configuração do DialogFlow",
}, },
}, },
}, },