From bc13b146afe65177988aced826e687ed70fa705d Mon Sep 17 00:00:00 2001 From: Matt Mazzola Date: Wed, 20 Sep 2017 15:05:15 -0700 Subject: [PATCH] Add ability to select activity in existing log dialog and show related entity, memory, and action (#125) --- public/App.css | 21 ++++++- src/containers/LogDialogAdmin.tsx | 88 +++++++++++++++++++++++++++++ src/containers/LogDialogModal.tsx | 68 ++++++++++++++++++---- src/containers/TrainDialogAdmin.tsx | 2 +- src/containers/Webchat.tsx | 21 ++++++- 5 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 src/containers/LogDialogAdmin.tsx diff --git a/public/App.css b/public/App.css index a187da72..818761d5 100644 --- a/public/App.css +++ b/public/App.css @@ -388,10 +388,10 @@ .teachSessionWindow{ width: 100% } -.teachVariationBox{ +/* .teachVariationBox{ margin-top: 5px; margin-left: 10px; -} +} */ .teachVariation{ overflow: hidden; } @@ -478,7 +478,7 @@ border-width: 1px; border-color: #999999; padding: 10px; - margin-bottom: 10px; + /* margin-bottom: 10px; */ } .extractorResponseBoxInvalid{ border-style: solid; @@ -537,6 +537,7 @@ grid-template-rows: 1fr min-content; } .blis-chatmodal_admin-controls { + padding: 1em; } .blis-chatmodal_modal-controls{ background-color: var(--color-secondary); @@ -563,3 +564,17 @@ .ms-Button--primary:hover { background-color: var(--color-primary-darker) !important; } + + +/** Log Dialog Admin */ +.log-dialog-admin { + display: grid; + grid-gap: 1rem; +} +.log-dialog-admin__title { + font-weight: bold; + border-bottom: 1px solid black; +} +.log-dialog-admin__content { + padding-bottom: 1em; +} \ No newline at end of file diff --git a/src/containers/LogDialogAdmin.tsx b/src/containers/LogDialogAdmin.tsx new file mode 100644 index 00000000..290bad75 --- /dev/null +++ b/src/containers/LogDialogAdmin.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { returntypeof } from 'react-redux-typescript'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { State } from '../types' +import ExtractorResponseEditor from './ExtractorResponseEditor'; +import { Activity, Message } from 'botframework-directlinejs' +import { LogDialog, LogRound, LogScorerStep, ActionBase, EntityBase } from 'blis-models' + +class LogDialogAdmin extends React.Component { + findRoundAndScorerStep(logDialog: LogDialog, activity: Activity): { round: LogRound, scorerStep: LogScorerStep } { + // TODO: Add roundIndex and scoreIndex to activity instead of hiding within id if these are needed as first class properties. + const [roundIndex, scoreIndex] = activity.id.split(":").map(s => parseInt(s)) + + if (roundIndex > logDialog.rounds.length) { + throw new Error(`Index out of range: You are attempting to access round by index: ${roundIndex} but there are only: ${logDialog.rounds.length} rounds.`) + } + + const round = logDialog.rounds[roundIndex] + + if (scoreIndex > round.scorerSteps.length) { + throw new Error(`Index out of range: You are attempting to access scorer step by index: ${scoreIndex} but there are only: ${round.scorerSteps.length} scorere steps.`) + } + + const scorerStep = round.scorerSteps[scoreIndex] + + return { + round, + scorerStep + } + } + + render() { + let round: LogRound = null + let scorerStep: LogScorerStep = null + let action: ActionBase = null + let entities: EntityBase[] = [] + + if (this.props.logDialog && this.props.selectedActivity) { + const result = this.findRoundAndScorerStep(this.props.logDialog, this.props.selectedActivity) + round = result.round + scorerStep = result.scorerStep + action = this.props.actions.find(action => action.actionId == scorerStep.predictedAction) + entities = this.props.entities.filter(entity => scorerStep.input.filledEntities.includes(entity.entityId)) + } + + return ( +
+
Entity Detection
+
+ {round + ? + : "Select an activity"} +
+
Memory
+
+ {entities.length !== 0 && entities.map(entity =>
{entity.entityName}
)} +
+
Action
+
+ {action && action.payload} +
+
+ ); + } +} +const mapDispatchToProps = (dispatch: any) => { + return bindActionCreators({ + }, dispatch); +} +const mapStateToProps = (state: State) => { + return { + actions: state.actions, + entities: state.entities + } +} + +export interface ReceivedProps { + logDialog: LogDialog, + selectedActivity: Activity +} + +// Props types inferred from mapStateToProps & dispatchToProps +const stateProps = returntypeof(mapStateToProps); +const dispatchProps = returntypeof(mapDispatchToProps); +type Props = typeof stateProps & typeof dispatchProps & ReceivedProps; + +export default connect(mapStateToProps, mapDispatchToProps)(LogDialogAdmin); \ No newline at end of file diff --git a/src/containers/LogDialogModal.tsx b/src/containers/LogDialogModal.tsx index 8ab19e1f..c5334924 100644 --- a/src/containers/LogDialogModal.tsx +++ b/src/containers/LogDialogModal.tsx @@ -1,20 +1,48 @@ import * as React from 'react'; import { returntypeof } from 'react-redux-typescript'; -import { editEntityAsync } from '../actions/updateActions'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react'; import { State } from '../types'; import { Modal } from 'office-ui-fabric-react/lib/Modal'; import Webchat from './Webchat' -import TrainDialogAdmin from './TrainDialogAdmin' +import LogDialogAdmin from './LogDialogAdmin' import { Activity } from 'botframework-directlinejs' -import { BlisAppBase, LogDialog } from 'blis-models' +import { createChatSessionAsync } from '../actions/createActions' +import { BlisAppBase, LogDialog, Session } from 'blis-models' import { deleteLogDialogAsync } from '../actions/deleteActions' -class LogDialogModal extends React.Component { - - generateHistory() : Activity[] { +interface ComponentState { + selectedActivity: Activity | null, + chatSession: Session +} + +class LogDialogModal extends React.Component { + + componentWillMount() { + this.setState({ + selectedActivity: null, + chatSession : new Session({saveToLog : true}) + }, () => { + this.props.createChatSessionAsync(this.props.user.key, this.state.chatSession, this.props.apps.current.appId); + }) + } + + /** + * When modal is opened create a new session and reset the state. + */ + componentWillReceiveProps(nextProps: Props) { + if (this.props.open === false && nextProps.open === true) { + this.setState({ + selectedActivity: null, + chatSession : new Session({saveToLog : true}) + }, () => { + this.props.createChatSessionAsync(this.props.user.key, this.state.chatSession, this.props.apps.current.appId); + }) + } + } + + generateHistory(): Activity[] { if (!this.props.logDialog) { return []; } @@ -36,7 +64,7 @@ class LogDialogModal extends React.Component { let action = actions.filter(a => a.actionId === scorerStep.predictedAction)[0] return { id: `${i}:${j}`, - from:{ + from: { id: "BlisTrainer", name: "BlisTrainer" }, @@ -56,6 +84,16 @@ class LogDialogModal extends React.Component { this.props.onClose() } + onSelectWebChatActivity(activity: Activity) { + this.setState({ + selectedActivity: activity + }) + } + + onPostWebChatActivity(activity: Activity) { + console.log(`activity posted: `, activity) + } + render() { let history = this.generateHistory(); return ( @@ -67,11 +105,19 @@ class LogDialogModal extends React.Component { >
- + this.onSelectWebChatActivity(activity)} + onPostActivity={activity => this.onPostWebChatActivity(activity)} + />
- {/* */} +
{ } const mapDispatchToProps = (dispatch: any) => { return bindActionCreators({ + createChatSessionAsync, deleteLogDialogAsync }, dispatch); } const mapStateToProps = (state: State, ownProps: ReceivedProps) => { return { + apps: state.apps, user: state.user, actions: state.actions } @@ -117,4 +165,4 @@ const stateProps = returntypeof(mapStateToProps); const dispatchProps = returntypeof(mapDispatchToProps); type Props = typeof stateProps & typeof dispatchProps & ReceivedProps; -export default connect(mapStateToProps, mapDispatchToProps)(LogDialogModal); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(LogDialogModal); \ No newline at end of file diff --git a/src/containers/TrainDialogAdmin.tsx b/src/containers/TrainDialogAdmin.tsx index 216e0686..8674f129 100644 --- a/src/containers/TrainDialogAdmin.tsx +++ b/src/containers/TrainDialogAdmin.tsx @@ -47,7 +47,7 @@ class TrainDialogAdmin extends React.Component { details = []; for (let entityId of entityIds) { let entity: EntityBase = this.props.entities.find((a: EntityBase) => a.entityId == entityId); - details.push(
{entity.entityName}
); + details.push(
{entity.entityName}
); } } return ( diff --git a/src/containers/Webchat.tsx b/src/containers/Webchat.tsx index 2507382c..5fa18f77 100644 --- a/src/containers/Webchat.tsx +++ b/src/containers/Webchat.tsx @@ -17,6 +17,11 @@ class Webchat extends React.Component { private behaviorSubject : BehaviorSubject = null; private chatProps : BotChat.ChatProps = null; + static defaultProps = { + onSelectActivity: () => {}, + onPostActivity: () => {} + } + constructor(p: any) { super(p); this.behaviorSubject = null; @@ -29,7 +34,15 @@ class Webchat extends React.Component { this.behaviorSubject = new BehaviorSubject({}); this.behaviorSubject.subscribe((value) => { if (value.activity) { + const activity: Activity = value.activity + this.props.onSelectActivity(activity) + + // TODO: Remove split of id here. + // This is coupling knowledge about how ID was constructed within the generateHistory function + // Id should be an opaque and unique identifier. let [roundNum, scoreNum] = value.activity.id.split(":"); + + // TODO: Remove hard coding of train related actions from web chat this.props.setTrainDialogView(roundNum, scoreNum); } }) @@ -50,6 +63,8 @@ class Webchat extends React.Component { const _dl = { ...dl, postActivity: (activity: any) => { + // TODO: Remove hard coding of adding message to stack + // Webchat should not be aware of what happens when activity is posted, only that is is posted. if (activity.type = "message") { if (this.props.sessionType === 'teach') { @@ -63,6 +78,8 @@ class Webchat extends React.Component { } else { this.props.addMessageToChatConversationStack(activity) } + + this.props.onPostActivity(activity) } return dl.postActivity(activity) }, @@ -113,7 +130,9 @@ const mapStateToProps = (state: State, ownProps: any) => { interface ReceivedProps { sessionType: string, - history: Activity[] + history: Activity[], + onSelectActivity: (a: Activity) => void, + onPostActivity: (a: Activity) => void } // Props types inferred from mapStateToProps & dispatchToProps