import React, { createContext, useCallback, useContext, useMemo, useReducer, useRef, } from 'react'; import type { ReactNode } from 'react'; import type { ICopilotAction, ICopilotMessage, ICopilotPopupCustomization, ICopilotQuest, ICopilotQuestOverview, ICopilotQuickActionIcon, ICopilotStep, } from '../service/copilot/copilot.interface'; import { dismissCopilotQuest } from '../service/copilot/copilot.service'; import type { CopilotAction, ICopilotData } from './copilot-helpers'; import { copilotReducer, initialCopilotState } from './copilot-helpers'; /** * The copilot slice exposed on its own context: every state field plus the * actions and an imperative {@link ICopilotApi.getState} that returns the * latest copilot state, replicating zustand's ``getState()``. */ export interface ICopilotApi extends ICopilotData { /** Store the auth credentials so background calls (dismiss) can authenticate. */ setAuth: (token: string, clientId: string) => void; /** Open the panel. */ open: () => void; /** Collapse the panel. */ close: () => void; /** Toggle the panel open state. */ toggle: () => void; /** Replace the whole thread (used to hydrate persisted history). */ setMessages: (messages: ICopilotMessage[]) => void; /** Store the quest overview and derive the launcher badge/notification. */ setQuestOverview: (overview: ICopilotQuestOverview) => void; /** Dismiss the launcher notification toast (this session only). */ dismissQuestNotification: () => void; /** Permanently dismiss the current nudge quest so it stops surfacing. */ dismissNudge: () => void; /** Open the panel and queue a prompt for the copilot to send (page -> chat). */ requestCopilotTurn: (prompt: string) => void; /** Clear the queued prompt once the panel has sent it. */ clearPendingPrompt: () => void; /** Queue (or clear) a KB draft for the Knowledge Base review modal. */ setKbDraftFileName: (fileName: string | null) => void; /** Queue (or clear) a popup-customization proposal for the Settings page. */ setPendingPopupCustomization: ( suggestion: ICopilotPopupCustomization | null ) => void; /** Queue (or clear) generated quick-action icons for the Settings page. */ setPendingQuickActionIcons: (icons: ICopilotQuickActionIcon[] | null) => void; /** Append a user message to the thread. */ addUserMessage: (content: string) => void; /** Start an empty assistant message that steps and tokens append to. */ startAssistantMessage: () => void; /** Insert or update a tool/thinking step on the last assistant message. */ upsertStep: (step: ICopilotStep) => void; /** Attach quick-action buttons to the last assistant message. */ setAssistantActions: (actions: ICopilotAction[]) => void; /** Remove the quick-action buttons from a message once one is used. */ consumeMessageActions: (index: number) => void; /** Attach a started generation (article or content job) to the last message. */ setAssistantGeneration: (generation: { kind: 'article' | 'content'; articleId?: string; jobId?: string; }) => void; /** Mark a message's tracked generation as ready/sent (stops its poll). */ setMessageGenerationReady: (index: number) => void; /** Append streamed text to the last assistant message. */ appendToAssistant: (text: string) => void; /** Set the streaming flag. */ setStreaming: (value: boolean) => void; /** Replace the last assistant message with an error notice. */ failLastAssistant: (message: string) => void; /** Clear the thread to start a fresh conversation. */ reset: () => void; /** Imperatively patch fields (replaces zustand's ``setState(partial)``). */ setState: (patch: Partial) => void; /** Read the latest copilot state + actions imperatively (replaces ``getState()``). */ getState: () => ICopilotApi; } const noopCopilot: ICopilotApi = { ...initialCopilotState, setAuth: () => {}, open: () => {}, close: () => {}, toggle: () => {}, setMessages: () => {}, setQuestOverview: () => {}, dismissQuestNotification: () => {}, dismissNudge: () => {}, requestCopilotTurn: () => {}, clearPendingPrompt: () => {}, setKbDraftFileName: () => {}, setPendingPopupCustomization: () => {}, setPendingQuickActionIcons: () => {}, addUserMessage: () => {}, startAssistantMessage: () => {}, upsertStep: () => {}, setAssistantActions: () => {}, consumeMessageActions: () => {}, setAssistantGeneration: () => {}, setMessageGenerationReady: () => {}, appendToAssistant: () => {}, setStreaming: () => {}, failLastAssistant: () => {}, reset: () => {}, setState: () => {}, getState: () => noopCopilot, }; const CopilotContext = createContext(noopCopilot); /** Hook returning the copilot slice of the app context. */ export const useCopilot = (): ICopilotApi => useContext(CopilotContext); interface CopilotProviderProps { children: ReactNode; } /** * Wraps the app with the Reco copilot state. Holds the reducer-backed slice and * exposes both the actions and an imperative ``getState()`` so streamed turns * can read the latest thread without going stale between renders. * * @param props - Component props. * @param props.children - The subtree that can read copilot state. * @return The provider element. */ export const CopilotProvider = ({ children, }: CopilotProviderProps): JSX.Element => { const [copilotState, dispatch] = useReducer( copilotReducer, initialCopilotState ); // Holds the full copilot API (state + actions) so getState() returns the // latest values together with the actions, exactly like zustand's getState(). const copilotApiRef = useRef(noopCopilot); const getState = useCallback((): ICopilotApi => copilotApiRef.current, []); const dispatchCopilot = useCallback( (action: CopilotAction) => dispatch(action), [] ); const setAuth = useCallback( (nextToken: string, nextClientId: string) => dispatchCopilot({ type: 'setAuth', token: nextToken, clientId: nextClientId, }), [dispatchCopilot] ); const open = useCallback( () => dispatchCopilot({ type: 'open' }), [dispatchCopilot] ); const close = useCallback( () => dispatchCopilot({ type: 'close' }), [dispatchCopilot] ); const toggle = useCallback( () => dispatchCopilot({ type: 'toggle' }), [dispatchCopilot] ); const setMessages = useCallback( (messages: ICopilotMessage[]) => dispatchCopilot({ type: 'setMessages', messages }), [dispatchCopilot] ); const setQuestOverview = useCallback( (overview: ICopilotQuestOverview) => dispatchCopilot({ type: 'setQuestOverview', overview }), [dispatchCopilot] ); const dismissQuestNotification = useCallback( () => dispatchCopilot({ type: 'dismissQuestNotification' }), [dispatchCopilot] ); const dismissNudge = useCallback(() => { const nudge: ICopilotQuest | null = copilotApiRef.current.questNudge; if (!nudge) { dispatchCopilot({ type: 'dismissQuestNotification' }); return; } // Clear the toast immediately, then persist the dismissal and re-apply the // refreshed overview so the next-best nudge surfaces. dispatchCopilot({ type: 'clearNudgeToast' }); const { token: copilotToken, clientId: copilotClientId } = copilotApiRef.current; void dismissCopilotQuest( nudge.quest_id, copilotToken, copilotClientId ).then(overview => { if (overview) dispatchCopilot({ type: 'setQuestOverview', overview }); }); }, [dispatchCopilot]); const requestCopilotTurn = useCallback( (prompt: string) => dispatchCopilot({ type: 'requestCopilotTurn', prompt }), [dispatchCopilot] ); const clearPendingPrompt = useCallback( () => dispatchCopilot({ type: 'clearPendingPrompt' }), [dispatchCopilot] ); const setKbDraftFileName = useCallback( (fileName: string | null) => dispatchCopilot({ type: 'setKbDraftFileName', fileName }), [dispatchCopilot] ); const setPendingPopupCustomization = useCallback( (suggestion: ICopilotPopupCustomization | null) => dispatchCopilot({ type: 'setPendingPopupCustomization', suggestion }), [dispatchCopilot] ); const setPendingQuickActionIcons = useCallback( (icons: ICopilotQuickActionIcon[] | null) => dispatchCopilot({ type: 'setPendingQuickActionIcons', icons }), [dispatchCopilot] ); const addUserMessage = useCallback( (content: string) => dispatchCopilot({ type: 'addUserMessage', content }), [dispatchCopilot] ); const startAssistantMessage = useCallback( () => dispatchCopilot({ type: 'startAssistantMessage' }), [dispatchCopilot] ); const upsertStep = useCallback( (step: ICopilotStep) => dispatchCopilot({ type: 'upsertStep', step }), [dispatchCopilot] ); const setAssistantActions = useCallback( (actions: ICopilotAction[]) => dispatchCopilot({ type: 'setAssistantActions', actions }), [dispatchCopilot] ); const consumeMessageActions = useCallback( (index: number) => dispatchCopilot({ type: 'consumeMessageActions', index }), [dispatchCopilot] ); const setAssistantGeneration = useCallback( (generation: { kind: 'article' | 'content'; articleId?: string; jobId?: string; }) => dispatchCopilot({ type: 'setAssistantGeneration', generation }), [dispatchCopilot] ); const setMessageGenerationReady = useCallback( (index: number) => dispatchCopilot({ type: 'setMessageGenerationReady', index }), [dispatchCopilot] ); const appendToAssistant = useCallback( (text: string) => dispatchCopilot({ type: 'appendToAssistant', text }), [dispatchCopilot] ); const setStreaming = useCallback( (value: boolean) => dispatchCopilot({ type: 'setStreaming', value }), [dispatchCopilot] ); const failLastAssistant = useCallback( (message: string) => dispatchCopilot({ type: 'failLastAssistant', message }), [dispatchCopilot] ); const reset = useCallback( () => dispatchCopilot({ type: 'reset' }), [dispatchCopilot] ); const setState = useCallback( (patch: Partial) => dispatchCopilot({ type: 'patch', patch }), [dispatchCopilot] ); const copilot = useMemo( () => ({ ...copilotState, setAuth, open, close, toggle, setMessages, setQuestOverview, dismissQuestNotification, dismissNudge, requestCopilotTurn, clearPendingPrompt, setKbDraftFileName, setPendingPopupCustomization, setPendingQuickActionIcons, addUserMessage, startAssistantMessage, upsertStep, setAssistantActions, consumeMessageActions, setAssistantGeneration, setMessageGenerationReady, appendToAssistant, setStreaming, failLastAssistant, reset, setState, getState, }), [ copilotState, setAuth, open, close, toggle, setMessages, setQuestOverview, dismissQuestNotification, dismissNudge, requestCopilotTurn, clearPendingPrompt, setKbDraftFileName, setPendingPopupCustomization, setPendingQuickActionIcons, addUserMessage, startAssistantMessage, upsertStep, setAssistantActions, consumeMessageActions, setAssistantGeneration, setMessageGenerationReady, appendToAssistant, setStreaming, failLastAssistant, reset, setState, getState, ] ); // Keep the imperative-read ref pointed at the latest API object so getState() // sees current state and actions during the same render and afterward. copilotApiRef.current = copilot; return ( {children} ); };