import { isEqual } from 'lodash'; import { AtomEffect, DefaultValue, atom, selector } from 'recoil'; import { Socket } from 'socket.io-client'; import { v4 as uuidv4 } from 'uuid'; import { ICommand } from './types/command'; import { IMode } from './types/mode'; import { IAction, IAsk, IAuthConfig, ICallFn, IChainlitConfig, IMcp, IMessageElement, IStep, ITasklistElement, IUser, ThreadHistory } from './types'; import { groupByDate } from './utils/group'; import { WavRecorder, WavStreamPlayer } from './wavtools'; export interface ISession { socket: Socket; error?: boolean; } export const threadIdToResumeState = atom({ key: 'ThreadIdToResume', default: undefined }); export const resumeThreadErrorState = atom({ key: 'ResumeThreadErrorState', default: undefined }); export const chatProfileState = atom({ key: 'ChatProfile', default: undefined }); const sessionIdAtom = atom({ key: 'SessionId', default: uuidv4() }); export const sessionIdState = selector({ key: 'SessionIdSelector', get: ({ get }) => get(sessionIdAtom), set: ({ set }, newValue) => set(sessionIdAtom, newValue instanceof DefaultValue ? uuidv4() : newValue) }); export const sessionState = atom({ key: 'Session', dangerouslyAllowMutability: true, default: undefined }); export const actionState = atom({ key: 'Actions', default: [] }); export const messagesState = atom({ key: 'Messages', dangerouslyAllowMutability: true, default: [] }); export const commandsState = atom({ key: 'Commands', default: [] }); export const modesState = atom({ key: 'Modes', default: [] }); export const tokenCountState = atom({ key: 'TokenCount', default: 0 }); export const loadingState = atom({ key: 'Loading', default: false }); export const askUserState = atom({ key: 'AskUser', default: undefined }); export const wavRecorderState = atom({ key: 'WavRecorder', dangerouslyAllowMutability: true, default: new WavRecorder() }); export const wavStreamPlayerState = atom({ key: 'WavStreamPlayer', dangerouslyAllowMutability: true, default: new WavStreamPlayer() }); export const audioConnectionState = atom<'connecting' | 'on' | 'off'>({ key: 'AudioConnection', default: 'off' }); export const isAiSpeakingState = atom({ key: 'isAiSpeaking', default: false }); export const callFnState = atom({ key: 'CallFn', default: undefined }); export const chatSettingsInputsState = atom({ key: 'ChatSettings', default: [] }); export const chatSettingsDefaultValueSelector = selector({ key: 'ChatSettingsValue/Default', get: ({ get }) => { const chatSettings = get(chatSettingsInputsState); const collectInitialValues = ( inputs: any[], acc: Record ): Record => { if (!Array.isArray(inputs)) { return acc; } inputs.forEach((input) => { if (!input) { return; } if (Array.isArray(input?.inputs) && input.inputs.length > 0) { // Handle tabs collectInitialValues(input.inputs, acc); } else if (input?.id !== undefined) { acc[input.id] = input.initial; } }); return acc; }; return collectInitialValues(chatSettings, {}); } }); export const chatSettingsValueState = atom>({ key: 'ChatSettingsValue', default: chatSettingsDefaultValueSelector }); export const elementState = atom({ key: 'DisplayElements', default: [] }); export const tasklistState = atom({ key: 'TasklistElements', default: [] }); export const firstUserInteraction = atom({ key: 'FirstUserInteraction', default: undefined }); export const userState = atom({ key: 'User', default: undefined }); export const configState = atom({ key: 'ChainlitConfig', default: undefined }); export const authState = atom({ key: 'AuthConfig', default: undefined }); export const threadHistoryState = atom({ key: 'ThreadHistory', default: { threads: undefined, currentThreadId: undefined, timeGroupedThreads: undefined, pageInfo: undefined }, effects: [ ({ setSelf, onSet }: { setSelf: any; onSet: any }) => { onSet( ( newValue: ThreadHistory | undefined, oldValue: ThreadHistory | undefined ) => { let timeGroupedThreads = newValue?.timeGroupedThreads; if ( newValue?.threads && !isEqual(newValue.threads, oldValue?.timeGroupedThreads) ) { timeGroupedThreads = groupByDate(newValue.threads); } setSelf({ ...newValue, timeGroupedThreads }); } ); } ] }); export const sideViewState = atom< { title: string; elements: IMessageElement[]; key?: string } | undefined >({ key: 'SideView', default: undefined }); export const currentThreadIdState = atom({ key: 'CurrentThreadId', default: undefined }); const localStorageEffect = (key: string): AtomEffect => ({ setSelf, onSet }) => { // When the atom is first initialized, try to get its value from localStorage const savedValue = localStorage.getItem(key); if (savedValue != null) { try { setSelf(JSON.parse(savedValue)); } catch (error) { console.error( `Error parsing localStorage value for key "${key}":`, error ); } } // Subscribe to state changes and update localStorage onSet((newValue, _, isReset) => { if (isReset) { localStorage.removeItem(key); } else { localStorage.setItem(key, JSON.stringify(newValue)); } }); }; export const mcpState = atom({ key: 'Mcp', default: [], effects: [localStorageEffect('mcp_storage_key')] }); export const favoriteMessagesState = atom({ key: 'favoriteMessagesState', default: [] });