'use client'; import * as React from 'react'; import * as jsondiffpatch from 'jsondiffpatch'; import type { InternalAIProviderProps, AIProvider, InferAIState, ValueOrUpdater, InferActions, InferUIState, } from '../types'; import { isFunction } from '../utils'; const InternalUIStateProvider = React.createContext(null); const InternalAIStateProvider = React.createContext(undefined); const InternalActionProvider = React.createContext(null); const InternalSyncUIStateProvider = React.createContext(null); export function InternalAIProvider({ children, initialUIState, initialAIState, initialAIStatePatch, wrappedActions, wrappedSyncUIState, }: InternalAIProviderProps) { if (!('use' in React)) { throw new Error('Unsupported React version.'); } const uiState = React.useState(initialUIState); const setUIState = uiState[1]; const resolvedInitialAIStatePatch = initialAIStatePatch ? (React as any).use(initialAIStatePatch) : undefined; initialAIState = React.useMemo(() => { if (resolvedInitialAIStatePatch) { return jsondiffpatch.patch( jsondiffpatch.clone(initialAIState), resolvedInitialAIStatePatch, ); } return initialAIState; }, [initialAIState, resolvedInitialAIStatePatch]); const aiState = React.useState(initialAIState); const setAIState = aiState[1]; const aiStateRef = React.useRef(aiState[0]); React.useEffect(() => { aiStateRef.current = aiState[0]; }, [aiState[0]]); const clientWrappedActions = React.useMemo( () => Object.fromEntries( Object.entries(wrappedActions).map(([key, action]) => [ key, async (...args: any) => { const aiStateSnapshot = aiStateRef.current; const [aiStateDelta, result] = await action( aiStateSnapshot, ...args, ); (async () => { const delta = await aiStateDelta; if (delta !== undefined) { aiState[1]( jsondiffpatch.patch( jsondiffpatch.clone(aiStateSnapshot), delta, ), ); } })(); return result; }, ]), ), [wrappedActions], ); const clientWrappedSyncUIStateAction = React.useMemo(() => { if (!wrappedSyncUIState) { return () => {}; } return async () => { const aiStateSnapshot = aiStateRef.current; const [aiStateDelta, uiState] = await wrappedSyncUIState!( aiStateSnapshot, ); if (uiState !== undefined) { setUIState(uiState); } const delta = await aiStateDelta; if (delta !== undefined) { const patchedAiState = jsondiffpatch.patch( jsondiffpatch.clone(aiStateSnapshot), delta, ); setAIState(patchedAiState); } }; }, [wrappedSyncUIState]); return ( {children} ); } export function useUIState() { type T = InferUIState; const state = React.useContext< [T, (v: T | ((v_: T) => T)) => void] | null | undefined >(InternalUIStateProvider); if (state === null) { throw new Error('`useUIState` must be used inside an provider.'); } if (!Array.isArray(state)) { throw new Error('Invalid state'); } if (state[0] === undefined) { throw new Error( '`initialUIState` must be provided to `createAI` or ``', ); } return state; } // TODO: How do we avoid causing a re-render when the AI state changes but you // are only listening to a specific key? We need useSES perhaps? function useAIState(): [ InferAIState, (newState: ValueOrUpdater>) => void, ]; function useAIState( key: keyof InferAIState, ): [ InferAIState[typeof key], (newState: ValueOrUpdater[typeof key]>) => void, ]; function useAIState( ...args: [] | [keyof InferAIState] ) { type T = InferAIState; const state = React.useContext< [T, (newState: ValueOrUpdater) => void] | null | undefined >(InternalAIStateProvider); if (state === null) { throw new Error('`useAIState` must be used inside an provider.'); } if (!Array.isArray(state)) { throw new Error('Invalid state'); } if (state[0] === undefined) { throw new Error( '`initialAIState` must be provided to `createAI` or ``', ); } if (args.length >= 1 && typeof state[0] !== 'object') { throw new Error( 'When using `useAIState` with a key, the AI state must be an object.', ); } const key = args[0]; const setter = React.useCallback( typeof key === 'undefined' ? state[1] : (newState: ValueOrUpdater) => { if (isFunction(newState)) { return state[1](s => { return { ...s, [key]: newState(s[key]) }; }); } else { return state[1]({ ...state[0], [key]: newState }); } }, [key], ); if (args.length === 0) { return state; } else { return [state[0][args[0]], setter]; } } export function useActions() { type T = InferActions; const actions = React.useContext(InternalActionProvider); return actions; } export function useSyncUIState() { const syncUIState = React.useContext<() => Promise>( InternalSyncUIStateProvider, ); if (syncUIState === null) { throw new Error('`useSyncUIState` must be used inside an provider.'); } return syncUIState; } export { useAIState };