2018-04-05 15:08:04 +00:00
|
|
|
import { head, pathOr, without, concat, path, length, reject } from 'ramda'
|
2016-11-15 18:46:17 +00:00
|
|
|
import { combineReducers } from 'redux'
|
2017-01-10 18:22:44 +00:00
|
|
|
import reduceReducers from 'reduce-reducers'
|
2017-11-24 17:55:15 +00:00
|
|
|
import { reducer as formReducer, formValueSelector } from 'redux-form'
|
|
|
|
|
2018-03-21 15:12:13 +00:00
|
|
|
import { rules, collectDefaults, formatInputs, rulesFr } from 'Engine/rules'
|
2018-03-29 16:25:22 +00:00
|
|
|
import {
|
|
|
|
getNextSteps,
|
|
|
|
collectMissingVariablesByTarget
|
|
|
|
} from 'Engine/generateQuestions'
|
2017-07-02 17:12:02 +00:00
|
|
|
import computeThemeColours from 'Components/themeColours'
|
2017-11-24 17:55:15 +00:00
|
|
|
import {
|
|
|
|
STEP_ACTION,
|
|
|
|
EXPLAIN_VARIABLE,
|
2018-03-29 11:38:55 +00:00
|
|
|
CHANGE_THEME_COLOUR,
|
|
|
|
CHANGE_LANG
|
2017-11-24 17:55:15 +00:00
|
|
|
} from './actions'
|
2017-07-08 11:28:50 +00:00
|
|
|
|
2017-11-28 11:45:06 +00:00
|
|
|
import { analyseMany, parseAll } from 'Engine/traverse'
|
2017-07-08 11:28:50 +00:00
|
|
|
|
2017-11-13 14:50:48 +00:00
|
|
|
import ReactPiwik from 'Components/Tracker'
|
2017-10-12 14:08:43 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
import { popularTargetNames } from './components/TargetSelection'
|
|
|
|
|
2017-09-21 13:46:59 +00:00
|
|
|
// assume "wraps" a given situation function with one that overrides its values with
|
|
|
|
// the given assumptions
|
2017-12-08 14:12:41 +00:00
|
|
|
export let assume = (evaluator, assumptions) => state => name => {
|
2017-11-13 14:50:48 +00:00
|
|
|
let userInput = evaluator(state)(name)
|
|
|
|
return userInput != null ? userInput : assumptions[name]
|
|
|
|
}
|
2017-07-07 08:41:06 +00:00
|
|
|
|
2017-11-24 17:55:15 +00:00
|
|
|
export let reduceSteps = (tracker, flatRules, answerSource) => (
|
|
|
|
state,
|
|
|
|
action
|
|
|
|
) => {
|
2018-03-29 08:55:42 +00:00
|
|
|
state.flatRules = flatRules
|
2017-11-28 11:45:06 +00:00
|
|
|
// Optimization - don't parse on each analysis
|
2018-03-29 08:55:42 +00:00
|
|
|
if (!state.parsedRules) {
|
|
|
|
state.parsedRules = parseAll(flatRules)
|
|
|
|
}
|
2017-11-28 11:45:06 +00:00
|
|
|
|
2018-03-29 11:38:55 +00:00
|
|
|
// TODO
|
|
|
|
if (action.type == CHANGE_LANG) {
|
2018-03-12 15:22:16 +00:00
|
|
|
if (action.lang == 'fr') {
|
|
|
|
flatRules = rulesFr
|
|
|
|
} else flatRules = rules
|
2018-03-29 11:38:55 +00:00
|
|
|
return {
|
|
|
|
...state,
|
|
|
|
flatRules
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-17 15:56:30 +00:00
|
|
|
if (
|
2018-03-29 17:26:35 +00:00
|
|
|
![
|
|
|
|
'SET_CONVERSATION_TARGETS',
|
|
|
|
STEP_ACTION,
|
|
|
|
'USER_INPUT_UPDATE',
|
|
|
|
'START_CONVERSATION'
|
|
|
|
].includes(action.type)
|
2018-01-17 15:56:30 +00:00
|
|
|
)
|
|
|
|
return state
|
2017-07-07 08:41:06 +00:00
|
|
|
|
2018-03-21 15:12:13 +00:00
|
|
|
if (path(['form', 'conversation', 'syncErrors'], state)) return state
|
|
|
|
|
2018-04-05 15:08:04 +00:00
|
|
|
let targetNames = reject(
|
|
|
|
name => state.activeTargetInput && state.activeTargetInput.includes(name)
|
|
|
|
)(state.targetNames)
|
|
|
|
|
2018-04-24 09:53:32 +00:00
|
|
|
// Most rules have default values
|
|
|
|
let rulesDefaults = collectDefaults(flatRules),
|
|
|
|
situationWithDefaults = assume(answerSource, rulesDefaults)
|
2017-09-21 13:46:59 +00:00
|
|
|
|
2018-04-05 15:08:04 +00:00
|
|
|
let analysis = analyseMany(state.parsedRules, targetNames)(
|
2018-01-17 15:56:30 +00:00
|
|
|
situationWithDefaults(state)
|
|
|
|
)
|
|
|
|
|
2018-01-17 16:39:26 +00:00
|
|
|
if (action.type === 'USER_INPUT_UPDATE') {
|
2018-01-17 15:56:30 +00:00
|
|
|
return { ...state, analysis, situationGate: situationWithDefaults(state) }
|
|
|
|
}
|
|
|
|
|
2018-04-05 15:08:04 +00:00
|
|
|
let nextStepsAnalysis = analyseMany(state.parsedRules, targetNames)(
|
2018-04-24 09:53:32 +00:00
|
|
|
answerSource(state)
|
2018-03-29 17:26:35 +00:00
|
|
|
),
|
2018-03-29 16:25:22 +00:00
|
|
|
missingVariablesByTarget = collectMissingVariablesByTarget(
|
|
|
|
nextStepsAnalysis.targets
|
|
|
|
),
|
|
|
|
nextSteps = getNextSteps(missingVariablesByTarget)
|
2017-11-27 14:03:36 +00:00
|
|
|
|
2017-09-26 15:20:46 +00:00
|
|
|
let newState = {
|
2017-07-07 08:41:06 +00:00
|
|
|
...state,
|
2017-11-07 18:46:40 +00:00
|
|
|
analysis,
|
2017-11-24 17:55:15 +00:00
|
|
|
situationGate: situationWithDefaults(state),
|
|
|
|
explainedVariable: null,
|
2018-03-12 15:22:16 +00:00
|
|
|
nextSteps,
|
2018-04-09 12:19:48 +00:00
|
|
|
// store the missingVariables when no question has been answered yet,
|
|
|
|
// to be able to compute a progress by objective
|
|
|
|
missingVariablesByTarget: {
|
|
|
|
initial:
|
|
|
|
state.foldedSteps.length === 0
|
|
|
|
? missingVariablesByTarget
|
|
|
|
: state.missingVariablesByTarget.initial,
|
|
|
|
current: missingVariablesByTarget
|
|
|
|
},
|
2018-03-12 16:36:34 +00:00
|
|
|
currentQuestion: head(nextSteps),
|
|
|
|
foldedSteps:
|
|
|
|
action.type === 'SET_CONVERSATION_TARGETS' && action.reset
|
|
|
|
? []
|
|
|
|
: state.foldedSteps
|
2017-07-07 08:41:06 +00:00
|
|
|
}
|
|
|
|
|
2018-03-29 17:26:35 +00:00
|
|
|
if (action.type == 'START_CONVERSATION') return newState
|
2017-12-06 16:10:11 +00:00
|
|
|
|
2017-07-07 08:41:06 +00:00
|
|
|
if (action.type == STEP_ACTION && action.name == 'fold') {
|
2017-11-24 17:55:15 +00:00
|
|
|
tracker.push([
|
|
|
|
'trackEvent',
|
2018-02-22 16:23:47 +00:00
|
|
|
'answer:' + action.source,
|
2017-11-24 17:55:15 +00:00
|
|
|
action.step + ': ' + situationWithDefaults(state)(action.step)
|
|
|
|
])
|
2017-10-13 10:06:04 +00:00
|
|
|
|
2018-01-30 13:50:47 +00:00
|
|
|
if (!newState.currentQuestion) {
|
|
|
|
tracker.push([
|
|
|
|
'trackEvent',
|
|
|
|
'done',
|
2018-02-22 16:23:47 +00:00
|
|
|
'after' + length(newState.foldedSteps) + 'questions'
|
2018-01-30 13:50:47 +00:00
|
|
|
])
|
|
|
|
}
|
|
|
|
|
2017-07-07 08:41:06 +00:00
|
|
|
return {
|
2017-09-26 15:20:46 +00:00
|
|
|
...newState,
|
2017-11-24 17:55:15 +00:00
|
|
|
foldedSteps: [...state.foldedSteps, state.currentQuestion]
|
2017-07-07 08:41:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (action.type == STEP_ACTION && action.name == 'unfold') {
|
2017-11-13 14:50:48 +00:00
|
|
|
tracker.push(['trackEvent', 'unfold', action.step])
|
2017-10-12 14:08:43 +00:00
|
|
|
|
2017-11-05 14:19:49 +00:00
|
|
|
// We are possibly "refolding" a previously open question
|
2017-11-04 15:09:13 +00:00
|
|
|
let previous = state.currentQuestion,
|
2017-11-05 14:19:49 +00:00
|
|
|
// we fold it back into foldedSteps if it had been answered
|
|
|
|
answered = previous && answerSource(state)(previous) != undefined,
|
2017-11-24 17:55:15 +00:00
|
|
|
foldedSteps = answered
|
2018-01-08 15:07:26 +00:00
|
|
|
? concat(state.foldedSteps, [previous])
|
2017-11-24 17:55:15 +00:00
|
|
|
: state.foldedSteps
|
2017-07-07 08:41:06 +00:00
|
|
|
|
|
|
|
return {
|
2017-09-26 15:20:46 +00:00
|
|
|
...newState,
|
2018-01-08 15:07:26 +00:00
|
|
|
foldedSteps: without([action.step], foldedSteps),
|
2017-11-04 15:09:13 +00:00
|
|
|
currentQuestion: action.step
|
2017-07-07 08:41:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-01-10 18:22:44 +00:00
|
|
|
|
2017-11-24 17:55:15 +00:00
|
|
|
function themeColours(state = computeThemeColours(), { type, colour }) {
|
|
|
|
if (type == CHANGE_THEME_COLOUR) return computeThemeColours(colour)
|
2017-01-10 18:22:44 +00:00
|
|
|
else return state
|
|
|
|
}
|
|
|
|
|
2017-11-24 17:55:15 +00:00
|
|
|
function explainedVariable(state = null, { type, variableName = null }) {
|
2017-02-09 17:38:51 +00:00
|
|
|
switch (type) {
|
2018-01-03 15:54:19 +00:00
|
|
|
case EXPLAIN_VARIABLE:
|
|
|
|
return variableName
|
|
|
|
default:
|
|
|
|
return state
|
2017-02-09 17:38:51 +00:00
|
|
|
}
|
2017-02-08 16:50:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-22 16:23:47 +00:00
|
|
|
function currentExample(state = null, { type, situation, name }) {
|
|
|
|
switch (type) {
|
|
|
|
case 'SET_EXAMPLE':
|
|
|
|
return name != null ? { name, situation } : null
|
|
|
|
default:
|
|
|
|
return state
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-29 17:26:35 +00:00
|
|
|
function conversationStarted(state = false, { type }) {
|
|
|
|
switch (type) {
|
|
|
|
case 'START_CONVERSATION':
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return state
|
|
|
|
}
|
|
|
|
}
|
2018-04-05 15:08:04 +00:00
|
|
|
function activeTargetInput(state = null, { type, name }) {
|
|
|
|
switch (type) {
|
|
|
|
case 'SET_ACTIVE_TARGET_INPUT':
|
|
|
|
return name
|
|
|
|
default:
|
|
|
|
return state
|
|
|
|
}
|
|
|
|
}
|
2018-03-29 17:26:35 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
export default initialRules =>
|
|
|
|
reduceReducers(
|
|
|
|
combineReducers({
|
|
|
|
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
|
|
|
|
// this is handled by redux-form, pas touche !
|
|
|
|
form: formReducer,
|
2017-01-10 18:22:44 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
/* Have forms been filled or ignored ?
|
2017-01-10 18:22:44 +00:00
|
|
|
false means the user is reconsidering its previous input */
|
2018-03-12 15:22:16 +00:00
|
|
|
foldedSteps: (steps = []) => steps,
|
|
|
|
currentQuestion: (state = null) => state,
|
|
|
|
nextSteps: (state = []) => state,
|
2018-03-29 16:25:22 +00:00
|
|
|
missingVariablesByTarget: (state = {}) => state,
|
2017-01-10 18:22:44 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
parsedRules: (state = null) => state,
|
|
|
|
flatRules: (state = null) => state,
|
|
|
|
analysis: (state = null) => state,
|
2017-11-07 18:46:40 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
targetNames: (state = popularTargetNames) => state,
|
2017-01-10 18:22:44 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
situationGate: (state = name => null) => state,
|
2017-11-27 14:03:36 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
iframe: (state = false) => state,
|
2017-09-21 13:46:59 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
themeColours,
|
2018-01-15 20:01:05 +00:00
|
|
|
|
2018-03-12 15:22:16 +00:00
|
|
|
explainedVariable,
|
2017-02-08 16:50:22 +00:00
|
|
|
|
2018-03-29 17:26:35 +00:00
|
|
|
currentExample,
|
2018-04-05 15:08:04 +00:00
|
|
|
conversationStarted,
|
|
|
|
activeTargetInput
|
2018-03-12 15:22:16 +00:00
|
|
|
}),
|
|
|
|
// cross-cutting concerns because here `state` is the whole state tree
|
|
|
|
reduceSteps(
|
|
|
|
ReactPiwik,
|
|
|
|
initialRules,
|
|
|
|
formatInputs(initialRules, formValueSelector)
|
|
|
|
)
|
|
|
|
)
|