Skip to content

Pinia Task 11 - product-expert #6828

@n-lark

Description

@n-lark

Task 11 — product-expert (PR 12)

Gate: PRs 5 (ux-drawers), 10 (product-expert-ff-agent), and 11 (product-expert-operator-agent) must all be merged first.

Vuex module: frontend/src/store/modules/product/expert/index.js
New files: frontend/src/stores/product-expert.js, frontend/src/stores/product-expert-context.js
Persistence: shouldWakeUpAssistant → localStorage, owned by product-expert-context (cleared on logout)
Cross-store dependencies: ux-drawers (open drawer), product-expert-context, product-expert-ff-agent, product-expert-operator-agent, context (expert getter), account bridge (featuresCheck)
Also: frontend/src/api/client.js top-level import store → lazy require to break a circular dependency introduced by this migration (see 11.x below)

11.1 — Key architectural change: sub-module state access

In Vuex, nested module state is merged into the parent. The expert store accesses sub-module state as state['ff-agent'].messages. In Pinia, each store is independent. All state[state.agentMode].* references must be replaced with imports of the relevant agent store:

// Vuex (current)
state[state.agentMode].messages

// Pinia (new)
const agentStore = this.agentMode === FF_AGENT
    ? useProductExpertFfAgentStore()
    : useProductExpertOperatorAgentStore()
agentStore.messages

This affects: messages, hasMessages, hasUserMessages, lastMessage, isSessionExpired getters, and every action that writes to state[state.agentMode].*.

11.2 — Create the Pinia store

// frontend/src/stores/product-expert.js
import { defineStore } from 'pinia'
import { markRaw } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import expertApi from '../api/expert.js'
// Note: ExpertDrawer is NOT imported here — importing it would form a circular dependency
// (product-expert.js → ExpertDrawer.vue → product-expert.js). ExpertButton.vue imports it
// directly; wakeUpAssistant uses a dynamic import() for the wake-up-from-context path.
import { FF_AGENT, OPERATOR_AGENT } from '../store/modules/product/expert/agents.js'
import { useUxDrawersStore } from './ux-drawers.js'
import { useContextStore } from './context.js'
import { useProductAssistantStore } from './product-assistant.js'
import { useProductExpertContextStore } from './product-expert-context.js'
import { useProductExpertFfAgentStore } from './product-expert-ff-agent.js'
import { useProductExpertOperatorAgentStore } from './product-expert-operator-agent.js'
import { useAccountBridge } from './_account-bridge.js'

export const useProductExpertStore = defineStore('product-expert', {
    state: () => ({
        // shouldWakeUpAssistant lives in product-expert-context — exposed here as a getter
        agentMode: FF_AGENT,
        loadingVariant: FF_AGENT,
    }),
    getters: {
        // Helper to get the current agent store
        _agentStore () {
            return this.agentMode === FF_AGENT
                ? useProductExpertFfAgentStore()
                : useProductExpertOperatorAgentStore()
        },
        messages () { return this._agentStore.messages },
        hasMessages () { return this._agentStore.messages.length > 0 },
        hasUserMessages () { return this._agentStore.messages.some(m => m.type === 'human') },
        lastMessage () {
            const msgs = this._agentStore.messages
            return msgs.length > 0 ? msgs[msgs.length - 1] : null
        },
        shouldWakeUpAssistant () { return useProductExpertContextStore().shouldWakeUpAssistant },
        isSessionExpired () { return this._agentStore.sessionExpiredShown },
        isFfAgent: (state) => state.agentMode === FF_AGENT,
        isOperatorAgent: (state) => state.agentMode === OPERATOR_AGENT,
        hasSelectedCapabilities () {
            return useProductExpertOperatorAgentStore().selectedCapabilities?.length > 0
        },
        canImportFlows () {
            const assistantStore = useProductAssistantStore()
            return !!assistantStore.immersiveInstance && !!assistantStore.supportedActions['custom:import-flow']
        },
        canManagePalette () {
            const assistantStore = useProductAssistantStore()
            return !!assistantStore.immersiveInstance && !!assistantStore.supportedActions['core:manage-palette']
        }
    },
    actions: {
        // Port all actions from Vuex, replacing:
        //   state[state.agentMode].* → this._agentStore.*
        //   commit('SET_SESSION_CHECK_TIMER', t) → agentStore.setSessionCheckTimer(t)
        //   commit('SET_ABORT_CONTROLLER', c) → this.abortController = markRaw(c)
        //   commit('SET_STREAMING_TIMER', t) → this.streamingTimer = markRaw(t)
        //   dispatch('ux/drawers/openRightDrawer', ...) → useUxDrawersStore().openRightDrawer(...)
        //   dispatch('ux/drawers/setRightDrawerWider', ...) → useUxDrawersStore().setRightDrawerWider(...)
        //   rootGetters['context/expert'] → useContextStore().expert
        //   rootGetters['account/featuresCheck'] → useAccountBridge().featuresCheck
        //   dispatch('product/expert/ff-agent/reset') → useProductExpertFfAgentStore().reset()
        //   dispatch('product/expert/operator-agent/...') → useProductExpertOperatorAgentStore().*

        setAbortController (controller) {
            this.abortController = controller ? markRaw(controller) : null
        },
        // ... all other actions ported verbatim with substitutions above
    },
    // No persist block — shouldWakeUpAssistant persistence is handled by product-expert-context
})

11.3 — Update context.js expert getter (final)

Now that all stores it depends on are on Pinia, replace the remaining bridge reads in the expert getter with direct Pinia store imports:

// In frontend/src/stores/context.js expert getter
import { useAccountBridge } from './_account-bridge.js'          // keep for account (still Vuex)
import { useProductAssistantStore } from './product-assistant.js' // now Pinia
import { useProductExpertStore } from './product-expert.js'       // now Pinia

11.4 — Find and update all consumers

grep -rl "product/expert\|mapState.*expert\|mapGetters.*expert\|mapActions.*expert" frontend/src/ | grep -v "ff-agent\|operator-agent"

Primary consumers: frontend/src/components/expert/*.vue, ExpertButton.vue, ExpertDrawer.vue.

For each consumer, check if it's inside a mixin or rendered as <component :is="..."> inside <ff-dialog> — use Pattern C from PINIA_COMPONENT_PATTERNS.md (mapState / mapActions from Pinia) — this works correctly in all component types.

Also update messaging.service.js:126: replace this.$store.dispatch('product/expert/setContext', payload) with useProductExpertContextStore().setContext(payload). The context store is a leaf — importing it here does not form a cycle.

11.5 — Delete the Vuex modules

Delete all three expert modules and the now-empty expert directory:

  • frontend/src/store/modules/product/expert/index.js
  • frontend/src/store/modules/product/expert/ff-agent/index.js
  • frontend/src/store/modules/product/expert/operator-agent/index.js

Remove expert from store/modules/product/index.js modules registration.

At this point the product Vuex root module only contains assistant (already migrated in PR 9), so this is also when you delete the product Vuex module entirely and remove it from store/index.js.

Logout bridge: uncomment useProductExpertStore().$reset() in the Vuex logout action (Task 0.7). At this point all product stores are migrated — the bridge should now have lines uncommented for Tasks 6, 7, 8, 9, 10, and 11.

11.6 — Write store tests

Two spec files are needed:

test/unit/frontend/stores/product-expert.spec.js — covers product-expert.js. Mock product-expert-context.js so shouldWakeUpAssistant resolves without the real store.

Key areas: _agentStore routing, isWaitingForResponse, addUserMessage, addAiMessage, addPredefinedAiMessage, addSystemMessage, updateMessageStreamedState, updateAnswerStreamedState, setAgentMode, setAbortController, reset.

test/unit/frontend/stores/product-expert-context.spec.js — covers product-expert-context.js. Mock product-expert-ff-agent.js with a module-level shared reference (not an inline factory) so mutations inside setContext are visible to test assertions.

Key areas: initial state, setContext (sets context/sessionId on ff-agent, sets flag, skips sessionId if absent, no-ops when feature disabled), clearWakeUp.

11.x — Circular import fixes

Migrating product-expert to Pinia exposes circular import cycles that ESM will crash on (TDZ errors). Three cycles exist; two are fixed here:

Cycle 1: product-expert.js → ExpertDrawer.vue → product-expert.js
Fix: ExpertButton.vue imports ExpertDrawer statically and opens the drawer directly. wakeUpAssistant (used for the wake-up-from-context path) uses a dynamic import() with a comment explaining why.

Cycle 2: product-expert.js → product-assistant.js → messaging.service.js → product-expert.js
Fix: extract shouldWakeUpAssistant + setContext into a new leaf store product-expert-context.js that imports only _account_bridge.js and product-expert-ff-agent.js (both leaves). messaging.service.js imports the context store directly — no cycle.
Side effect: setContext no longer calls wakeUpAssistant directly. pages/team/index.vue adds a watcher on shouldWakeUpAssistant to restore that reactivity.

Cycle 3: product-expert.js → api/client.js → store/index.js → … → product-expert.js
Deferred to Task 12 (account store migration). For now, api/client.js converts its top-level import store to a lazy require.

11.7 — Export from stores index

export { useProductExpertStore } from './product-expert.js'

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

Done

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions