chore: import upstream snapshot with attribution
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
Copilot Setup Steps / copilot-setup-steps (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
import {
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useResetRecoilState,
|
||||
useSetRecoilState
|
||||
} from 'recoil';
|
||||
import io from 'socket.io-client';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
actionState,
|
||||
askUserState,
|
||||
audioConnectionState,
|
||||
callFnState,
|
||||
chatProfileState,
|
||||
chatSettingsInputsState,
|
||||
chatSettingsValueState,
|
||||
commandsState,
|
||||
currentThreadIdState,
|
||||
elementState,
|
||||
favoriteMessagesState,
|
||||
firstUserInteraction,
|
||||
isAiSpeakingState,
|
||||
loadingState,
|
||||
mcpState,
|
||||
messagesState,
|
||||
modesState,
|
||||
resumeThreadErrorState,
|
||||
sessionIdState,
|
||||
sessionState,
|
||||
sideViewState,
|
||||
tasklistState,
|
||||
threadIdToResumeState,
|
||||
tokenCountState,
|
||||
wavRecorderState,
|
||||
wavStreamPlayerState
|
||||
} from 'src/state';
|
||||
import {
|
||||
IAction,
|
||||
ICommand,
|
||||
IElement,
|
||||
IMessageElement,
|
||||
IMode,
|
||||
IStep,
|
||||
ITasklistElement,
|
||||
IThread
|
||||
} from 'src/types';
|
||||
import {
|
||||
addMessage,
|
||||
deleteMessageById,
|
||||
updateMessageById,
|
||||
updateMessageContentById
|
||||
} from 'src/utils/message';
|
||||
|
||||
import { OutputAudioChunk } from './types/audio';
|
||||
|
||||
import { ChainlitContext } from './context';
|
||||
import type { IToken } from './useChatData';
|
||||
|
||||
const useChatSession = () => {
|
||||
const client = useContext(ChainlitContext);
|
||||
const sessionId = useRecoilValue(sessionIdState);
|
||||
|
||||
const [session, setSession] = useRecoilState(sessionState);
|
||||
const setIsAiSpeaking = useSetRecoilState(isAiSpeakingState);
|
||||
const setAudioConnection = useSetRecoilState(audioConnectionState);
|
||||
const resetChatSettingsValue = useResetRecoilState(chatSettingsValueState);
|
||||
const setChatSettingsValue = useSetRecoilState(chatSettingsValueState);
|
||||
const setFirstUserInteraction = useSetRecoilState(firstUserInteraction);
|
||||
const setLoading = useSetRecoilState(loadingState);
|
||||
const setMcps = useSetRecoilState(mcpState);
|
||||
const wavStreamPlayer = useRecoilValue(wavStreamPlayerState);
|
||||
const wavRecorder = useRecoilValue(wavRecorderState);
|
||||
const setMessages = useSetRecoilState(messagesState);
|
||||
const setAskUser = useSetRecoilState(askUserState);
|
||||
const setCallFn = useSetRecoilState(callFnState);
|
||||
const setCommands = useSetRecoilState(commandsState);
|
||||
const setModes = useSetRecoilState(modesState);
|
||||
const setSideView = useSetRecoilState(sideViewState);
|
||||
const setElements = useSetRecoilState(elementState);
|
||||
const setTasklists = useSetRecoilState(tasklistState);
|
||||
const setActions = useSetRecoilState(actionState);
|
||||
const setChatSettingsInputs = useSetRecoilState(chatSettingsInputsState);
|
||||
const setTokenCount = useSetRecoilState(tokenCountState);
|
||||
const [chatProfile, setChatProfile] = useRecoilState(chatProfileState);
|
||||
const idToResume = useRecoilValue(threadIdToResumeState);
|
||||
const setThreadResumeError = useSetRecoilState(resumeThreadErrorState);
|
||||
const setFavoriteMessages = useSetRecoilState(favoriteMessagesState);
|
||||
|
||||
const [currentThreadId, setCurrentThreadId] =
|
||||
useRecoilState(currentThreadIdState);
|
||||
|
||||
// Use currentThreadId as thread id in websocket header
|
||||
useEffect(() => {
|
||||
if (session?.socket) {
|
||||
session.socket.auth['threadId'] = currentThreadId || '';
|
||||
}
|
||||
}, [currentThreadId]);
|
||||
|
||||
const _connect = useCallback(
|
||||
async ({
|
||||
transports,
|
||||
userEnv
|
||||
}: {
|
||||
transports?: string[];
|
||||
userEnv: Record<string, string>;
|
||||
}) => {
|
||||
const { protocol, host, pathname } = new URL(client.httpEndpoint);
|
||||
const uri = `${protocol}//${host}`;
|
||||
const path =
|
||||
pathname && pathname !== '/'
|
||||
? `${pathname}/ws/socket.io`
|
||||
: '/ws/socket.io';
|
||||
|
||||
try {
|
||||
await client.stickyCookie(sessionId);
|
||||
} catch (err) {
|
||||
console.error(`Failed to set sticky session cookie: ${err}`);
|
||||
}
|
||||
|
||||
const socket = io(uri, {
|
||||
path,
|
||||
withCredentials: true,
|
||||
transports,
|
||||
auth: {
|
||||
clientType: client.type,
|
||||
sessionId,
|
||||
threadId: idToResume || '',
|
||||
userEnv: JSON.stringify(userEnv),
|
||||
chatProfile: chatProfile ? encodeURIComponent(chatProfile) : ''
|
||||
}
|
||||
});
|
||||
setSession((old) => {
|
||||
old?.socket?.removeAllListeners();
|
||||
old?.socket?.close();
|
||||
return {
|
||||
socket
|
||||
};
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('connection_successful');
|
||||
setSession((s) => ({ ...s!, error: false }));
|
||||
socket.emit('fetch_favorites');
|
||||
setMcps((prev) =>
|
||||
prev.map((mcp) => {
|
||||
let promise;
|
||||
if (mcp.clientType === 'sse') {
|
||||
promise = client.connectSseMCP(sessionId, mcp.name, mcp.url!);
|
||||
} else if (mcp.clientType === 'streamable-http') {
|
||||
promise = client.connectStreamableHttpMCP(
|
||||
sessionId,
|
||||
mcp.name,
|
||||
mcp.url!,
|
||||
mcp.headers || {}
|
||||
);
|
||||
} else {
|
||||
promise = client.connectStdioMCP(
|
||||
sessionId,
|
||||
mcp.name,
|
||||
mcp.command!
|
||||
);
|
||||
}
|
||||
promise
|
||||
.then(async ({ success, mcp }) => {
|
||||
setMcps((prev) =>
|
||||
prev.map((existingMcp) => {
|
||||
if (existingMcp.name === mcp.name) {
|
||||
return {
|
||||
...existingMcp,
|
||||
status: success ? 'connected' : 'failed',
|
||||
tools: mcp ? mcp.tools : existingMcp.tools
|
||||
};
|
||||
}
|
||||
return existingMcp;
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
setMcps((prev) =>
|
||||
prev.map((existingMcp) => {
|
||||
if (existingMcp.name === mcp.name) {
|
||||
return {
|
||||
...existingMcp,
|
||||
status: 'failed'
|
||||
};
|
||||
}
|
||||
return existingMcp;
|
||||
})
|
||||
);
|
||||
});
|
||||
return { ...mcp, status: 'connecting' };
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('connect_error', (_) => {
|
||||
setSession((s) => ({ ...s!, error: true }));
|
||||
});
|
||||
|
||||
socket.on('task_start', () => {
|
||||
setLoading(true);
|
||||
});
|
||||
|
||||
socket.on('task_end', () => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
socket.on('reload', () => {
|
||||
socket.emit('clear_session');
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
socket.on('audio_connection', async (state: 'on' | 'off') => {
|
||||
if (state === 'on') {
|
||||
let isFirstChunk = true;
|
||||
const startTime = Date.now();
|
||||
const mimeType = 'pcm16';
|
||||
try {
|
||||
await wavRecorder.begin();
|
||||
await wavStreamPlayer.connect();
|
||||
await wavRecorder.record(async (data) => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
socket.emit('audio_chunk', {
|
||||
isStart: isFirstChunk,
|
||||
mimeType,
|
||||
elapsedTime,
|
||||
data: data.mono
|
||||
});
|
||||
isFirstChunk = false;
|
||||
});
|
||||
wavStreamPlayer.onStop = () => setIsAiSpeaking(false);
|
||||
} catch {
|
||||
try {
|
||||
await wavRecorder.end();
|
||||
} catch {
|
||||
// ignored
|
||||
}
|
||||
await wavStreamPlayer.interrupt();
|
||||
socket.emit('audio_end');
|
||||
setAudioConnection('off');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await wavRecorder.end();
|
||||
await wavStreamPlayer.interrupt();
|
||||
}
|
||||
setAudioConnection(state);
|
||||
});
|
||||
|
||||
socket.on('audio_chunk', (chunk: OutputAudioChunk) => {
|
||||
wavStreamPlayer.add16BitPCM(chunk.data, chunk.track);
|
||||
setIsAiSpeaking(true);
|
||||
});
|
||||
|
||||
socket.on('audio_interrupt', () => {
|
||||
wavStreamPlayer.interrupt();
|
||||
});
|
||||
|
||||
socket.on('resume_thread', (thread: IThread) => {
|
||||
const isReadOnlyView = Boolean(
|
||||
(thread as any)?.metadata?.viewer_read_only
|
||||
);
|
||||
if (!isReadOnlyView && idToResume && thread.id !== idToResume) {
|
||||
window.location.href = `/thread/${thread.id}`;
|
||||
}
|
||||
if (!isReadOnlyView && idToResume) {
|
||||
setCurrentThreadId(thread.id);
|
||||
}
|
||||
let messages: IStep[] = [];
|
||||
for (const step of thread.steps) {
|
||||
messages = addMessage(messages, step);
|
||||
}
|
||||
if (thread.metadata?.chat_profile) {
|
||||
setChatProfile(thread.metadata?.chat_profile);
|
||||
}
|
||||
if (thread.metadata?.chat_settings) {
|
||||
setChatSettingsValue(thread.metadata?.chat_settings);
|
||||
}
|
||||
setMessages(messages);
|
||||
const elements = thread.elements || [];
|
||||
setTasklists(
|
||||
(elements as ITasklistElement[]).filter((e) => e.type === 'tasklist')
|
||||
);
|
||||
setElements(
|
||||
(elements as IMessageElement[]).filter(
|
||||
(e) => ['avatar', 'tasklist'].indexOf(e.type) === -1
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('resume_thread_error', (error?: string) => {
|
||||
setThreadResumeError(error);
|
||||
});
|
||||
|
||||
socket.on('new_message', (message: IStep) => {
|
||||
setMessages((oldMessages) => addMessage(oldMessages, message));
|
||||
});
|
||||
|
||||
socket.on(
|
||||
'first_interaction',
|
||||
(event: { interaction: string; thread_id: string }) => {
|
||||
setFirstUserInteraction(event.interaction);
|
||||
setCurrentThreadId(event.thread_id);
|
||||
}
|
||||
);
|
||||
|
||||
socket.on('update_message', (message: IStep) => {
|
||||
setMessages((oldMessages) =>
|
||||
updateMessageById(oldMessages, message.id, message)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('delete_message', (message: IStep) => {
|
||||
setMessages((oldMessages) =>
|
||||
deleteMessageById(oldMessages, message.id)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on('stream_start', (message: IStep) => {
|
||||
setMessages((oldMessages) => addMessage(oldMessages, message));
|
||||
});
|
||||
|
||||
socket.on(
|
||||
'stream_token',
|
||||
({ id, token, isSequence, isInput }: IToken) => {
|
||||
setMessages((oldMessages) =>
|
||||
updateMessageContentById(
|
||||
oldMessages,
|
||||
id,
|
||||
token,
|
||||
isSequence,
|
||||
isInput
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
socket.on('ask', ({ msg, spec }, callback) => {
|
||||
setAskUser({ spec, callback, parentId: msg.parentId });
|
||||
setMessages((oldMessages) => addMessage(oldMessages, msg));
|
||||
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
socket.on('ask_timeout', () => {
|
||||
setAskUser(undefined);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
socket.on('clear_ask', () => {
|
||||
setAskUser(undefined);
|
||||
});
|
||||
|
||||
socket.on('call_fn', ({ name, args }, callback) => {
|
||||
setCallFn({ name, args, callback });
|
||||
});
|
||||
|
||||
socket.on('clear_call_fn', () => {
|
||||
setCallFn(undefined);
|
||||
});
|
||||
|
||||
socket.on('call_fn_timeout', () => {
|
||||
setCallFn(undefined);
|
||||
});
|
||||
|
||||
socket.on('chat_settings', (inputs: any) => {
|
||||
setChatSettingsInputs(inputs);
|
||||
resetChatSettingsValue();
|
||||
});
|
||||
|
||||
socket.on('set_commands', (commands: ICommand[]) => {
|
||||
setCommands(commands);
|
||||
});
|
||||
|
||||
socket.on('set_modes', (modes: IMode[]) => {
|
||||
setModes(modes);
|
||||
});
|
||||
|
||||
socket.on('set_favorites', (steps: IStep[]) => {
|
||||
setFavoriteMessages(steps);
|
||||
});
|
||||
|
||||
socket.on('set_sidebar_title', (title: string) => {
|
||||
setSideView((prev) => {
|
||||
if (prev?.title === title) return prev;
|
||||
return { title, elements: prev?.elements || [] };
|
||||
});
|
||||
});
|
||||
|
||||
socket.on(
|
||||
'set_sidebar_elements',
|
||||
({ elements, key }: { elements: IMessageElement[]; key?: string }) => {
|
||||
if (!elements.length) {
|
||||
setSideView(undefined);
|
||||
} else {
|
||||
elements.forEach((element) => {
|
||||
if (!element.url && element.chainlitKey) {
|
||||
element.url = client.getElementUrl(
|
||||
element.chainlitKey,
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
});
|
||||
setSideView((prev) => {
|
||||
if (prev?.key === key) return prev;
|
||||
return { title: prev?.title || '', elements: elements, key };
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
socket.on('element', (element: IElement) => {
|
||||
if (!element.url && element.chainlitKey) {
|
||||
element.url = client.getElementUrl(element.chainlitKey, sessionId);
|
||||
}
|
||||
|
||||
if (element.type === 'tasklist') {
|
||||
setTasklists((old) => {
|
||||
const index = old.findIndex((e) => e.id === element.id);
|
||||
if (index === -1) {
|
||||
return [...old, element];
|
||||
} else {
|
||||
return [...old.slice(0, index), element, ...old.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setElements((old) => {
|
||||
const index = old.findIndex((e) => e.id === element.id);
|
||||
if (index === -1) {
|
||||
return [...old, element];
|
||||
} else {
|
||||
return [...old.slice(0, index), element, ...old.slice(index + 1)];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('remove_element', (remove: { id: string }) => {
|
||||
setElements((old) => {
|
||||
return old.filter((e) => e.id !== remove.id);
|
||||
});
|
||||
setTasklists((old) => {
|
||||
return old.filter((e) => e.id !== remove.id);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('action', (action: IAction) => {
|
||||
setActions((old) => [...old, action]);
|
||||
});
|
||||
|
||||
socket.on('remove_action', (action: IAction) => {
|
||||
setActions((old) => {
|
||||
const index = old.findIndex((a) => a.id === action.id);
|
||||
if (index === -1) return old;
|
||||
return [...old.slice(0, index), ...old.slice(index + 1)];
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('token_usage', (count: number) => {
|
||||
setTokenCount((old) => old + count);
|
||||
});
|
||||
|
||||
socket.on('window_message', (data: any) => {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(data, '*');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('toast', (data: { message: string; type: string }) => {
|
||||
if (!data.message) {
|
||||
console.warn('No message received for toast.');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case 'info':
|
||||
toast.info(data.message);
|
||||
break;
|
||||
case 'error':
|
||||
toast.error(data.message);
|
||||
break;
|
||||
case 'success':
|
||||
toast.success(data.message);
|
||||
break;
|
||||
case 'warning':
|
||||
toast.warning(data.message);
|
||||
break;
|
||||
default:
|
||||
toast(data.message);
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
[setSession, sessionId, idToResume, chatProfile]
|
||||
);
|
||||
|
||||
const connect = useCallback(debounce(_connect, 200), [_connect]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
if (session?.socket) {
|
||||
session.socket.removeAllListeners();
|
||||
session.socket.close();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
return {
|
||||
connect,
|
||||
disconnect,
|
||||
session,
|
||||
sessionId,
|
||||
chatProfile,
|
||||
idToResume,
|
||||
setChatProfile
|
||||
};
|
||||
};
|
||||
|
||||
export { useChatSession };
|
||||
Reference in New Issue
Block a user