mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

* feat: Add CodeArtifacts component to Beta settings tab * chore: Update npm dependency to @codesandbox/sandpack-react@2.18.2 * WIP: artifacts first pass * WIP first pass remark-directive * chore: revert markdown to original component + new artifacts rendering * refactor: first pass rewrite * refactor: add throttling * first pass styling * style: Add Radix Tabs, more styling changes * feat: second pass * style: code styling * fix: package markdown fixes * feat: Add useEffect hook to Artifacts component for visibility control, slide in animation * fix: only set artifact if there is content * refactor: typing and make latest artifact active if the number of artifacts changed * feat: artifacts + shadcnui * feat: Add Copy Code button to Artifacts component * feat: first pass streaming updates * refactor: optimize ordering of artifacts in Artifacts component * refactor: optimize ordering of artifacts and add latest artifact activation in Artifacts component * refactor: add order prop to Artifact * feat: update to latest, use update time for ordering * refactor: optimize ordering of artifacts and activate latest artifact in Artifacts component * wip: remove thinking text and artifact formatting if empty * refactor: optimize Markdown rendering and add support for code artifacts * feat: global state for current artifact Id and set on artifact preview click * refactor: Rename CodePreview component to ArtifactButton * refactor: apply growth to artifact frame so artifact preview can take full space * refactor: remove artifactIdsState * refactor: nullify artifact state and reset on empty conversation * feat: reset artifact state on conversation change * feat: artifacts system prompt in backend * refactor: update UI artifact toggle label to match localization key * style: remove ArtifactButton inline-block styling * feat: memoize ArtifactPreview, add html support * refactor: abstract out components * chore: bump react-resizable-panel * refactor: resizable panel order props * fix: side panel resizing crashes * style: temporarily remove scrolling, add better styling * chore: remove thinking for now * chore: preprocess artifacts for now * feat: Add auto scrolling to CodeMarkdown (artifacts) * feat: autoswitch to preview * feat: auto switch to code, adjust prompt, remove unused code * feat: refresh button * feat: open/close artifacts * wip: mermaid * refactor: w-fit Artifact button * chore: organize code * feat: first pass mermaid * refactor: improve panning logic in MermaidDiagram component * feat: center/zoom on first render * refactor: add centering with reset button * style: mermaid styling * refactor: add back MermaidDiagram * fix: static/html template * fix: mermaid * add examples to artifacts prompt * refactor: fix CodeBar plugin prop logic * refactor: remove unnecessary mention of artifacts when not requested * fix: remove preprocessCodeArtifacts function and fix imports * feat: improve artifacts guidelines and remove unnecessary mentions * refactor: improve artifacts guidelines and remove unnecessary mentions * chore: uninstall unused packages * chore: bump vite * chore: update three dependency to version 0.167.1 * refactor: move beta settings, add additional artifacts toggles * feat: artifacts mode toggles * refactor: adjust prompt * feat: shadcnui instructions * feat: code artifacts custom prompt mode * chore: Update artifacts UI labels and instructions localizations * refactor: Remove unused code in Markdown component
279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Constants,
|
|
QueryKeys,
|
|
ContentTypes,
|
|
parseCompactConvo,
|
|
isAssistantsEndpoint,
|
|
} from 'librechat-data-provider';
|
|
import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil';
|
|
import type {
|
|
TMessage,
|
|
TSubmission,
|
|
TConversation,
|
|
TEndpointOption,
|
|
TEndpointsConfig,
|
|
} from 'librechat-data-provider';
|
|
import type { SetterOrUpdater } from 'recoil';
|
|
import type { TAskFunction, ExtendedFile } from '~/common';
|
|
import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete';
|
|
import useGetSender from '~/hooks/Conversations/useGetSender';
|
|
import { getArtifactsMode } from '~/utils/artifacts';
|
|
import { getEndpointField, logger } from '~/utils';
|
|
import useUserKey from '~/hooks/Input/useUserKey';
|
|
import store from '~/store';
|
|
|
|
export default function useChatFunctions({
|
|
index = 0,
|
|
files,
|
|
setFiles,
|
|
getMessages,
|
|
setMessages,
|
|
isSubmitting,
|
|
conversation,
|
|
latestMessage,
|
|
setSubmission,
|
|
setLatestMessage,
|
|
}: {
|
|
index?: number;
|
|
isSubmitting: boolean;
|
|
paramId?: string | undefined;
|
|
conversation: TConversation | null;
|
|
latestMessage: TMessage | null;
|
|
getMessages: () => TMessage[] | undefined;
|
|
setMessages: (messages: TMessage[]) => void;
|
|
files?: Map<string, ExtendedFile>;
|
|
setFiles?: SetterOrUpdater<Map<string, ExtendedFile>>;
|
|
setSubmission: SetterOrUpdater<TSubmission | null>;
|
|
setLatestMessage?: SetterOrUpdater<TMessage | null>;
|
|
}) {
|
|
const codeArtifacts = useRecoilValue(store.codeArtifacts);
|
|
const includeShadcnui = useRecoilValue(store.includeShadcnui);
|
|
const customPromptMode = useRecoilValue(store.customPromptMode);
|
|
const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1));
|
|
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
|
|
const setFilesToDelete = useSetFilesToDelete();
|
|
const getSender = useGetSender();
|
|
|
|
const queryClient = useQueryClient();
|
|
const { getExpiry } = useUserKey(conversation?.endpoint ?? '');
|
|
|
|
const ask: TAskFunction = (
|
|
{
|
|
text,
|
|
overrideConvoId,
|
|
overrideUserMessageId,
|
|
parentMessageId = null,
|
|
conversationId = null,
|
|
messageId = null,
|
|
},
|
|
{
|
|
editedText = null,
|
|
editedMessageId = null,
|
|
resubmitFiles = false,
|
|
isRegenerate = false,
|
|
isContinued = false,
|
|
isEdited = false,
|
|
overrideMessages,
|
|
} = {},
|
|
) => {
|
|
setShowStopButton(false);
|
|
resetLatestMultiMessage();
|
|
if (!!isSubmitting || text === '') {
|
|
return;
|
|
}
|
|
|
|
const endpoint = conversation?.endpoint;
|
|
if (endpoint === null) {
|
|
console.error('No endpoint available');
|
|
return;
|
|
}
|
|
|
|
conversationId = conversationId ?? conversation?.conversationId ?? null;
|
|
if (conversationId == 'search') {
|
|
console.error('cannot send any message under search view!');
|
|
return;
|
|
}
|
|
|
|
if (isContinued && !latestMessage) {
|
|
console.error('cannot continue AI message without latestMessage!');
|
|
return;
|
|
}
|
|
|
|
const isEditOrContinue = isEdited || isContinued;
|
|
|
|
let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? [];
|
|
|
|
// construct the query message
|
|
// this is not a real messageId, it is used as placeholder before real messageId returned
|
|
text = text.trim();
|
|
const intermediateId = overrideUserMessageId ?? v4();
|
|
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
|
|
|
|
logger.dir('Ask function called with:', {
|
|
index,
|
|
latestMessage,
|
|
conversationId,
|
|
intermediateId,
|
|
parentMessageId,
|
|
currentMessages,
|
|
});
|
|
logger.log('=====================================');
|
|
|
|
if (conversationId == Constants.NEW_CONVO) {
|
|
parentMessageId = Constants.NO_PARENT;
|
|
currentMessages = [];
|
|
conversationId = null;
|
|
}
|
|
|
|
const parentMessage = currentMessages.find(
|
|
(msg) => msg.messageId === latestMessage?.parentMessageId,
|
|
);
|
|
|
|
let thread_id = parentMessage?.thread_id ?? latestMessage?.thread_id;
|
|
if (!thread_id) {
|
|
thread_id = currentMessages.find((message) => message.thread_id)?.thread_id;
|
|
}
|
|
|
|
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
|
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
|
|
|
|
// set the endpoint option
|
|
const convo = parseCompactConvo({
|
|
endpoint,
|
|
endpointType,
|
|
conversation: conversation ?? {},
|
|
});
|
|
|
|
const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {};
|
|
const endpointOption = {
|
|
...convo,
|
|
endpoint,
|
|
thread_id,
|
|
endpointType,
|
|
overrideConvoId,
|
|
key: getExpiry(),
|
|
modelDisplayLabel,
|
|
overrideUserMessageId,
|
|
artifacts: getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }),
|
|
} as TEndpointOption;
|
|
const responseSender = getSender({ model: conversation?.model, ...endpointOption });
|
|
|
|
const currentMsg: TMessage = {
|
|
text,
|
|
sender: 'User',
|
|
isCreatedByUser: true,
|
|
parentMessageId,
|
|
conversationId,
|
|
messageId: isContinued && messageId ? messageId : intermediateId,
|
|
thread_id,
|
|
error: false,
|
|
};
|
|
|
|
const reuseFiles = (isRegenerate || resubmitFiles) && parentMessage?.files;
|
|
if (setFiles && reuseFiles && parentMessage.files?.length) {
|
|
currentMsg.files = parentMessage.files;
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
} else if (setFiles && files && files.size > 0) {
|
|
currentMsg.files = Array.from(files.values()).map((file) => ({
|
|
file_id: file.file_id,
|
|
filepath: file.filepath,
|
|
type: file.type || '', // Ensure type is not undefined
|
|
height: file.height,
|
|
width: file.width,
|
|
}));
|
|
setFiles(new Map());
|
|
setFilesToDelete({});
|
|
}
|
|
|
|
// construct the placeholder response message
|
|
const generation = editedText ?? latestMessage?.text ?? '';
|
|
const responseText = isEditOrContinue ? generation : '';
|
|
|
|
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
|
|
const initialResponse: TMessage = {
|
|
sender: responseSender,
|
|
text: responseText,
|
|
endpoint: endpoint ?? '',
|
|
parentMessageId: isRegenerate ? messageId : intermediateId,
|
|
messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`,
|
|
thread_id,
|
|
conversationId,
|
|
unfinished: false,
|
|
isCreatedByUser: false,
|
|
isEdited: isEditOrContinue,
|
|
iconURL: convo.iconURL,
|
|
error: false,
|
|
};
|
|
|
|
if (isAssistantsEndpoint(endpoint)) {
|
|
initialResponse.model = conversation?.assistant_id ?? '';
|
|
initialResponse.text = '';
|
|
initialResponse.content = [
|
|
{
|
|
type: ContentTypes.TEXT,
|
|
[ContentTypes.TEXT]: {
|
|
value: responseText,
|
|
},
|
|
},
|
|
];
|
|
} else {
|
|
setShowStopButton(true);
|
|
}
|
|
|
|
if (isContinued) {
|
|
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
|
|
}
|
|
|
|
const submission: TSubmission = {
|
|
conversation: {
|
|
...conversation,
|
|
conversationId,
|
|
},
|
|
endpointOption,
|
|
userMessage: {
|
|
...currentMsg,
|
|
generation,
|
|
responseMessageId,
|
|
overrideParentMessageId: isRegenerate ? messageId : null,
|
|
},
|
|
messages: currentMessages,
|
|
isEdited: isEditOrContinue,
|
|
isContinued,
|
|
isRegenerate,
|
|
initialResponse,
|
|
};
|
|
|
|
if (isRegenerate) {
|
|
setMessages([...submission.messages, initialResponse]);
|
|
} else {
|
|
setMessages([...submission.messages, currentMsg, initialResponse]);
|
|
}
|
|
if (index === 0 && setLatestMessage) {
|
|
setLatestMessage(initialResponse);
|
|
}
|
|
|
|
setSubmission(submission);
|
|
logger.dir('message_stream', submission, { depth: null });
|
|
};
|
|
|
|
const regenerate = ({ parentMessageId }) => {
|
|
const messages = getMessages();
|
|
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
|
|
|
|
if (parentMessage && parentMessage.isCreatedByUser) {
|
|
ask({ ...parentMessage }, { isRegenerate: true });
|
|
} else {
|
|
console.error(
|
|
'Failed to regenerate the message: parentMessage not found or not created by user.',
|
|
);
|
|
}
|
|
};
|
|
|
|
return {
|
|
ask,
|
|
regenerate,
|
|
};
|
|
}
|