feat: init @librechat/client

This commit is contained in:
Marco Beretta 2025-07-05 22:49:28 +02:00
parent 545a909953
commit dcaa5af598
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
207 changed files with 21329 additions and 239 deletions

View file

@ -1,37 +0,0 @@
import { useMemo, useState } from 'react';
import { matchSorter } from 'match-sorter';
import type { OptionWithIcon, MentionOption } from '~/common';
export default function useCombobox({
value,
options,
}: {
value: string;
options: Array<OptionWithIcon | MentionOption>;
}) {
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState('');
const matches = useMemo(() => {
if (!searchValue) {
return options;
}
const keys = ['label', 'value'];
const matches = matchSorter(options, searchValue, { keys });
// Radix Select does not work if we don't render the selected item, so we
// make sure to include it in the list of matches.
const selectedItem = options.find((currentItem) => currentItem.value === value);
if (selectedItem && !matches.includes(selectedItem)) {
matches.push(selectedItem);
}
return matches;
}, [searchValue, value, options]);
return {
open,
setOpen,
searchValue,
setSearchValue,
matches,
};
}

View file

@ -1,82 +0,0 @@
//ThemeContext.js
// source: https://plainenglish.io/blog/light-and-dark-mode-in-react-web-application-with-tailwind-css-89674496b942
import { useSetRecoilState } from 'recoil';
import React, { createContext, useState, useEffect } from 'react';
import { getInitialTheme, applyFontSize } from '~/utils';
import store from '~/store';
type ProviderValue = {
theme: string;
setTheme: React.Dispatch<React.SetStateAction<string>>;
};
const defaultContextValue: ProviderValue = {
theme: getInitialTheme(),
setTheme: () => {
return;
},
};
export const isDark = (theme: string): boolean => {
if (theme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return theme === 'dark';
};
export const ThemeContext = createContext<ProviderValue>(defaultContextValue);
export const ThemeProvider = ({ initialTheme, children }) => {
const [theme, setTheme] = useState(getInitialTheme);
const setFontSize = useSetRecoilState(store.fontSize);
const rawSetTheme = (rawTheme: string) => {
const root = window.document.documentElement;
const darkMode = isDark(rawTheme);
root.classList.remove(darkMode ? 'light' : 'dark');
root.classList.add(darkMode ? 'dark' : 'light');
localStorage.setItem('color-theme', rawTheme);
};
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const changeThemeOnSystemChange = () => {
rawSetTheme(mediaQuery.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', changeThemeOnSystemChange);
return () => {
mediaQuery.removeEventListener('change', changeThemeOnSystemChange);
};
}, []);
useEffect(() => {
const fontSize = localStorage.getItem('fontSize');
if (fontSize == null) {
setFontSize('text-base');
applyFontSize('text-base');
localStorage.setItem('fontSize', JSON.stringify('text-base'));
return;
}
try {
applyFontSize(JSON.parse(fontSize));
} catch (error) {
console.log(error);
}
// Reason: This effect should only run once, and `setFontSize` is a stable function
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (initialTheme) {
rawSetTheme(initialTheme);
}
useEffect(() => {
rawSetTheme(theme);
}, [theme]);
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
};

View file

@ -14,24 +14,19 @@ export * from './Prompts';
export * from './Roles';
export * from './SSE';
export * from './AuthContext';
export * from './ThemeContext';
export * from './ScreenshotContext';
export * from './ApiErrorBoundaryContext';
export * from './Endpoint';
export type { TranslationKeys } from './useLocalize';
export { default as useToast } from './useToast';
export { default as useTimeout } from './useTimeout';
export { default as useNewConvo } from './useNewConvo';
export { default as useLocalize } from './useLocalize';
export { default as useMediaQuery } from './useMediaQuery';
export { default as useChatBadges } from './useChatBadges';
export { default as useScrollToRef } from './useScrollToRef';
export { default as useLocalStorage } from './useLocalStorage';
export { default as useDocumentTitle } from './useDocumentTitle';
export { default as useDelayedRender } from './useDelayedRender';
export { default as useOnClickOutside } from './useOnClickOutside';
export { default as useSpeechToText } from './Input/useSpeechToText';
export { default as useTextToSpeech } from './Input/useTextToSpeech';
export { default as useGenerationsByLatest } from './useGenerationsByLatest';

View file

@ -1,37 +0,0 @@
import { useState, useRef, useEffect } from 'react';
import type { ReactNode } from 'react';
const useDelayedRender = (delay: number) => {
const [delayed, setDelayed] = useState(true);
const timerPromiseRef = useRef<Promise<void> | null>(null);
useEffect(() => {
if (delayed) {
const timerPromise = new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
setDelayed(false);
resolve();
}, delay);
return () => {
clearTimeout(timeout);
};
});
timerPromiseRef.current = timerPromise;
}
return () => {
timerPromiseRef.current = null;
};
}, [delay, delayed]);
return (fn: () => ReactNode) => {
if (delayed && timerPromiseRef.current) {
throw timerPromiseRef.current;
}
return fn();
};
};
export default useDelayedRender;

View file

@ -1,17 +0,0 @@
import { useEffect, useState } from 'react';
export default function useMediaQuery(query: string) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [matches, query]);
return matches;
}

View file

@ -1,41 +0,0 @@
import { useEffect, RefObject } from 'react';
type Handler = () => void;
export default function useOnClickOutside(
ref: RefObject<HTMLElement>,
handler: Handler,
excludeIds: string[],
customCondition?: (target: EventTarget | Element | null) => boolean,
): void {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node | null;
if (target && 'id' in target && excludeIds.includes((target as HTMLElement).id)) {
return;
}
if (
target?.parentNode &&
'id' in target.parentNode &&
excludeIds.includes((target.parentNode as HTMLElement).id)
) {
return;
}
if (customCondition && customCondition(target)) {
return;
}
if (ref.current && !ref.current.contains(target)) {
handler();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, handler]);
}

View file

@ -1,58 +0,0 @@
import { useRecoilState } from 'recoil';
import { useRef, useEffect } from 'react';
import type { TShowToast } from '~/common';
import { NotificationSeverity } from '~/common';
import store from '~/store';
export default function useToast(showDelay = 100) {
const [toast, setToast] = useRecoilState(store.toastState);
const showTimerRef = useRef<number | null>(null);
const hideTimerRef = useRef<number | null>(null);
useEffect(() => {
return () => {
if (showTimerRef.current !== null) {
clearTimeout(showTimerRef.current);
}
if (hideTimerRef.current !== null) {
clearTimeout(hideTimerRef.current);
}
};
}, []);
const showToast = ({
message,
severity = NotificationSeverity.SUCCESS,
showIcon = true,
duration = 3000, // default duration for the toast to be visible
status,
}: TShowToast) => {
// Clear existing timeouts
if (showTimerRef.current !== null) {
clearTimeout(showTimerRef.current);
}
if (hideTimerRef.current !== null) {
clearTimeout(hideTimerRef.current);
}
// Timeout to show the toast
showTimerRef.current = window.setTimeout(() => {
setToast({
open: true,
message,
severity: (status as NotificationSeverity) ?? severity,
showIcon,
});
// Hides the toast after the specified duration
hideTimerRef.current = window.setTimeout(() => {
setToast((prevToast) => ({ ...prevToast, open: false }));
}, duration);
}, showDelay);
};
return {
toast,
onOpenChange: (open: boolean) => setToast({ ...toast, open }),
showToast,
};
}