import R from 'ramda' import { combineReducers } from 'redux' import reduceReducers from 'reduce-reducers' import {reducer as formReducer, formValueSelector} from 'redux-form' import {rules, findRuleByName, findRuleByDottedName } from 'Engine/rules' import {nextSteps} from 'Engine/generateQuestions' import computeThemeColours from 'Components/themeColours' import { STEP_ACTION, START_CONVERSATION, EXPLAIN_VARIABLE, CHANGE_THEME_COLOUR} from './actions' import {analyse} from 'Engine/traverse' import ReactPiwik from 'Components/Tracker' import formValueTypes from 'Components/conversation/formValueTypes' let fromConversation = flatRules => state => name => { // Our situationGate retrieves data from the "conversation" form // The search below is to apply input conversions such as replacing "," with "." if (name.startsWith("sys.")) return null let rule = findRuleByDottedName(flatRules, name), format = rule ? formValueTypes[rule.format] : null, pre = format && format.validator.pre ? format.validator.pre : R.identity, value = formValueSelector('conversation')(state, name) return value && pre(value) } // assume "wraps" a given situation function with one that overrides its values with // the given assumptions 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) => { if (![START_CONVERSATION, STEP_ACTION].includes(action.type)) return state let targetNames = action.type == START_CONVERSATION ? action.targetNames : state.targetNames let sim = targetNames.length === 1 ? findRuleByName(flatRules, targetNames[0]) : {}, // Hard assumptions cannot be changed, they are used to specialise a simulator // before the user sees the first question hardAssumptions = R.pathOr({},['simulateur','hypothèses'],sim), // Soft assumptions are revealed after the simulation ends, and can be changed softAssumptions = R.pathOr({},['simulateur','par défaut'],sim), intermediateSituation = assume(answerSource, hardAssumptions), completeSituation = assume(intermediateSituation,softAssumptions) let situationGate = completeSituation(state), analysis = analyse(flatRules, targetNames)(situationGate) let newState = { ...state, targetNames, analysis, situationGate: situationGate, extraSteps: [], explainedVariable: null } if (action.type == START_CONVERSATION) { let next = nextSteps(situationGate, flatRules, newState.analysis) return { ...newState, foldedSteps: [], currentQuestion: R.head(next) } } if (action.type == STEP_ACTION && action.name == 'fold') { tracker.push(['trackEvent', 'answer', action.step+': '+situationGate(action.step)]) let foldedSteps = [...state.foldedSteps, state.currentQuestion], next = nextSteps(situationGate, flatRules, newState.analysis), assumptionsMade = !R.isEmpty(softAssumptions), done = next.length == 0 // The simulation is "over" - except we can now fill in extra questions // where the answers were previously given default reasonable assumptions if (done && assumptionsMade) { let newSituation = intermediateSituation(state), reanalysis = analyse(flatRules, targetNames)(newSituation), extraSteps = nextSteps(newSituation, flatRules, reanalysis) tracker.push(['trackEvent', 'done', 'extra questions: '+extraSteps.length]) return { ...newState, foldedSteps, extraSteps, currentQuestion: null } } if (done) { tracker.push(['trackEvent', 'done', 'no more questions']) } return { ...newState, foldedSteps, currentQuestion: R.head(next) } } 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 ? R.concat(state.foldedSteps, [previous]) : state.foldedSteps, // we fold it back into "extra steps" if it came from there fromExtra = previous && softAssumptions[previous] != undefined, extraSteps = fromExtra ? R.concat(state.extraSteps, [previous]) : state.extraSteps return { ...newState, foldedSteps: R.without([action.step], foldedSteps), extraSteps: R.without([action.step], extraSteps), 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 } } export default 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, extraSteps: (steps = []) => steps, currentQuestion: (state = null) => state, analysis: (state = null) => state, targetNames: (state = null) => state, situationGate: (state = name => null) => state, refine: (state = false) => state, themeColours, explainedVariable }), // cross-cutting concerns because here `state` is the whole state tree reduceSteps(ReactPiwik, rules, fromConversation(rules)) )