mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🧹 chore: pre-release cleanup 2 (#3600)
* refactor: scrollToEnd * fix(validateConvoAccess): search conversation by ID for proper validation * feat: Add unique index for conversationId and user in convoSchema * refactor: Update font sizes 1 rem -> font-size-base in style.css * fix: Assistants map type issues * refactor: Remove obsolete scripts * fix: Update DropdownNoState component to handle both string and OptionType values * refactor: Remove config/loader.js file * fix: remove crypto.randomBytes(); refactor: Create reusable function for generating token and hash
This commit is contained in:
parent
6fead1005b
commit
1ff4841603
20 changed files with 172 additions and 637 deletions
|
@ -2,6 +2,20 @@ const Conversation = require('./schema/convoSchema');
|
||||||
const { getMessages, deleteMessages } = require('./Message');
|
const { getMessages, deleteMessages } = require('./Message');
|
||||||
const logger = require('~/config/winston');
|
const logger = require('~/config/winston');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches for a conversation by conversationId and returns a lean document with only conversationId and user.
|
||||||
|
* @param {string} conversationId - The conversation's ID.
|
||||||
|
* @returns {Promise<{conversationId: string, user: string} | null>} The conversation object with selected fields or null if not found.
|
||||||
|
*/
|
||||||
|
const searchConversation = async (conversationId) => {
|
||||||
|
try {
|
||||||
|
return await Conversation.findOne({ conversationId }, 'conversationId user').lean();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[searchConversation] Error searching conversation', error);
|
||||||
|
throw new Error('Error searching conversation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a single conversation for a given user and conversation ID.
|
* Retrieves a single conversation for a given user and conversation ID.
|
||||||
* @param {string} user - The user's ID.
|
* @param {string} user - The user's ID.
|
||||||
|
@ -19,6 +33,7 @@ const getConvo = async (user, conversationId) => {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Conversation,
|
Conversation,
|
||||||
|
searchConversation,
|
||||||
/**
|
/**
|
||||||
* Saves a conversation to the database.
|
* Saves a conversation to the database.
|
||||||
* @param {Object} req - The request object.
|
* @param {Object} req - The request object.
|
||||||
|
|
|
@ -61,6 +61,7 @@ if (process.env.MEILI_HOST && process.env.MEILI_MASTER_KEY) {
|
||||||
}
|
}
|
||||||
|
|
||||||
convoSchema.index({ createdAt: 1, updatedAt: 1 });
|
convoSchema.index({ createdAt: 1, updatedAt: 1 });
|
||||||
|
convoSchema.index({ conversationId: 1, user: 1 }, { unique: true });
|
||||||
|
|
||||||
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
const Conversation = mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
const { Constants, ViolationTypes, Time } = require('librechat-data-provider');
|
const { Constants, ViolationTypes, Time } = require('librechat-data-provider');
|
||||||
|
const { searchConversation } = require('~/models/Conversation');
|
||||||
const denyRequest = require('~/server/middleware/denyRequest');
|
const denyRequest = require('~/server/middleware/denyRequest');
|
||||||
const { logViolation, getLogStores } = require('~/cache');
|
const { logViolation, getLogStores } = require('~/cache');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
const { getConvo } = require('~/models');
|
|
||||||
|
|
||||||
const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {};
|
const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {};
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ const validateConvoAccess = async (req, res, next) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = await getConvo(userId, conversationId);
|
const conversation = await searchConversation(conversationId);
|
||||||
|
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { webcrypto } = require('node:crypto');
|
||||||
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
const { SystemRoles, errorsToString } = require('librechat-data-provider');
|
||||||
const {
|
const {
|
||||||
findUser,
|
findUser,
|
||||||
|
@ -53,14 +54,23 @@ const logoutUser = async (userId, refreshToken) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Token and corresponding Hash for verification
|
||||||
|
* @returns {[string, string]}
|
||||||
|
*/
|
||||||
|
const createTokenHash = () => {
|
||||||
|
const token = Buffer.from(webcrypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
||||||
|
const hash = bcrypt.hashSync(token, 10);
|
||||||
|
return [token, hash];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Verification Email
|
* Send Verification Email
|
||||||
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
|
* @param {Partial<MongoUser> & { _id: ObjectId, email: string, name: string}} user
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const sendVerificationEmail = async (user) => {
|
const sendVerificationEmail = async (user) => {
|
||||||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
const [verifyToken, hash] = createTokenHash();
|
||||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
|
||||||
|
|
||||||
const verificationLink = `${
|
const verificationLink = `${
|
||||||
domains.client
|
domains.client
|
||||||
|
@ -226,8 +236,7 @@ const requestPasswordReset = async (req) => {
|
||||||
await token.deleteOne();
|
await token.deleteOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
let resetToken = crypto.randomBytes(32).toString('hex');
|
const [resetToken, hash] = createTokenHash();
|
||||||
const hash = bcrypt.hashSync(resetToken, 10);
|
|
||||||
|
|
||||||
await new Token({
|
await new Token({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
|
@ -365,8 +374,7 @@ const resendVerificationEmail = async (req) => {
|
||||||
return { status: 200, message: genericVerificationMessage };
|
return { status: 200, message: genericVerificationMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
let verifyToken = crypto.randomBytes(32).toString('hex');
|
const [verifyToken, hash] = createTokenHash();
|
||||||
const hash = bcrypt.hashSync(verifyToken, 10);
|
|
||||||
|
|
||||||
const verificationLink = `${
|
const verificationLink = `${
|
||||||
domains.client
|
domains.client
|
||||||
|
|
|
@ -31,10 +31,10 @@ export default function Landing({ Header }: { Header?: ReactNode }) {
|
||||||
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
|
|
||||||
const isAssistant = isAssistantsEndpoint(endpoint);
|
const isAssistant = isAssistantsEndpoint(endpoint);
|
||||||
const assistant = isAssistant && assistantMap[endpoint][assistant_id ?? ''];
|
const assistant = isAssistant ? assistantMap?.[endpoint][assistant_id ?? ''] : undefined;
|
||||||
const assistantName = (assistant && assistant.name) || '';
|
const assistantName = assistant && assistant.name;
|
||||||
const assistantDesc = (assistant && assistant.description) || '';
|
const assistantDesc = assistant && assistant.description;
|
||||||
const avatar = (assistant && (assistant.metadata?.avatar as string)) || '';
|
const avatar = assistant && (assistant.metadata?.avatar as string);
|
||||||
|
|
||||||
const containerClassName =
|
const containerClassName =
|
||||||
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
|
'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||||
|
import { useMessageHelpers, useLocalize } from '~/hooks';
|
||||||
import ContentParts from './Content/ContentParts';
|
import ContentParts from './Content/ContentParts';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
import { useMessageHelpers } from '~/hooks';
|
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import HoverButtons from './HoverButtons';
|
import HoverButtons from './HoverButtons';
|
||||||
|
@ -12,6 +12,7 @@ import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function Message(props: TMessageProps) {
|
export default function Message(props: TMessageProps) {
|
||||||
|
const localize = useLocalize();
|
||||||
const { message, siblingIdx, siblingCount, setSiblingIdx, currentEditId, setCurrentEditId } =
|
const { message, siblingIdx, siblingCount, setSiblingIdx, currentEditId, setCurrentEditId } =
|
||||||
props;
|
props;
|
||||||
|
|
||||||
|
@ -31,7 +32,6 @@ export default function Message(props: TMessageProps) {
|
||||||
regenerateMessage,
|
regenerateMessage,
|
||||||
} = useMessageHelpers(props);
|
} = useMessageHelpers(props);
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
const fontSize = useRecoilValue(store.fontSize);
|
||||||
|
|
||||||
const { content, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {};
|
const { content, children, messageId = null, isCreatedByUser, error, unfinished } = message ?? {};
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -59,12 +59,13 @@ export default function Message(props: TMessageProps) {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full flex-col',
|
'relative flex w-full flex-col',
|
||||||
isCreatedByUser != null ? '' : 'agent-turn',
|
isCreatedByUser === true ? '' : 'agent-turn',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn('select-none font-semibold', fontSize)}>
|
<div className={cn('select-none font-semibold', fontSize)}>
|
||||||
{/* TODO: LOCALIZE */}
|
{isCreatedByUser === true
|
||||||
{isCreatedByUser != null ? 'You' : (assistant && assistant.name) ?? 'Assistant'}
|
? localize('com_user_message')
|
||||||
|
: (assistant && assistant.name) ?? localize('com_ui_assistant')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-col gap-1 md:gap-3">
|
<div className="flex-col gap-1 md:gap-3">
|
||||||
<div className="flex max-w-full flex-grow flex-col gap-0">
|
<div className="flex max-w-full flex-grow flex-col gap-0">
|
||||||
|
@ -76,7 +77,7 @@ export default function Message(props: TMessageProps) {
|
||||||
message={message}
|
message={message}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
enterEdit={enterEdit}
|
enterEdit={enterEdit}
|
||||||
error={!!error}
|
error={!!(error ?? false)}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
unfinished={unfinished ?? false}
|
unfinished={unfinished ?? false}
|
||||||
isCreatedByUser={isCreatedByUser ?? true}
|
isCreatedByUser={isCreatedByUser ?? true}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
import { isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import type {
|
import type {
|
||||||
TAssistantsMap,
|
TAssistantsMap,
|
||||||
|
@ -20,7 +21,7 @@ export default function ConvoIcon({
|
||||||
}: {
|
}: {
|
||||||
conversation: TConversation | TPreset | null;
|
conversation: TConversation | TPreset | null;
|
||||||
endpointsConfig: TEndpointsConfig;
|
endpointsConfig: TEndpointsConfig;
|
||||||
assistantMap: TAssistantsMap;
|
assistantMap: TAssistantsMap | undefined;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
context?: 'message' | 'nav' | 'landing' | 'menu-item';
|
context?: 'message' | 'nav' | 'landing' | 'menu-item';
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -29,11 +30,19 @@ export default function ConvoIcon({
|
||||||
const iconURL = conversation?.iconURL;
|
const iconURL = conversation?.iconURL;
|
||||||
let endpoint = conversation?.endpoint;
|
let endpoint = conversation?.endpoint;
|
||||||
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint });
|
||||||
const assistant =
|
const assistant = useMemo(() => {
|
||||||
isAssistantsEndpoint(endpoint) && assistantMap?.[endpoint]?.[conversation?.assistant_id ?? ''];
|
if (!isAssistantsEndpoint(conversation?.endpoint)) {
|
||||||
const assistantName = (assistant && assistant?.name) || '';
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const avatar = (assistant && (assistant?.metadata?.avatar as string)) || '';
|
const endpointKey = conversation?.endpoint ?? '';
|
||||||
|
const assistantId = conversation?.assistant_id ?? '';
|
||||||
|
|
||||||
|
return assistantMap?.[endpointKey] ? assistantMap[endpointKey][assistantId] : undefined;
|
||||||
|
}, [conversation?.endpoint, conversation?.assistant_id, assistantMap]);
|
||||||
|
const assistantName = assistant && (assistant.name ?? '');
|
||||||
|
|
||||||
|
const avatar = (assistant && (assistant.metadata?.avatar as string)) || '';
|
||||||
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL');
|
||||||
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
const iconKey = getIconKey({ endpoint, endpointsConfig, endpointIconURL });
|
||||||
const Icon = icons[iconKey];
|
const Icon = icons[iconKey];
|
||||||
|
|
|
@ -62,12 +62,12 @@ export default function ActionsInput({
|
||||||
const [functions, setFunctions] = useState<FunctionTool[] | null>(null);
|
const [functions, setFunctions] = useState<FunctionTool[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!action?.metadata?.raw_spec) {
|
if (!action?.metadata.raw_spec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setInputValue(action.metadata.raw_spec);
|
setInputValue(action.metadata.raw_spec);
|
||||||
debouncedValidation(action.metadata.raw_spec, handleResult);
|
debouncedValidation(action.metadata.raw_spec, handleResult);
|
||||||
}, [action?.metadata?.raw_spec]);
|
}, [action?.metadata.raw_spec]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!validationResult || !validationResult.status || !validationResult.spec) {
|
if (!validationResult || !validationResult.status || !validationResult.spec) {
|
||||||
|
@ -100,7 +100,7 @@ export default function ActionsInput({
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
showToast({
|
showToast({
|
||||||
message: (error as Error)?.message ?? localize('com_assistants_update_actions_error'),
|
message: (error as Error).message ?? localize('com_assistants_update_actions_error'),
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -180,7 +180,7 @@ export default function ActionsInput({
|
||||||
assistant_id,
|
assistant_id,
|
||||||
endpoint,
|
endpoint,
|
||||||
version,
|
version,
|
||||||
model: assistantMap[endpoint][assistant_id].model,
|
model: assistantMap?.[endpoint][assistant_id].model ?? '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -195,16 +195,32 @@ export default function ActionsInput({
|
||||||
debouncedValidation(newValue, handleResult);
|
debouncedValidation(newValue, handleResult);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitContext = () => {
|
||||||
|
if (updateAction.isLoading) {
|
||||||
|
return <Spinner className="icon-md" />;
|
||||||
|
} else if (action?.action_id.length ?? 0) {
|
||||||
|
return localize('com_ui_update');
|
||||||
|
} else {
|
||||||
|
return localize('com_ui_create');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="mb-1 flex flex-wrap items-center justify-between gap-4">
|
<div className="mb-1 flex flex-wrap items-center justify-between gap-4">
|
||||||
<label className="text-token-text-primary whitespace-nowrap font-medium">Schema</label>
|
<label
|
||||||
|
htmlFor="example-schema"
|
||||||
|
className="text-token-text-primary whitespace-nowrap font-medium"
|
||||||
|
>
|
||||||
|
Schema
|
||||||
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium">
|
{/* <button className="btn btn-neutral border-token-border-light relative h-8 min-w-[100px] rounded-lg font-medium">
|
||||||
<div className="flex w-full items-center justify-center text-xs">Import from URL</div>
|
<div className="flex w-full items-center justify-center text-xs">Import from URL</div>
|
||||||
</button> */}
|
</button> */}
|
||||||
<select
|
<select
|
||||||
|
id="example-schema"
|
||||||
onChange={(e) => console.log(e.target.value)}
|
onChange={(e) => console.log(e.target.value)}
|
||||||
className="border-token-border-medium h-8 min-w-[100px] rounded-lg border bg-transparent px-2 py-0 text-sm"
|
className="border-token-border-medium h-8 min-w-[100px] rounded-lg border bg-transparent px-2 py-0 text-sm"
|
||||||
>
|
>
|
||||||
|
@ -250,23 +266,15 @@ export default function ActionsInput({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="mb-1.5 flex items-center">
|
|
||||||
<span className="" data-state="closed">
|
|
||||||
<label className="text-token-text-primary block font-medium">
|
|
||||||
{localize('com_ui_privacy_policy')}
|
|
||||||
</label>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-md border border-gray-300 px-3 py-2 shadow-none focus-within:border-gray-800 focus-within:ring-1 focus-within:ring-gray-800 dark:border-gray-700 dark:bg-gray-700 dark:focus-within:border-gray-500 dark:focus-within:ring-gray-500">
|
<div className="rounded-md border border-gray-300 px-3 py-2 shadow-none focus-within:border-gray-800 focus-within:ring-1 focus-within:ring-gray-800 dark:border-gray-700 dark:bg-gray-700 dark:focus-within:border-gray-500 dark:focus-within:ring-gray-500">
|
||||||
<label
|
<label htmlFor="privacyPolicyUrl" className="block text-xs text-text-secondary">
|
||||||
htmlFor="privacyPolicyUrl"
|
Privacy Policy URL
|
||||||
className="block text-xs font-medium text-gray-900 dark:text-gray-100"
|
</label>
|
||||||
/>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
name="privacyPolicyUrl"
|
name="privacyPolicyUrl"
|
||||||
id="privacyPolicyUrl"
|
id="privacyPolicyUrl"
|
||||||
className="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 shadow-none outline-none focus-within:shadow-none focus-within:outline-none focus-within:ring-0 focus:border-none focus:ring-0 dark:bg-gray-700 dark:text-gray-100 sm:text-sm"
|
className="block w-full border-0 bg-transparent p-0 placeholder-text-secondary shadow-none outline-none focus-within:shadow-none focus-within:outline-none focus-within:ring-0 focus:border-none focus:ring-0 sm:text-sm"
|
||||||
placeholder="https://api.example-weather-app.com/privacy"
|
placeholder="https://api.example-weather-app.com/privacy"
|
||||||
// value=""
|
// value=""
|
||||||
/>
|
/>
|
||||||
|
@ -280,13 +288,7 @@ export default function ActionsInput({
|
||||||
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
|
className="focus:shadow-outline mt-1 flex min-w-[100px] items-center justify-center rounded bg-green-500 px-4 py-2 font-semibold text-white hover:bg-green-400 focus:border-green-500 focus:outline-none focus:ring-0 disabled:bg-green-400"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{updateAction.isLoading ? (
|
{submitContext()}
|
||||||
<Spinner className="icon-md" />
|
|
||||||
) : action?.action_id ? (
|
|
||||||
localize('com_ui_update')
|
|
||||||
) : (
|
|
||||||
localize('com_ui_create')
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -51,7 +51,7 @@ function Avatar({
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
const activeModel = useMemo(() => {
|
const activeModel = useMemo(() => {
|
||||||
return assistantsMap[endpoint][assistant_id ?? '']?.model ?? '';
|
return assistantsMap?.[endpoint][assistant_id ?? '']?.model ?? '';
|
||||||
}, [assistantsMap, endpoint, assistant_id]);
|
}, [assistantsMap, endpoint, assistant_id]);
|
||||||
|
|
||||||
const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({
|
const { mutate: uploadAvatar } = useUploadAssistantAvatarMutation({
|
||||||
|
@ -59,7 +59,7 @@ function Avatar({
|
||||||
setProgress(0.4);
|
setProgress(0.4);
|
||||||
},
|
},
|
||||||
onSuccess: (data, vars) => {
|
onSuccess: (data, vars) => {
|
||||||
if (!vars.postCreation) {
|
if (vars.postCreation !== true) {
|
||||||
showToast({ message: localize('com_ui_upload_success') });
|
showToast({ message: localize('com_ui_upload_success') });
|
||||||
} else if (lastSeenCreatedId.current !== createMutation.data?.id) {
|
} else if (lastSeenCreatedId.current !== createMutation.data?.id) {
|
||||||
lastSeenCreatedId.current = createMutation.data?.id ?? '';
|
lastSeenCreatedId.current = createMutation.data?.id ?? '';
|
||||||
|
@ -136,9 +136,9 @@ function Avatar({
|
||||||
createMutation.isSuccess &&
|
createMutation.isSuccess &&
|
||||||
input &&
|
input &&
|
||||||
previewUrl &&
|
previewUrl &&
|
||||||
previewUrl?.includes('base64')
|
previewUrl.includes('base64')
|
||||||
);
|
);
|
||||||
if (sharedUploadCondition && lastSeenCreatedId.current === createMutation.data?.id) {
|
if (sharedUploadCondition && lastSeenCreatedId.current === createMutation.data.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,8 +149,8 @@ function Avatar({
|
||||||
formData.append('file', input, input.name);
|
formData.append('file', input, input.name);
|
||||||
formData.append('assistant_id', createMutation.data.id);
|
formData.append('assistant_id', createMutation.data.id);
|
||||||
|
|
||||||
if (typeof createMutation.data?.metadata === 'object') {
|
if (typeof createMutation.data.metadata === 'object') {
|
||||||
formData.append('metadata', JSON.stringify(createMutation.data?.metadata));
|
formData.append('metadata', JSON.stringify(createMutation.data.metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadAvatar({
|
uploadAvatar({
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default function AssistantPanel({
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
showToast({
|
showToast({
|
||||||
message: `${localize('com_assistants_update_error')}${
|
message: `${localize('com_assistants_update_error')}${
|
||||||
error?.message ? ` ${localize('com_ui_error')}: ${error?.message}` : ''
|
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||||
}`,
|
}`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
|
@ -119,7 +119,7 @@ export default function AssistantPanel({
|
||||||
const error = err as Error;
|
const error = err as Error;
|
||||||
showToast({
|
showToast({
|
||||||
message: `${localize('com_assistants_create_error')}${
|
message: `${localize('com_assistants_create_error')}${
|
||||||
error?.message ? ` ${localize('com_ui_error')}: ${error?.message}` : ''
|
error.message ? ` ${localize('com_ui_error')}: ${error.message}` : ''
|
||||||
}`,
|
}`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
});
|
});
|
||||||
|
@ -139,7 +139,7 @@ export default function AssistantPanel({
|
||||||
return functionName;
|
return functionName;
|
||||||
} else {
|
} else {
|
||||||
const assistant = assistantMap?.[endpoint]?.[assistant_id];
|
const assistant = assistantMap?.[endpoint]?.[assistant_id];
|
||||||
const tool = assistant?.tools?.find((tool) => tool.function?.name === functionName);
|
const tool = assistant?.tools.find((tool) => tool.function?.name === functionName);
|
||||||
if (assistant && tool) {
|
if (assistant && tool) {
|
||||||
return tool;
|
return tool;
|
||||||
}
|
}
|
||||||
|
@ -193,6 +193,16 @@ export default function AssistantPanel({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let submitContext: string | JSX.Element;
|
||||||
|
|
||||||
|
if (create.isLoading || update.isLoading) {
|
||||||
|
submitContext = <Spinner className="icon-md" />;
|
||||||
|
} else if (assistant_id) {
|
||||||
|
submitContext = localize('com_ui_save');
|
||||||
|
} else {
|
||||||
|
submitContext = localize('com_ui_create');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
|
@ -235,7 +245,7 @@ export default function AssistantPanel({
|
||||||
<AssistantAvatar
|
<AssistantAvatar
|
||||||
createMutation={create}
|
createMutation={create}
|
||||||
assistant_id={assistant_id ?? null}
|
assistant_id={assistant_id ?? null}
|
||||||
metadata={assistant?.['metadata'] ?? null}
|
metadata={assistant['metadata'] ?? null}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
version={version}
|
version={version}
|
||||||
/>
|
/>
|
||||||
|
@ -425,13 +435,7 @@ export default function AssistantPanel({
|
||||||
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
className="btn btn-primary focus:shadow-outline flex w-full items-center justify-center px-4 py-2 font-semibold text-white hover:bg-green-600 focus:border-green-500"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{create.isLoading || update.isLoading ? (
|
{submitContext}
|
||||||
<Spinner className="icon-md" />
|
|
||||||
) : assistant_id ? (
|
|
||||||
localize('com_ui_save')
|
|
||||||
) : (
|
|
||||||
localize('com_ui_create')
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default function ContextButton({
|
||||||
createMutation,
|
createMutation,
|
||||||
endpoint,
|
endpoint,
|
||||||
}: {
|
}: {
|
||||||
activeModel: string;
|
activeModel?: string;
|
||||||
assistant_id: string;
|
assistant_id: string;
|
||||||
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAssistantId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
|
createMutation: UseMutationResult<Assistant, Error, AssistantCreateParams>;
|
||||||
|
@ -38,7 +38,7 @@ export default function ContextButton({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (createMutation.data?.id) {
|
if (createMutation.data?.id !== undefined) {
|
||||||
console.log('[deleteAssistant] resetting createMutation');
|
console.log('[deleteAssistant] resetting createMutation');
|
||||||
createMutation.reset();
|
createMutation.reset();
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ export default function ContextButton({
|
||||||
return setOption('assistant_id')(firstAssistant.id);
|
return setOption('assistant_id')(firstAssistant.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentAssistant = updatedList?.find(
|
const currentAssistant = updatedList.find(
|
||||||
(assistant) => assistant.id === conversation?.assistant_id,
|
(assistant) => assistant.id === conversation?.assistant_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -75,6 +75,10 @@ export default function ContextButton({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeModel?.length === 0 || activeModel === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|
|
@ -15,9 +15,9 @@ type OptionType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
value: string;
|
value: string | OptionType;
|
||||||
label?: string;
|
label?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void | ((value: OptionType) => void);
|
||||||
options: (string | OptionType)[];
|
options: (string | OptionType)[];
|
||||||
className?: string;
|
className?: string;
|
||||||
anchor?: AnchorPropsWithSelection;
|
anchor?: AnchorPropsWithSelection;
|
||||||
|
@ -35,14 +35,19 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
testId = 'dropdown-menu',
|
testId = 'dropdown-menu',
|
||||||
}) => {
|
}) => {
|
||||||
|
const getValue = (option: string | OptionType): string =>
|
||||||
|
typeof option === 'string' ? option : option.value;
|
||||||
|
|
||||||
|
const getDisplay = (option: string | OptionType): string =>
|
||||||
|
typeof option === 'string' ? option : (option.display ?? '') || option.value;
|
||||||
|
|
||||||
|
const selectedOption = options.find((option) => getValue(option) === getValue(value));
|
||||||
|
|
||||||
|
const displayValue = selectedOption != null ? getDisplay(selectedOption) : getDisplay(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
<Listbox
|
<Listbox value={value} onChange={onChange}>
|
||||||
value={value}
|
|
||||||
onChange={(newValue) => {
|
|
||||||
onChange(newValue);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={cn('relative', className)}>
|
<div className={cn('relative', className)}>
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
|
@ -55,9 +60,7 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
>
|
>
|
||||||
<span className="block truncate">
|
<span className="block truncate">
|
||||||
{label}
|
{label}
|
||||||
{options
|
{displayValue}
|
||||||
.map((o) => (typeof o === 'string' ? { value: o, display: o } : o))
|
|
||||||
.find((o) => o.value === value)?.display || value}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<svg
|
<svg
|
||||||
|
@ -79,7 +82,7 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
'absolute z-50 mt-1 flex flex-col items-start gap-1 overflow-auto rounded-lg border border-gray-300 bg-white p-1.5 text-gray-700 shadow-lg transition-opacity focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
@ -89,17 +92,15 @@ const Dropdown: FC<DropdownProps> = ({
|
||||||
{options.map((item, index) => (
|
{options.map((item, index) => (
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={index}
|
key={index}
|
||||||
value={typeof item === 'string' ? item : item.value}
|
value={item}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
'relative cursor-pointer select-none rounded border-gray-300 bg-white py-2.5 pl-3 pr-3 text-sm text-gray-700 hover:bg-gray-100 dark:border-gray-300 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600',
|
||||||
)}
|
)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
data-theme={typeof item === 'string' ? item : (item as OptionType).value}
|
data-theme={getValue(item)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span className="block truncate">
|
<span className="block truncate">{getDisplay(item)}</span>
|
||||||
{typeof item === 'string' ? item : (item as OptionType).display}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ListboxOption>
|
</ListboxOption>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default function useAssistantsMap({
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
}: {
|
}: {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
}): TAssistantsMap {
|
}): TAssistantsMap | undefined {
|
||||||
const { data: assistants = {} } = useListAssistantsQuery(EModelEndpoint.assistants, undefined, {
|
const { data: assistants = {} } = useListAssistantsQuery(EModelEndpoint.assistants, undefined, {
|
||||||
select: (res) => mapAssistants(res.data),
|
select: (res) => mapAssistants(res.data),
|
||||||
enabled: isAuthenticated,
|
enabled: isAuthenticated,
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function useMessageActions(props: TMessageActions) {
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
const { conversation: addedConvo, isSubmitting: isSubmittingAdditional } = useAddedChatContext();
|
const { conversation: addedConvo, isSubmitting: isSubmittingAdditional } = useAddedChatContext();
|
||||||
const conversation = useMemo(
|
const conversation = useMemo(
|
||||||
() => (isMultiMessage ? addedConvo : rootConvo),
|
() => (isMultiMessage === true ? addedConvo : rootConvo),
|
||||||
[isMultiMessage, addedConvo, rootConvo],
|
[isMultiMessage, addedConvo, rootConvo],
|
||||||
);
|
);
|
||||||
const assistantMap = useAssistantsMapContext();
|
const assistantMap = useAssistantsMapContext();
|
||||||
|
@ -41,24 +41,28 @@ export default function useMessageActions(props: TMessageActions) {
|
||||||
const edit = useMemo(() => messageId === currentEditId, [messageId, currentEditId]);
|
const edit = useMemo(() => messageId === currentEditId, [messageId, currentEditId]);
|
||||||
|
|
||||||
const enterEdit = useCallback(
|
const enterEdit = useCallback(
|
||||||
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId),
|
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel === true ? -1 : messageId),
|
||||||
[messageId, setCurrentEditId],
|
[messageId, setCurrentEditId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const assistant = useMemo(
|
const assistant = useMemo(() => {
|
||||||
() =>
|
if (!isAssistantsEndpoint(conversation?.endpoint)) {
|
||||||
isAssistantsEndpoint(conversation?.endpoint) &&
|
return undefined;
|
||||||
assistantMap?.[conversation?.endpoint ?? '']?.[message?.model ?? ''],
|
}
|
||||||
[assistantMap, conversation?.endpoint, message?.model],
|
|
||||||
);
|
const endpointKey = conversation?.endpoint ?? '';
|
||||||
|
const modelKey = message?.model ?? '';
|
||||||
|
|
||||||
|
return assistantMap?.[endpointKey] ? assistantMap[endpointKey][modelKey] : undefined;
|
||||||
|
}, [conversation?.endpoint, message?.model, assistantMap]);
|
||||||
|
|
||||||
const isSubmitting = useMemo(
|
const isSubmitting = useMemo(
|
||||||
() => (isMultiMessage ? isSubmittingAdditional : isSubmittingRoot),
|
() => (isMultiMessage === true ? isSubmittingAdditional : isSubmittingRoot),
|
||||||
[isMultiMessage, isSubmittingAdditional, isSubmittingRoot],
|
[isMultiMessage, isSubmittingAdditional, isSubmittingRoot],
|
||||||
);
|
);
|
||||||
|
|
||||||
const regenerateMessage = useCallback(() => {
|
const regenerateMessage = useCallback(() => {
|
||||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
if ((isSubmitting && isCreatedByUser === true) || !message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +72,8 @@ export default function useMessageActions(props: TMessageActions) {
|
||||||
const copyToClipboard = useCopyToClipboard({ text, content });
|
const copyToClipboard = useCopyToClipboard({ text, content });
|
||||||
|
|
||||||
const messageLabel = useMemo(() => {
|
const messageLabel = useMemo(() => {
|
||||||
if (message?.isCreatedByUser) {
|
if (message?.isCreatedByUser === true) {
|
||||||
return UsernameDisplay ? user?.name || user?.username : localize('com_user_message');
|
return UsernameDisplay ? user?.name != null || user?.username : localize('com_user_message');
|
||||||
} else if (assistant) {
|
} else if (assistant) {
|
||||||
return assistant.name ?? 'Assistant';
|
return assistant.name ?? 'Assistant';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
|
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
import { useChatContext, useAssistantsMapContext } from '~/Providers';
|
||||||
|
@ -24,7 +24,7 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
|
|
||||||
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
|
const { text, content, children, messageId = null, isCreatedByUser } = message ?? {};
|
||||||
const edit = messageId === currentEditId;
|
const edit = messageId === currentEditId;
|
||||||
const isLast = !children?.length;
|
const isLast = children?.length === 0 || children?.length === undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const convoId = conversation?.conversationId;
|
const convoId = conversation?.conversationId;
|
||||||
|
@ -44,7 +44,7 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
const logInfo = {
|
const logInfo = {
|
||||||
textKey,
|
textKey,
|
||||||
'latestText.current': latestText.current,
|
'latestText.current': latestText.current,
|
||||||
messageId: message?.messageId,
|
messageId: message.messageId,
|
||||||
convoId,
|
convoId,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
|
@ -60,7 +60,7 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
|
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
|
||||||
|
|
||||||
const enterEdit = useCallback(
|
const enterEdit = useCallback(
|
||||||
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId),
|
(cancel?: boolean) => setCurrentEditId && setCurrentEditId(cancel === true ? -1 : messageId),
|
||||||
[messageId, setCurrentEditId],
|
[messageId, setCurrentEditId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -72,12 +72,19 @@ export default function useMessageHelpers(props: TMessageProps) {
|
||||||
}
|
}
|
||||||
}, [isSubmitting, setAbortScroll]);
|
}, [isSubmitting, setAbortScroll]);
|
||||||
|
|
||||||
const assistant =
|
const assistant = useMemo(() => {
|
||||||
isAssistantsEndpoint(conversation?.endpoint) &&
|
if (!isAssistantsEndpoint(conversation?.endpoint)) {
|
||||||
assistantMap?.[conversation?.endpoint ?? '']?.[message?.model ?? ''];
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpointKey = conversation?.endpoint ?? '';
|
||||||
|
const modelKey = message?.model ?? '';
|
||||||
|
|
||||||
|
return assistantMap?.[endpointKey] ? assistantMap[endpointKey][modelKey] : undefined;
|
||||||
|
}, [conversation?.endpoint, message?.model, assistantMap]);
|
||||||
|
|
||||||
const regenerateMessage = () => {
|
const regenerateMessage = () => {
|
||||||
if ((isSubmitting && isCreatedByUser) || !message) {
|
if ((isSubmitting && isCreatedByUser === true) || !message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2217,31 +2217,31 @@ ol ol):not(:where([class~=not-prose] *)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
.message-content {
|
||||||
font-size: var(--markdown-font-size, 1rem);
|
font-size: var(--markdown-font-size, var(--font-size-base));
|
||||||
line-height: 1.75;
|
line-height: 1.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content pre code {
|
.message-content pre code {
|
||||||
font-size: calc(0.85 * var(--markdown-font-size, 1rem));
|
font-size: calc(0.85 * var(--markdown-font-size, var(--font-size-base)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content pre {
|
.message-content pre {
|
||||||
font-size: var(--markdown-font-size, 1rem);
|
font-size: var(--markdown-font-size, var(--font-size-base));
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-analyze-block pre code,
|
.code-analyze-block pre code,
|
||||||
.code-analyze-block .overflow-y-auto code {
|
.code-analyze-block .overflow-y-auto code {
|
||||||
font-size: calc(0.85 * var(--markdown-font-size, 1rem));
|
font-size: calc(0.85 * var(--markdown-font-size, var(--font-size-base)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-analyze-block pre,
|
.code-analyze-block pre,
|
||||||
.code-analyze-block .overflow-y-auto {
|
.code-analyze-block .overflow-y-auto {
|
||||||
font-size: var(--markdown-font-size, 1rem);
|
font-size: var(--markdown-font-size, var(--font-size-base));
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text-wrapper {
|
.progress-text-wrapper {
|
||||||
font-size: var(--markdown-font-size, 1rem);
|
font-size: var(--markdown-font-size, var(--font-size-base));
|
||||||
line-height: calc(1.25 * var(--markdown-font-size, 1rem));
|
line-height: calc(1.25 * var(--markdown-font-size, var(--font-size-base)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text-content {
|
.progress-text-content {
|
||||||
|
|
|
@ -45,10 +45,8 @@ export const getTextKey = (message?: TMessage | null, convoId?: string | null) =
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scrollToEnd = () => {
|
export const scrollToEnd = () => {
|
||||||
setTimeout(() => {
|
const messagesEndElement = document.getElementById('messages-end');
|
||||||
const messagesEndElement = document.getElementById('messages-end');
|
if (messagesEndElement) {
|
||||||
if (messagesEndElement) {
|
messagesEndElement.scrollIntoView({ behavior: 'instant' });
|
||||||
messagesEndElement.scrollIntoView({ behavior: 'instant' });
|
}
|
||||||
}
|
|
||||||
}, 750);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
/**
|
|
||||||
* Install script: WIP
|
|
||||||
*/
|
|
||||||
const fs = require('fs');
|
|
||||||
const { exit } = require('process');
|
|
||||||
const { askQuestion } = require('./helpers');
|
|
||||||
|
|
||||||
// If we are not in a TTY, lets exit
|
|
||||||
if (!process.stdin.isTTY) {
|
|
||||||
console.log('Note: we are not in a TTY, skipping install script.');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are in CI env, lets exit
|
|
||||||
if (process.env.NODE_ENV === 'CI') {
|
|
||||||
console.log('Note: we are in a CI environment, skipping install script.');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the original console.log function
|
|
||||||
const originalConsoleWarn = console.warn;
|
|
||||||
console.warn = () => {};
|
|
||||||
const loader = require('./loader');
|
|
||||||
console.warn = originalConsoleWarn;
|
|
||||||
|
|
||||||
const rootEnvPath = loader.resolve('.env');
|
|
||||||
|
|
||||||
// Skip if the env file exists
|
|
||||||
if (fs.existsSync(rootEnvPath)) {
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the upgrade script if the legacy api/env file exists
|
|
||||||
// Todo: remove this in a future version
|
|
||||||
if (fs.existsSync(loader.resolve('api/.env'))) {
|
|
||||||
console.warn('Upgrade script has yet to run, lets do that!');
|
|
||||||
require('./upgrade');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the example file exists
|
|
||||||
if (!fs.existsSync(rootEnvPath + '.example')) {
|
|
||||||
console.red('It looks like the example env file is missing, please complete setup manually.');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the example file
|
|
||||||
fs.copyFileSync(rootEnvPath + '.example', rootEnvPath);
|
|
||||||
|
|
||||||
// Update the secure keys!
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_KEY', 32);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_IV', 16);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'MEILI_MASTER_KEY', 32);
|
|
||||||
|
|
||||||
// Init env
|
|
||||||
let env = {};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
// Lets colour the console
|
|
||||||
console.purple('=== LibreChat First Install ===');
|
|
||||||
console.blue('Note: Leave blank to use the default value.');
|
|
||||||
console.log(''); // New line
|
|
||||||
|
|
||||||
// Ask for the app title
|
|
||||||
const title = await askQuestion('Enter the app title (default: "LibreChat"): ');
|
|
||||||
env['APP_TITLE'] = title || 'LibreChat';
|
|
||||||
|
|
||||||
// Ask for OPENAI_API_KEY
|
|
||||||
const key = await askQuestion('Enter your OPENAI_API_KEY (default: "user_provided"): ');
|
|
||||||
env['OPENAI_API_KEY'] = key || 'user_provided';
|
|
||||||
|
|
||||||
// Ask about mongodb
|
|
||||||
const mongodb = await askQuestion(
|
|
||||||
'What is your mongodb url? (default: mongodb://127.0.0.1:27018/LibreChat)',
|
|
||||||
);
|
|
||||||
env['MONGO_URI'] = mongodb || 'mongodb://127.0.0.1:27018/LibreChat';
|
|
||||||
// Very basic check to make sure they entered a url
|
|
||||||
if (!env['MONGO_URI'].includes('://')) {
|
|
||||||
console.orange(
|
|
||||||
'Warning: Your mongodb url looks incorrect, please double check it in the `.env` file.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lets ask about open registration
|
|
||||||
const openReg = await askQuestion('Do you want to allow user registration (y/n)? Default: y');
|
|
||||||
if (openReg === 'n' || openReg === 'no') {
|
|
||||||
env['ALLOW_REGISTRATION'] = 'false';
|
|
||||||
// Lets tell them about how to create an account:
|
|
||||||
console.red(
|
|
||||||
'Note: You can create an account by running: `npm run create-user <email> <name> <username>`',
|
|
||||||
);
|
|
||||||
// sleep for 1 second so they can read this
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the env file
|
|
||||||
loader.writeEnvFile(rootEnvPath, env);
|
|
||||||
|
|
||||||
// We can ask for more here if we want
|
|
||||||
console.log(''); // New line
|
|
||||||
console.green('Success! Please read our docs if you need help setting up the rest of the app.');
|
|
||||||
console.log(''); // New line
|
|
||||||
})();
|
|
252
config/loader.js
252
config/loader.js
|
@ -1,252 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const dotenv = require('dotenv');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is responsible for loading the environment variables
|
|
||||||
*
|
|
||||||
* Inspired by: https://thekenyandev.com/blog/environment-variables-strategy-for-node/
|
|
||||||
*/
|
|
||||||
class Env {
|
|
||||||
constructor() {
|
|
||||||
this.envMap = {
|
|
||||||
default: '.env',
|
|
||||||
development: '.env.development',
|
|
||||||
test: '.env.test',
|
|
||||||
production: '.env.production',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
|
|
||||||
this.isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
this.domains = {
|
|
||||||
client: process.env.DOMAIN_CLIENT,
|
|
||||||
server: process.env.DOMAIN_SERVER,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the environment variables
|
|
||||||
*/
|
|
||||||
init() {
|
|
||||||
let hasDefault = false;
|
|
||||||
// Load the default env file if it exists
|
|
||||||
if (fs.existsSync(this.envMap.default)) {
|
|
||||||
hasDefault = true;
|
|
||||||
dotenv.config({
|
|
||||||
// path: this.resolve(this.envMap.default),
|
|
||||||
path: path.resolve(__dirname, '..', this.envMap.default),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('The default .env file was not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const environment = this.currentEnvironment();
|
|
||||||
|
|
||||||
// Load the environment specific env file
|
|
||||||
const envFile = this.envMap[environment];
|
|
||||||
|
|
||||||
// check if the file exists
|
|
||||||
if (fs.existsSync(envFile)) {
|
|
||||||
dotenv.config({
|
|
||||||
// path: this.resolve(envFile),
|
|
||||||
path: path.resolve(__dirname, '..', envFile),
|
|
||||||
});
|
|
||||||
} else if (!hasDefault) {
|
|
||||||
console.warn('No env files found, have you completed the install process?');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate Config
|
|
||||||
*/
|
|
||||||
validate() {
|
|
||||||
const requiredKeys = [
|
|
||||||
'NODE_ENV',
|
|
||||||
'JWT_SECRET',
|
|
||||||
'DOMAIN_CLIENT',
|
|
||||||
'DOMAIN_SERVER',
|
|
||||||
'CREDS_KEY',
|
|
||||||
'CREDS_IV',
|
|
||||||
];
|
|
||||||
|
|
||||||
const missingKeys = requiredKeys
|
|
||||||
.map((key) => {
|
|
||||||
const variable = process.env[key];
|
|
||||||
if (variable === undefined || variable === null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((value) => value !== undefined);
|
|
||||||
|
|
||||||
// Throw an error if any required keys are missing
|
|
||||||
if (missingKeys.length) {
|
|
||||||
const message = `
|
|
||||||
The following required env variables are missing:
|
|
||||||
${missingKeys.toString()}.
|
|
||||||
Please add them to your env file or run 'npm run install'
|
|
||||||
`;
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check JWT secret for default
|
|
||||||
if (process.env.JWT_SECRET === 'secret') {
|
|
||||||
console.warn('Warning: JWT_SECRET is set to default value');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the location of the env file
|
|
||||||
*
|
|
||||||
* @param {String} envFile
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
resolve(envFile) {
|
|
||||||
return path.resolve(process.cwd(), envFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add secure keys to the env
|
|
||||||
*
|
|
||||||
* @param {String} filePath The path of the .env you are updating
|
|
||||||
* @param {String} key The env you are adding
|
|
||||||
* @param {Number} length The length of the secure key
|
|
||||||
*/
|
|
||||||
addSecureEnvVar(filePath, key, length) {
|
|
||||||
const env = {};
|
|
||||||
env[key] = this.generateSecureRandomString(length);
|
|
||||||
this.writeEnvFile(filePath, env);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the change to the env file
|
|
||||||
*/
|
|
||||||
writeEnvFile(filePath, env) {
|
|
||||||
const content = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const updatedLines = lines
|
|
||||||
.map((line) => {
|
|
||||||
if (line.trim().startsWith('#')) {
|
|
||||||
// Allow comment removal
|
|
||||||
if (env[line] === 'remove') {
|
|
||||||
return null; // Mark the line for removal
|
|
||||||
}
|
|
||||||
// Preserve comments
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [key, value] = line.split('=');
|
|
||||||
if (key && value && Object.prototype.hasOwnProperty.call(env, key.trim())) {
|
|
||||||
if (env[key.trim()] === 'remove') {
|
|
||||||
return null; // Mark the line for removal
|
|
||||||
}
|
|
||||||
return `${key.trim()}=${env[key.trim()]}`;
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
})
|
|
||||||
.filter((line) => line !== null); // Remove lines marked for removal
|
|
||||||
|
|
||||||
// Add any new environment variables that are not in the file yet
|
|
||||||
Object.entries(env).forEach(([key, value]) => {
|
|
||||||
if (value !== 'remove' && !updatedLines.some((line) => line.startsWith(`${key}=`))) {
|
|
||||||
updatedLines.push(`${key}=${value}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Loop through updatedLines and wrap values with spaces in double quotes
|
|
||||||
const fixedLines = updatedLines.map((line) => {
|
|
||||||
// lets only split the first = sign
|
|
||||||
const [key, value] = line.split(/=(.+)/);
|
|
||||||
if (typeof value === 'undefined' || line.trim().startsWith('#')) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
// Skip lines with quotes and numbers already
|
|
||||||
// Todo: this could be one regex
|
|
||||||
const wrappedValue =
|
|
||||||
value.includes(' ') && !value.includes('"') && !value.includes('\'') && !/\d/.test(value)
|
|
||||||
? `"${value}"`
|
|
||||||
: value;
|
|
||||||
return `${key}=${wrappedValue}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedContent = fixedLines.join('\n');
|
|
||||||
fs.writeFileSync(filePath, updatedContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Secure Random Strings
|
|
||||||
*
|
|
||||||
* @param {Number} length The length of the random string
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
generateSecureRandomString(length = 32) {
|
|
||||||
return crypto.randomBytes(length).toString('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the environment variables
|
|
||||||
*/
|
|
||||||
all() {
|
|
||||||
return process.env;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an environment variable
|
|
||||||
*
|
|
||||||
* @param {String} variable
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
get(variable) {
|
|
||||||
return process.env[variable];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current environment name
|
|
||||||
*
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
currentEnvironment() {
|
|
||||||
return this.get('NODE_ENV');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running in development?
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
isDevelopment() {
|
|
||||||
return this.currentEnvironment() === 'development';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running tests?
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
isTest() {
|
|
||||||
return this.currentEnvironment() === 'test';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running in production?
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
isProduction() {
|
|
||||||
return this.currentEnvironment() === 'production';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Are we running in CI?
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
isCI() {
|
|
||||||
return this.currentEnvironment() === 'CI';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const env = new Env();
|
|
||||||
|
|
||||||
module.exports = env;
|
|
|
@ -1,163 +0,0 @@
|
||||||
/**
|
|
||||||
* Upgrade script
|
|
||||||
*/
|
|
||||||
const fs = require('fs');
|
|
||||||
const dotenv = require('dotenv');
|
|
||||||
const { exit } = require('process');
|
|
||||||
|
|
||||||
// Suppress default warnings
|
|
||||||
const originalConsoleWarn = console.warn;
|
|
||||||
console.warn = () => {};
|
|
||||||
const loader = require('./loader');
|
|
||||||
console.warn = originalConsoleWarn;
|
|
||||||
|
|
||||||
// Old Paths
|
|
||||||
const apiEnvPath = loader.resolve('api/.env');
|
|
||||||
const clientEnvPath = loader.resolve('client/.env');
|
|
||||||
|
|
||||||
// Load into env
|
|
||||||
dotenv.config({
|
|
||||||
path: loader.resolve(apiEnvPath),
|
|
||||||
});
|
|
||||||
dotenv.config({
|
|
||||||
path: loader.resolve(clientEnvPath),
|
|
||||||
});
|
|
||||||
// JS was doing spooky actions at a distance, lets prevent that
|
|
||||||
const initEnv = JSON.parse(JSON.stringify(process.env));
|
|
||||||
|
|
||||||
// New Paths
|
|
||||||
const rootEnvPath = loader.resolve('.env');
|
|
||||||
const devEnvPath = loader.resolve('.env.development');
|
|
||||||
const prodEnvPath = loader.resolve('.env.production');
|
|
||||||
|
|
||||||
if (fs.existsSync(rootEnvPath)) {
|
|
||||||
console.error('Root env file already exists! Aborting');
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate old configs
|
|
||||||
if (!fs.existsSync(apiEnvPath)) {
|
|
||||||
console.error('Api env doesn\'t exit! Did you mean to run install?');
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(clientEnvPath)) {
|
|
||||||
console.error('Client env doesn\'t exit! But api/.env does. Manual upgrade required');
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refactor the ENV if it has a prod_/dev_ version
|
|
||||||
*
|
|
||||||
* @param {*} varDev
|
|
||||||
* @param {*} varProd
|
|
||||||
* @param {*} varName
|
|
||||||
*/
|
|
||||||
function refactorPairedEnvVar(varDev, varProd, varName) {
|
|
||||||
// Lets validate if either of these are undefined, if so lets use the non-undefined one
|
|
||||||
if (initEnv[varDev] === undefined && initEnv[varProd] === undefined) {
|
|
||||||
console.error(`Both ${varDev} and ${varProd} are undefined! Manual intervention required!`);
|
|
||||||
} else if (initEnv[varDev] === undefined) {
|
|
||||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varProd]}`);
|
|
||||||
} else if (initEnv[varProd] === undefined) {
|
|
||||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varDev]}`);
|
|
||||||
} else if (initEnv[varDev] === initEnv[varProd]) {
|
|
||||||
fs.appendFileSync(rootEnvPath, `\n${varName}=${initEnv[varDev]}`);
|
|
||||||
} else {
|
|
||||||
fs.appendFileSync(rootEnvPath, `${varName}=${initEnv[varProd]}\n`);
|
|
||||||
fs.appendFileSync(devEnvPath, `${varName}=${initEnv[varDev]}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upgrade the env files!
|
|
||||||
* 1. /api/.env will merge into /.env
|
|
||||||
* 2. /client/.env will merge into /.env
|
|
||||||
* 3. Any prod_/dev_ keys will be split up into .env.development / .env.production files (if they are different)
|
|
||||||
*/
|
|
||||||
if (fs.existsSync(apiEnvPath)) {
|
|
||||||
fs.copyFileSync(apiEnvPath, rootEnvPath);
|
|
||||||
fs.copyFileSync(apiEnvPath, rootEnvPath + '.api.bak');
|
|
||||||
fs.unlinkSync(apiEnvPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up Domain variables
|
|
||||||
fs.appendFileSync(
|
|
||||||
rootEnvPath,
|
|
||||||
'\n\n##########################\n# Domain Variables:\n# Note: DOMAIN_ vars are passed to vite\n##########################\n',
|
|
||||||
);
|
|
||||||
refactorPairedEnvVar('CLIENT_URL_DEV', 'CLIENT_URL_PROD', 'DOMAIN_CLIENT');
|
|
||||||
refactorPairedEnvVar('SERVER_URL_DEV', 'SERVER_URL_PROD', 'DOMAIN_SERVER');
|
|
||||||
|
|
||||||
// Remove the old vars
|
|
||||||
const removeEnvs = {
|
|
||||||
NODE_ENV: 'remove',
|
|
||||||
OPENAI_KEY: 'remove',
|
|
||||||
CLIENT_URL_DEV: 'remove',
|
|
||||||
CLIENT_URL_PROD: 'remove',
|
|
||||||
SERVER_URL_DEV: 'remove',
|
|
||||||
SERVER_URL_PROD: 'remove',
|
|
||||||
JWT_SECRET_DEV: 'remove', // Lets regen
|
|
||||||
JWT_SECRET_PROD: 'remove', // Lets regen
|
|
||||||
VITE_APP_TITLE: 'remove',
|
|
||||||
// Comments to remove:
|
|
||||||
'#JWT:': 'remove',
|
|
||||||
'# Add a secure secret for production if deploying to live domain.': 'remove',
|
|
||||||
'# Site URLs:': 'remove',
|
|
||||||
'# Don\'t forget to set Node env to development in the Server configuration section above':
|
|
||||||
'remove',
|
|
||||||
'# if you want to run in dev mode': 'remove',
|
|
||||||
'# Change these values to domain if deploying:': 'remove',
|
|
||||||
'# Set Node env to development if running in dev mode.': 'remove',
|
|
||||||
};
|
|
||||||
loader.writeEnvFile(rootEnvPath, removeEnvs);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lets make things more secure!
|
|
||||||
* 1. Add CREDS_KEY
|
|
||||||
* 2. Add CREDS_IV
|
|
||||||
* 3. Add JWT_SECRET
|
|
||||||
*/
|
|
||||||
fs.appendFileSync(
|
|
||||||
rootEnvPath,
|
|
||||||
'\n\n##########################\n# Secure Keys:\n##########################\n',
|
|
||||||
);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_KEY', 32);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'CREDS_IV', 16);
|
|
||||||
loader.addSecureEnvVar(rootEnvPath, 'JWT_SECRET', 32);
|
|
||||||
|
|
||||||
// Lets update the openai key name, not the best spot in the env file but who cares ¯\_(ツ)_/¯
|
|
||||||
loader.writeEnvFile(rootEnvPath, { OPENAI_API_KEY: initEnv['OPENAI_KEY'] });
|
|
||||||
|
|
||||||
// TODO: we need to copy over the value of: APP_TITLE
|
|
||||||
fs.appendFileSync(
|
|
||||||
rootEnvPath,
|
|
||||||
'\n\n##########################\n# Frontend Vite Variables:\n##########################\n',
|
|
||||||
);
|
|
||||||
const frontend = {
|
|
||||||
APP_TITLE: initEnv['VITE_APP_TITLE'] || '"LibreChat"',
|
|
||||||
ALLOW_REGISTRATION: 'true',
|
|
||||||
};
|
|
||||||
loader.writeEnvFile(rootEnvPath, frontend);
|
|
||||||
|
|
||||||
// Ensure .env.development and .env.production files end with a newline
|
|
||||||
if (fs.existsSync(devEnvPath)) {
|
|
||||||
fs.appendFileSync(devEnvPath, '\n');
|
|
||||||
}
|
|
||||||
if (fs.existsSync(prodEnvPath)) {
|
|
||||||
fs.appendFileSync(prodEnvPath, '\n');
|
|
||||||
}
|
|
||||||
// Remove client file
|
|
||||||
fs.copyFileSync(clientEnvPath, rootEnvPath + '.client.bak');
|
|
||||||
fs.unlinkSync(clientEnvPath);
|
|
||||||
|
|
||||||
console.log('###############################################');
|
|
||||||
console.log('Upgrade completed! Please review the new .env file and make any changes as needed.');
|
|
||||||
console.log('###############################################');
|
|
||||||
|
|
||||||
// if the .env.development file exists, lets tell the user
|
|
||||||
if (fs.existsSync(devEnvPath)) {
|
|
||||||
console.log(
|
|
||||||
'NOTE: A .env.development file was created. This will take precedence over the .env file when running in dev mode.',
|
|
||||||
);
|
|
||||||
console.log('###############################################');
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue