mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-06 10:38:50 +01:00
feat: init @librechat/client
This commit is contained in:
parent
545a909953
commit
dcaa5af598
207 changed files with 21329 additions and 239 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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>;
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue