Commit bd7065b6 authored by henrik.prangel's avatar henrik.prangel
Browse files

JUT-58 (PART 1) Add functionality to extract "intent data" from validated...

JUT-58 (PART 1) Add functionality to extract "intent data" from validated chats and save it into the model
parent a6de8030
......@@ -5,12 +5,14 @@ import { INSTITUTION_NAMES } from "app/modules/training-interface/training-inter
import { AUTHORITIES } from "app/config/constants";
export const ACTION_TYPES = {
FETCH_INTENTS_LIST: 'intents/FETCH_INTENTS_LIST',
FETCH_NLU_FILE: 'intents/FETCH_NLU_FILE',
FETCH_STORIES_FILE: 'intents/FETCH_STORIES_FILE',
UPDATE_NLU_FILE: 'intents/UPDATE_NLU_FILE',
UPDATE_STORIES_FILE: 'intents/UPDATE_STORIES_FILE',
UPDATE_EDITOR_FILE: 'intents/UPDATE_EDITOR_FILE',
GET_INTENTS_LIST: 'intents/GET_INTENTS_LIST',
POST_NLU_FILE: 'intents/POST_NLU_FILE',
POST_STORIES_FILE: 'intents/POST_STORIES_FILE',
SET_CURRENTLY_ACTIVE_FILE: 'intents/SET_CURRENTLY_ACTIVE_FILE',
};
export const RASA_FILE_TYPES = {
......@@ -22,11 +24,13 @@ export const RASA_FILE_TYPES = {
const initialState = {
loading: false,
errorMessage: null,
editorFile: {text: [], type: RASA_FILE_TYPES.NLU},
intentList: [],
updating: false,
updateSuccess: false,
errorMessage: null,
currentlyActiveFile: RASA_FILE_TYPES.NLU,
nluFile: [],
storiesFile: [],
intentList: [],
};
export type IntentsState = Readonly<typeof initialState>;
......@@ -37,17 +41,26 @@ export default (state: IntentsState = initialState, action): IntentsState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.FETCH_STORIES_FILE):
case REQUEST(ACTION_TYPES.FETCH_NLU_FILE):
return {
...state,
errorMessage: null,
updateSuccess: false,
loading: true,
nluFile: [],
};
case REQUEST(ACTION_TYPES.POST_NLU_FILE):
case REQUEST(ACTION_TYPES.POST_STORIES_FILE):
return {
...state,
errorMessage: null,
updateSuccess: false,
updating: true,
editorFile: {text: [], type: RASA_FILE_TYPES.NLU}
};
case FAILURE(ACTION_TYPES.FETCH_STORIES_FILE):
case FAILURE(ACTION_TYPES.FETCH_NLU_FILE):
case FAILURE(ACTION_TYPES.UPDATE_NLU_FILE):
case FAILURE(ACTION_TYPES.UPDATE_STORIES_FILE):
case FAILURE(ACTION_TYPES.POST_NLU_FILE):
case FAILURE(ACTION_TYPES.POST_STORIES_FILE):
case FAILURE(ACTION_TYPES.FETCH_INTENTS_LIST):
return {
...state,
loading: false,
......@@ -55,31 +68,48 @@ export default (state: IntentsState = initialState, action): IntentsState => {
updateSuccess: false,
errorMessage: action.payload,
};
case SUCCESS(ACTION_TYPES.POST_NLU_FILE):
case SUCCESS(ACTION_TYPES.POST_STORIES_FILE): {
return {
...state,
updateSuccess: true,
updating: false
};
}
case SUCCESS(ACTION_TYPES.FETCH_STORIES_FILE): {
return {
...state,
loading: false,
editorFile: {text: action.payload.data, type: RASA_FILE_TYPES.STORIES},
storiesFile: action.payload.data,
};
}
case SUCCESS(ACTION_TYPES.FETCH_NLU_FILE): {
return {
...state,
loading: false,
editorFile: {text: action.payload.data, type: RASA_FILE_TYPES.NLU},
nluFile: action.payload.data
};
}
case SUCCESS(ACTION_TYPES.GET_INTENTS_LIST): {
case SUCCESS(ACTION_TYPES.FETCH_INTENTS_LIST): {
return {
...state,
loading: false,
intentList: action.payload.data,
};
}
case ACTION_TYPES.UPDATE_EDITOR_FILE:
case ACTION_TYPES.UPDATE_NLU_FILE:
return {
...state,
nluFile: action.payload
}
case ACTION_TYPES.UPDATE_STORIES_FILE:
return {
...state,
storiesFile: action.payload
}
case ACTION_TYPES.SET_CURRENTLY_ACTIVE_FILE:
return {
...state,
editorFile: action.payload
currentlyActiveFile: action.payload
}
default:
return state;
......@@ -113,6 +143,15 @@ const getBotUrlByUserAuthorities = (userAuthorities, specifier) => {
// Actions
export const getIntentsList = userAuthorities => async dispatch => {
const requestUrl = getBotUrlByUserAuthorities(userAuthorities, "intents");
return await dispatch({
type: ACTION_TYPES.FETCH_INTENTS_LIST,
payload: axios.get(`${requestUrl}?cacheBuster=${new Date().getTime()}`, {headers: {['Content-Type']: 'text/plain'}}),
});
};
export const getFile = (userAuthorities, fileType, callback) => async dispatch => {
const requestUrl = getBotUrlByUserAuthorities(userAuthorities, fileType);
let actionType
......@@ -136,51 +175,62 @@ export const getFile = (userAuthorities, fileType, callback) => async dispatch =
});
};
export const getIntentsList = userAuthorities => async dispatch => {
const requestUrl = getBotUrlByUserAuthorities(userAuthorities, "intents");
export const postNLUFile = (userAuthorities, nluFile) => {
const requestUrl = getBotUrlByUserAuthorities(userAuthorities, RASA_FILE_TYPES.NLU);
return await dispatch({
type: ACTION_TYPES.GET_INTENTS_LIST,
payload: axios.get(`${requestUrl}?cacheBuster=${new Date().getTime()}`, {headers: {['Content-Type']: 'text/plain'}}),
});
return {
type: ACTION_TYPES.POST_NLU_FILE,
payload: axios.post(requestUrl, nluFile),
};
};
export const postStoriesFile = (userAuthorities, storiesFile) => {
const requestUrl = getBotUrlByUserAuthorities(userAuthorities, RASA_FILE_TYPES.STORIES);
return {
type: ACTION_TYPES.POST_STORIES_FILE,
payload: axios.post(requestUrl, storiesFile, {headers: {['Content-Type']: 'text/plain'}}),
};
};
export const postCurrentEditorFile = (userAuthorities, text, fileType) => {
// let requestUrl;
// let type;
// let payload
//
// switch (fileType) {
// case RASA_FILE_TYPES.STORIES:
// requestUrl = getBotUrlByUserAuthorities(userAuthorities, fileType);
// type = ACTION_TYPES.UPDATE_STORIES_FILE;
// payload = axios.post(requestUrl, text, {headers: {['Content-Type']: 'text/plain'}})
// break
// case RASA_FILE_TYPES.NLU:
// requestUrl = getBotUrlByUserAuthorities(userAuthorities, fileType);
// type = ACTION_TYPES.UPDATE_NLU_FILE
// payload = axios.post(requestUrl, text)
// break
// default:
// return
// }
//
// return {
// type,
// payload,
// };
export const updateNLUFile = (intent, intentExampleList, nluFile) => {
const newNLUFile = [...nluFile]
const intentIndexInArray = newNLUFile.findIndex((intentInArray) => intentInArray.name === intent);
newNLUFile[intentIndexInArray].examples = intentExampleList
return {
type: ACTION_TYPES.UPDATE_NLU_FILE,
payload: newNLUFile
};
}
export const updateStoriesFile = (storiesFile) => {
return {
type: ACTION_TYPES.UPDATE_STORIES_FILE,
payload: storiesFile
};
};
export const updateEditorFile = (intent, intentExampleList, editorFile) => {
const newEditorFile = {...editorFile}
const newEditorFileList = [...editorFile.text]
const intentIndexInArray = newEditorFileList.findIndex((intentInArray) => intentInArray.name === intent);
newEditorFileList[intentIndexInArray].examples = intentExampleList
newEditorFile.text = newEditorFileList
export const addNewIntentsToNLUFile = (nluFile, labeledMessagesList) => {
const newNLUFile = [...nluFile]
const unWrappedList = labeledMessagesList.map(labeledMessage => labeledMessage.intentObjects)
const intentObjectList = [].concat(...unWrappedList);
intentObjectList.map(intentObject => {
const intent = newNLUFile.find(intentInFile => intentInFile.name.trim().toLowerCase() === intentObject.name.trim().toLowerCase())
intent.examples.push(intentObject.example)
})
return {
type: ACTION_TYPES.UPDATE_NLU_FILE,
payload: newNLUFile
};
};
export const setCurrentlyActiveFile = (activeFileType) => {
return {
type: ACTION_TYPES.UPDATE_EDITOR_FILE,
payload: newEditorFile
type: ACTION_TYPES.SET_CURRENTLY_ACTIVE_FILE,
payload: activeFileType
};
};
import React, { useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { Button, Input } from 'reactstrap'
import { getFile, postCurrentEditorFile, updateEditorFile } from "app/modules/training-interface/intents/intents.reducer";
import { Translate } from 'react-jhipster';
import {
getFile,
postNLUFile,
postStoriesFile,
setCurrentlyActiveFile,
updateNLUFile,
updateStoriesFile
} from "app/modules/training-interface/intents/intents.reducer";
import { RASA_FILE_TYPES } from "app/modules/training-interface/intents/intents.reducer";
import Avatar from "app/shared/common-components/avatar/avatar";
import '../training-interface.scss'
......@@ -9,7 +17,7 @@ import '../training-interface.scss'
export type IIntentsProp = StateProps;
export const Intents = (props: IIntentsProp) => {
const {editorFile, userAuthorities} = props
const {nluFile, storiesFile, currentlyActiveFile, userAuthorities} = props
const [text, setText] = useState("")
const [newExample, setNewExample] = useState("")
const [activeIntent, setActiveIntent] = useState("")
......@@ -20,20 +28,23 @@ export const Intents = (props: IIntentsProp) => {
dispatch(getFile(userAuthorities, RASA_FILE_TYPES.NLU, () => {
setActiveIntent("")
setIntentExampleList([])
dispatch(setCurrentlyActiveFile(RASA_FILE_TYPES.NLU))
}))
}
const getStoriesFile = () => {
dispatch(getFile(userAuthorities, RASA_FILE_TYPES.STORIES, (value) => {
setText(value)
setActiveIntent("")
dispatch(setCurrentlyActiveFile(RASA_FILE_TYPES.STORIES))
}))
}
const postCurrentlyOpenedFile = () => {
if (editorFile.type === RASA_FILE_TYPES.NLU) {
dispatch(postCurrentEditorFile(userAuthorities, editorFile.text, editorFile.type))
const postCurrentlyActiveFile = () => {
if (currentlyActiveFile === RASA_FILE_TYPES.NLU) {
dispatch(postNLUFile(userAuthorities, nluFile))
} else {
dispatch(postCurrentEditorFile(userAuthorities, text, editorFile.type))
dispatch(postStoriesFile(userAuthorities, storiesFile))
}
}
......@@ -42,10 +53,6 @@ export const Intents = (props: IIntentsProp) => {
setIntentExampleList(intent.examples)
}
const handleChange = (event) => {
setText(event.target.value)
}
const handleIntentChange = (event, intent) => {
event.preventDefault();
const newIntentExampleList = [...intentExampleList];
......@@ -59,7 +66,17 @@ export const Intents = (props: IIntentsProp) => {
const handleSubmit = (event) => {
event.preventDefault();
dispatch(updateEditorFile(activeIntent, intentExampleList, editorFile))
switch (currentlyActiveFile) {
case RASA_FILE_TYPES.NLU:
dispatch(updateNLUFile(activeIntent, intentExampleList, nluFile))
break;
case RASA_FILE_TYPES.STORIES:
dispatch(updateStoriesFile(text))
break;
default:
break;
}
}
const removeExample = (event, example) => {
......@@ -83,10 +100,10 @@ export const Intents = (props: IIntentsProp) => {
return (
<div className="trainer-container">
{editorFile.type === RASA_FILE_TYPES.NLU && (
{currentlyActiveFile === RASA_FILE_TYPES.NLU && (
<div className="room">
<div className="chat-list">
{editorFile.text.map(intent => (
{nluFile.map(intent => (
<div onClick={() => setActiveIntentOnClick(intent)}
className={activeIntent === intent.name ? "chat active" : "chat"}
key={intent.name.toString()}>
......@@ -106,19 +123,21 @@ export const Intents = (props: IIntentsProp) => {
<div className="intent-header">
<div className="intent-name">
<span className="header">
{editorFile.type === RASA_FILE_TYPES.NLU ? "## Intent:" : "## Stories"}
{currentlyActiveFile === RASA_FILE_TYPES.NLU ? "## Intent:" : "## Stories"}
</span>
{activeIntent}
</div>
{editorFile.type === RASA_FILE_TYPES.NLU &&
{currentlyActiveFile === RASA_FILE_TYPES.NLU &&
<div>
<Button type="button">+</Button>
<Button type="submit">Salvesta</Button>
<Button className="save-active-file-button" type="submit">
<Translate contentKey="training.model.saveActiveFile"> Save current active file</Translate>
</Button>
</div>
}
</div>
<div className="background">
{intentExampleList && editorFile.type === RASA_FILE_TYPES.NLU && activeIntent !== "" && (
{intentExampleList && currentlyActiveFile === RASA_FILE_TYPES.NLU && activeIntent !== "" && (
<div>
{intentExampleList.map((example, id) => (
<div key={id} className="single-intent">
......@@ -133,17 +152,36 @@ export const Intents = (props: IIntentsProp) => {
</div>
</div>
)}
{editorFile.type === RASA_FILE_TYPES.STORIES &&
<textarea value={text} onChange={handleChange} className="text-editor"/>
{currentlyActiveFile === RASA_FILE_TYPES.STORIES &&
<textarea value={text} onChange={(event) => setText(event.target.value)} className="text-editor"/>
}
</div>
</form>
<div className="room additional-content">
<div className="room-block">
<Button className="room-button" onClick={getStoriesFile}> Get stories file</Button>
<Button className="room-button" onClick={getNLUFile}>Get NLU file</Button>
<Button className="room-button" onClick={postCurrentlyOpenedFile}>POST file</Button>
<span className="header">
<Translate contentKey="training.model.modelElements">Chatbot model elements</Translate>
</span>
<hr className="divider"/>
<Button className="room-button" onClick={getStoriesFile}>
<Translate contentKey="training.model.getStoriesFile">Get stories file</Translate>
</Button>
<Button className="room-button" onClick={getNLUFile}>
<Translate contentKey="training.model.getNLUFile">Get NLU file</Translate>
</Button>
</div>
<div className="room-block">
<span className="header">
<Translate contentKey="training.model.actions">Actions</Translate>
</span>
<hr className="divider"/>
<Button className="room-button" onClick={postCurrentlyActiveFile}>
<Translate contentKey="training.model.saveChangesInModel">Save changes in model</Translate>
</Button>
<Button className="room-button train-button">
<Translate contentKey="training.model.train">Train</Translate>
</Button>
</div>
</div>
</div>
......@@ -152,7 +190,9 @@ export const Intents = (props: IIntentsProp) => {
const mapStateToProps = storeState => {
return {
editorFile: storeState.trainingInterface.intents.editorFile,
currentlyActiveFile: storeState.trainingInterface.intents.currentlyActiveFile,
storiesFile: storeState.trainingInterface.intents.storiesFile,
nluFile: storeState.trainingInterface.intents.nluFile,
userAuthorities: storeState.authentication.account.authorities
}
}
......
......@@ -5,20 +5,20 @@ import { cleanEntity } from "app/shared/util/entity-utils";
import { IChat } from "app/shared/model/chat.model";
export const ACTION_TYPES = {
SET_ACTIVE_LABELING_CHAT: 'labeling/SET_ACTIVE_LABELING_CHAT',
FETCH_ACTIVE_CHAT_MESSAGE_LIST: 'labeling/FETCH_ACTIVE_CHAT_MESSAGE_LIST',
FETCH_CHATS: 'labeling/FETCH_CHATS',
SET_ACTIVE_LABELING_CHAT: 'labeling/SET_ACTIVE_LABELING_CHAT',
SET_CHAT_AS_LABELED: 'labeling/SET_CHAT_AS_LABELED',
ADD_LABELED_MESSAGE: 'labeling/ADD_LABELED_MESSAGE',
REMOVE_LABELED_MESSAGE: 'labeling/REMOVE_LABELED_MESSAGE',
SET_CHAT_AS_LABELED: 'labeling/SET_CHAT_AS_LABELED',
GET_CHATS: 'labeling/GET_CHATS',
};
const initialState = {
loading: false,
chats: [],
activeLabelingChatId: "",
activeLabelingChatMessages: [],
labeledMessagesList: [],
loading: false,
activeLabelingChatId: "",
errorMessage: ""
};
......@@ -28,14 +28,14 @@ export type LabelingState = Readonly<typeof initialState>;
export default (state: LabelingState = initialState, action): LabelingState => {
switch (action.type) {
case REQUEST(ACTION_TYPES.GET_CHATS):
case REQUEST(ACTION_TYPES.FETCH_CHATS):
case REQUEST(ACTION_TYPES.FETCH_ACTIVE_CHAT_MESSAGE_LIST):
case REQUEST(ACTION_TYPES.SET_CHAT_AS_LABELED):
return {
...state,
loading: true,
};
case FAILURE(ACTION_TYPES.GET_CHATS):
case FAILURE(ACTION_TYPES.FETCH_CHATS):
case FAILURE(ACTION_TYPES.FETCH_ACTIVE_CHAT_MESSAGE_LIST):
case FAILURE(ACTION_TYPES.SET_CHAT_AS_LABELED):
return {
......@@ -43,7 +43,7 @@ export default (state: LabelingState = initialState, action): LabelingState => {
loading: false,
errorMessage: action.payload,
};
case SUCCESS(ACTION_TYPES.GET_CHATS): {
case SUCCESS(ACTION_TYPES.FETCH_CHATS): {
return {
...state,
loading: false,
......@@ -89,7 +89,7 @@ export default (state: LabelingState = initialState, action): LabelingState => {
export const getExpiredChats = (page, size, sort) => async dispatch => {
const requestUrl = `api/chats/not-labeled${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`;
return await dispatch({
type: ACTION_TYPES.GET_CHATS,
type: ACTION_TYPES.FETCH_CHATS,
payload: axios.get(`${requestUrl}${sort ? '&' : '?'}cacheBuster=${new Date().getTime()}`),
});
};
......
import React from 'react';
import React, { useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import { Translate } from 'react-jhipster';
import { RouteComponentProps, Switch, Redirect, useHistory } from 'react-router-dom';
import { Translate } from 'react-jhipster';
import { Button } from 'reactstrap'
import { setActiveLabelingChatId, getLabelingMessagesByChatId, setChatAsLabeled } from './labeling.reducer';
import { addNewIntentsToNLUFile, getFile, RASA_FILE_TYPES, postNLUFile } from '../intents/intents.reducer';
import ErrorBoundaryRoute from "app/shared/error/error-boundary-route";
import ChatList from "app/shared/common-components/chat-list/chat-list";
import TrainingMessages from "app/modules/training-interface/labeling/messages/training-messages";
......@@ -12,16 +13,19 @@ import '../training-interface.scss'
export interface ILabelingProp extends StateProps, RouteComponentProps<{ id: string }> {}
export const Labeling = (props: ILabelingProp) => {
const {expiredChats, activeLabelingChatId, match} = props
const {expiredChats, activeLabelingChatId, labeledMessagesList, nluFile, userAuthorities, match} = props
const dispatch = useDispatch();
const history = useHistory();
useEffect(() => {
dispatch(getFile(userAuthorities, RASA_FILE_TYPES.NLU, () => {}))
}, [])
const setNewActiveLabelingChat = (chatId) => {
dispatch(setActiveLabelingChatId(chatId))
dispatch(getLabelingMessagesByChatId(chatId))
}
const setActiveChatAsLabeled = () => {
const activeChat = expiredChats.find(chat => chat.id === activeLabelingChatId)
dispatch(setChatAsLabeled(activeChat, () => {
......@@ -30,7 +34,8 @@ export const Labeling = (props: ILabelingProp) => {
}
const updateModelAndSetChatAsLabeled = () => {
// TODO: Update model
dispatch(addNewIntentsToNLUFile(nluFile, labeledMessagesList))
dispatch(postNLUFile(userAuthorities, nluFile))
setActiveChatAsLabeled()
}
......@@ -67,8 +72,11 @@ export const Labeling = (props: ILabelingProp) => {
const mapStateToProps = storeState => {
return {
userAuthorities: storeState.authentication.account.authorities,
nluFile: storeState.trainingInterface.intents.nluFile,
expiredChats: storeState.trainingInterface.labeling.chats,
activeLabelingChatId: storeState.trainingInterface.labeling.activeLabelingChatId,
labeledMessagesList: storeState.trainingInterface.labeling.labeledMessagesList
}
}
......
......@@ -106,7 +106,7 @@ export const TrainingMessage = props => {
<div className="message-intents">
{messageIntents[0] && messageIntents[0].name && (
<>
<Translate contentKey="training.intents.name">Intents</Translate>
<Translate contentKey="training.labeling.intents">Intents</Translate>
{messageIntents.map(intentObject => (
<span key={`${intentObject.name}_${intentObject.classes}`} className={intentObject.classes}>
{intentObject.name}
......
......@@ -15,6 +15,13 @@
border-top: 0;
border-radius: var(--general-border-radius);
font-family: 'Roboto', sans-serif;
button {
background: var(--global-button-color-primary);
}
}
.train-button {
background: var(--global-button-color-secondary) !important;
}
.intent-container {
......@@ -30,15 +37,15 @@
flex-flow: column;
height: calc(100% - 55px);
}
button {
.save-active-file-button {
margin-right: 10px;
}
.single-intent {
margin-top: 10px;
display: flex;
button {
margin-left: 5px;
}
}
button {
margin-left: 5px;
}
}
......
......@@ -20,7 +20,7 @@ export const TrainingInterface = ({match}) => {
return (
<div>