236 lines
6.1 KiB
JavaScript
236 lines
6.1 KiB
JavaScript
import { head, pathOr, without, concat, path, length, reject } from 'ramda'
|
|
import { combineReducers } from 'redux'
|
|
import reduceReducers from 'reduce-reducers'
|
|
import { reducer as formReducer, formValueSelector } from 'redux-form'
|
|
|
|
import { rules, collectDefaults, formatInputs, rulesFr } from 'Engine/rules'
|
|
import {
|
|
getNextSteps,
|
|
collectMissingVariablesByTarget
|
|
} from 'Engine/generateQuestions'
|
|
import computeThemeColours from 'Components/themeColours'
|
|
import {
|
|
STEP_ACTION,
|
|
EXPLAIN_VARIABLE,
|
|
CHANGE_THEME_COLOUR,
|
|
CHANGE_LANG
|
|
} from './actions'
|
|
|
|
import { analyseMany, parseAll } from 'Engine/traverse'
|
|
|
|
import ReactPiwik from 'Components/Tracker'
|
|
|
|
import { popularTargetNames } from './components/TargetSelection'
|
|
|
|
// assume "wraps" a given situation function with one that overrides its values with
|
|
// the given assumptions
|
|
export let assume = (evaluator, assumptions) => state => name => {
|
|
let userInput = evaluator(state)(name)
|
|
return userInput != null ? userInput : assumptions[name]
|
|
}
|
|
|
|
export let reduceSteps = (tracker, flatRules, answerSource) => (
|
|
state,
|
|
action
|
|
) => {
|
|
state.flatRules = flatRules
|
|
// Optimization - don't parse on each analysis
|
|
if (!state.parsedRules) {
|
|
state.parsedRules = parseAll(flatRules)
|
|
}
|
|
|
|
// TODO
|
|
if (action.type == CHANGE_LANG) {
|
|
if (action.lang == 'fr') {
|
|
flatRules = rulesFr
|
|
} else flatRules = rules
|
|
return {
|
|
...state,
|
|
flatRules
|
|
}
|
|
}
|
|
|
|
if (
|
|
![
|
|
'SET_CONVERSATION_TARGETS',
|
|
STEP_ACTION,
|
|
'USER_INPUT_UPDATE',
|
|
'START_CONVERSATION'
|
|
].includes(action.type)
|
|
)
|
|
return state
|
|
|
|
if (path(['form', 'conversation', 'syncErrors'], state)) return state
|
|
|
|
let targetNames = reject(
|
|
name => state.activeTargetInput && state.activeTargetInput.includes(name)
|
|
)(state.targetNames)
|
|
|
|
let sim = {},
|
|
// Hard assumptions cannot be changed, they are used to specialise a simulator
|
|
// before the user sees the first question
|
|
hardAssumptions = pathOr({}, ['simulateur', 'hypothèses'], sim),
|
|
intermediateSituation = assume(answerSource, hardAssumptions),
|
|
// Most rules have default values
|
|
rulesDefaults = collectDefaults(flatRules),
|
|
situationWithDefaults = assume(intermediateSituation, rulesDefaults)
|
|
|
|
let analysis = analyseMany(state.parsedRules, targetNames)(
|
|
situationWithDefaults(state)
|
|
)
|
|
|
|
if (action.type === 'USER_INPUT_UPDATE') {
|
|
return { ...state, analysis, situationGate: situationWithDefaults(state) }
|
|
}
|
|
|
|
let nextStepsAnalysis = analyseMany(state.parsedRules, targetNames)(
|
|
intermediateSituation(state)
|
|
),
|
|
missingVariablesByTarget = collectMissingVariablesByTarget(
|
|
nextStepsAnalysis.targets
|
|
),
|
|
nextSteps = getNextSteps(missingVariablesByTarget)
|
|
|
|
let newState = {
|
|
...state,
|
|
analysis,
|
|
situationGate: situationWithDefaults(state),
|
|
explainedVariable: null,
|
|
nextSteps,
|
|
// 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
|
|
},
|
|
currentQuestion: head(nextSteps),
|
|
foldedSteps:
|
|
action.type === 'SET_CONVERSATION_TARGETS' && action.reset
|
|
? []
|
|
: state.foldedSteps
|
|
}
|
|
|
|
if (action.type == 'START_CONVERSATION') return newState
|
|
|
|
if (action.type == STEP_ACTION && action.name == 'fold') {
|
|
tracker.push([
|
|
'trackEvent',
|
|
'answer:' + action.source,
|
|
action.step + ': ' + situationWithDefaults(state)(action.step)
|
|
])
|
|
|
|
if (!newState.currentQuestion) {
|
|
tracker.push([
|
|
'trackEvent',
|
|
'done',
|
|
'after' + length(newState.foldedSteps) + 'questions'
|
|
])
|
|
}
|
|
|
|
return {
|
|
...newState,
|
|
foldedSteps: [...state.foldedSteps, state.currentQuestion]
|
|
}
|
|
}
|
|
if (action.type == STEP_ACTION && action.name == 'unfold') {
|
|
tracker.push(['trackEvent', 'unfold', action.step])
|
|
|
|
// We are possibly "refolding" a previously open question
|
|
let previous = state.currentQuestion,
|
|
// we fold it back into foldedSteps if it had been answered
|
|
answered = previous && answerSource(state)(previous) != undefined,
|
|
foldedSteps = answered
|
|
? concat(state.foldedSteps, [previous])
|
|
: state.foldedSteps
|
|
|
|
return {
|
|
...newState,
|
|
foldedSteps: without([action.step], foldedSteps),
|
|
currentQuestion: action.step
|
|
}
|
|
}
|
|
}
|
|
|
|
function themeColours(state = computeThemeColours(), { type, colour }) {
|
|
if (type == CHANGE_THEME_COLOUR) return computeThemeColours(colour)
|
|
else return state
|
|
}
|
|
|
|
function explainedVariable(state = null, { type, variableName = null }) {
|
|
switch (type) {
|
|
case EXPLAIN_VARIABLE:
|
|
return variableName
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
function currentExample(state = null, { type, situation, name }) {
|
|
switch (type) {
|
|
case 'SET_EXAMPLE':
|
|
return name != null ? { name, situation } : null
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
function conversationStarted(state = false, { type }) {
|
|
switch (type) {
|
|
case 'START_CONVERSATION':
|
|
return true
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
function activeTargetInput(state = null, { type, name }) {
|
|
switch (type) {
|
|
case 'SET_ACTIVE_TARGET_INPUT':
|
|
return name
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
export default initialRules =>
|
|
reduceReducers(
|
|
combineReducers({
|
|
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
|
|
// this is handled by redux-form, pas touche !
|
|
form: formReducer,
|
|
|
|
/* Have forms been filled or ignored ?
|
|
false means the user is reconsidering its previous input */
|
|
foldedSteps: (steps = []) => steps,
|
|
currentQuestion: (state = null) => state,
|
|
nextSteps: (state = []) => state,
|
|
missingVariablesByTarget: (state = {}) => state,
|
|
|
|
parsedRules: (state = null) => state,
|
|
flatRules: (state = null) => state,
|
|
analysis: (state = null) => state,
|
|
|
|
targetNames: (state = popularTargetNames) => state,
|
|
|
|
situationGate: (state = name => null) => state,
|
|
|
|
iframe: (state = false) => state,
|
|
|
|
themeColours,
|
|
|
|
explainedVariable,
|
|
|
|
currentExample,
|
|
conversationStarted,
|
|
activeTargetInput
|
|
}),
|
|
// cross-cutting concerns because here `state` is the whole state tree
|
|
reduceSteps(
|
|
ReactPiwik,
|
|
initialRules,
|
|
formatInputs(initialRules, formValueSelector)
|
|
)
|
|
)
|