🏄‍♂️ refactor: Optimize Reasoning UI & Token Streaming (#5546)

*  feat: Implement Show Thinking feature; refactor: testing thinking render optimizations

*  feat: Refactor Thinking component styles and enhance Markdown rendering

* chore: add back removed code, revert type changes

* chore: Add back resetCounter effect to Markdown component for improved code block indexing

* chore: bump @librechat/agents and google langchain packages

* WIP: reasoning type updates

* WIP: first pass, reasoning content blocks

* chore: revert code

* chore: bump @librechat/agents

* refactor: optimize reasoning tag handling

* style: ul indent padding

* feat: add Reasoning component to handle reasoning display

* feat: first pass, content reasoning part styling

* refactor: add content placeholder for endpoints using new stream handler

* refactor: only cache messages when requesting stream audio

* fix: circular dep.

* fix: add default param

* refactor: tts, only request after message stream, fix chrome autoplay

* style: update label for submitting state and add localization for 'Thinking...'

* fix: improve global audio pause logic and reset active run ID

* fix: handle artifact edge cases

* fix: remove unnecessary console log from artifact update test

* feat: add support for continued message handling with new streaming method

---------

Co-authored-by: Marco Beretta <81851188+berry-13@users.noreply.github.com>
This commit is contained in:
Danny Avila 2025-01-29 19:46:58 -05:00 committed by GitHub
parent d60a149ad9
commit 591a019766
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 1791 additions and 726 deletions

View file

@ -6,12 +6,13 @@ import store from '~/store';
function usePauseGlobalAudio(index = 0) {
/* Global Audio Variables */
const setAudioRunId = useSetRecoilState(store.audioRunFamily(index));
const setActiveRunId = useSetRecoilState(store.activeRunFamily(index));
const setGlobalIsPlaying = useSetRecoilState(store.globalAudioPlayingFamily(index));
const setIsGlobalAudioFetching = useSetRecoilState(store.globalAudioFetchingFamily(index));
const [globalAudioURL, setGlobalAudioURL] = useRecoilState(store.globalAudioURLFamily(index));
const pauseGlobalAudio = useCallback(() => {
if (globalAudioURL) {
if (globalAudioURL != null && globalAudioURL !== '') {
const globalAudio = document.getElementById(globalAudioId);
if (globalAudio) {
console.log('Pausing global audio', globalAudioURL);
@ -21,14 +22,16 @@ function usePauseGlobalAudio(index = 0) {
URL.revokeObjectURL(globalAudioURL);
setIsGlobalAudioFetching(false);
setGlobalAudioURL(null);
setActiveRunId(null);
setAudioRunId(null);
}
}, [
setAudioRunId,
setActiveRunId,
globalAudioURL,
setGlobalAudioURL,
setGlobalIsPlaying,
setIsGlobalAudioFetching,
setAudioRunId,
]);
return { pauseGlobalAudio };

View file

@ -4,9 +4,9 @@ import {
Constants,
QueryKeys,
ContentTypes,
EModelEndpoint,
parseCompactConvo,
isAssistantsEndpoint,
EModelEndpoint,
} from 'librechat-data-provider';
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
import type {
@ -31,6 +31,15 @@ const logChatRequest = (request: Record<string, unknown>) => {
logger.log('=====================================');
};
const usesContentStream = (endpoint: EModelEndpoint | undefined, endpointType?: string) => {
if (endpointType === EModelEndpoint.custom) {
return true;
}
if (endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI) {
return true;
}
};
export default function useChatFunctions({
index = 0,
files,
@ -219,8 +228,8 @@ export default function useChatFunctions({
unfinished: false,
isCreatedByUser: false,
isEdited: isEditOrContinue,
iconURL: convo.iconURL,
model: convo.model,
iconURL: convo?.iconURL,
model: convo?.model,
error: false,
};
@ -247,6 +256,17 @@ export default function useChatFunctions({
},
];
setShowStopButton(true);
} else if (usesContentStream(endpoint, endpointType)) {
initialResponse.text = '';
initialResponse.content = [
{
type: ContentTypes.TEXT,
[ContentTypes.TEXT]: {
value: responseText,
},
},
];
setShowStopButton(true);
} else {
setShowStopButton(true);
}

View file

@ -87,7 +87,7 @@ function useTextToSpeechExternal({
setDownloadFile(false);
};
const { mutate: processAudio, isLoading: isProcessing } = useTextToSpeechMutation({
const { mutate: processAudio } = useTextToSpeechMutation({
onMutate: (variables) => {
const inputText = (variables.get('input') ?? '') as string;
if (inputText.length >= 4096) {
@ -178,13 +178,14 @@ function useTextToSpeechExternal({
promiseAudioRef.current = null;
setIsSpeaking(false);
}
}, []);
}, [setIsSpeaking]);
useEffect(() => cancelPromiseSpeech, [cancelPromiseSpeech]);
const isLoading = useMemo(() => {
return isProcessing || (isLast && globalIsFetching && !globalIsPlaying);
}, [isProcessing, globalIsFetching, globalIsPlaying, isLast]);
const isLoading = useMemo(
() => isLast && globalIsFetching && !globalIsPlaying,
[globalIsFetching, globalIsPlaying, isLast],
);
const { data: voicesData = [] } = useVoicesQuery();

View file

@ -33,8 +33,11 @@ type TStepEvent = {
type MessageDeltaUpdate = { type: ContentTypes.TEXT; text: string; tool_call_ids?: string[] };
type ReasoningDeltaUpdate = { type: ContentTypes.THINK; think: string };
type AllContentTypes =
| ContentTypes.TEXT
| ContentTypes.THINK
| ContentTypes.TOOL_CALL
| ContentTypes.IMAGE_FILE
| ContentTypes.IMAGE_URL
@ -84,6 +87,18 @@ export default function useStepHandler({
if (contentPart.tool_call_ids != null) {
update.tool_call_ids = contentPart.tool_call_ids;
}
updatedContent[index] = update;
} else if (
contentType.startsWith(ContentTypes.THINK) &&
ContentTypes.THINK in contentPart &&
typeof contentPart.think === 'string'
) {
const currentContent = updatedContent[index] as ReasoningDeltaUpdate;
const update: ReasoningDeltaUpdate = {
type: ContentTypes.THINK,
think: (currentContent.think || '') + contentPart.think,
};
updatedContent[index] = update;
} else if (contentType === ContentTypes.IMAGE_URL && 'image_url' in contentPart) {
const currentContent = updatedContent[index] as {
@ -215,6 +230,28 @@ export default function useStepHandler({
const updatedResponse = updateContent(response, runStep.index, contentPart);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);
}
} else if (event === 'on_reasoning_delta') {
const reasoningDelta = data as Agents.ReasoningDeltaEvent;
const runStep = stepMap.current.get(reasoningDelta.id);
const responseMessageId = runStep?.runId ?? '';
if (!runStep || !responseMessageId) {
console.warn('No run step or runId found for reasoning delta event');
return;
}
const response = messageMap.current.get(responseMessageId);
if (response && reasoningDelta.delta.content != null) {
const contentPart = Array.isArray(reasoningDelta.delta.content)
? reasoningDelta.delta.content[0]
: reasoningDelta.delta.content;
const updatedResponse = updateContent(response, runStep.index, contentPart);
messageMap.current.set(responseMessageId, updatedResponse);
const currentMessages = getMessages() || [];
setMessages([...currentMessages.slice(0, -1), updatedResponse]);