diff --git a/frontend/package.json b/frontend/package.json index cc40c377..f8f1c7c2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", + "react-query": "^3.39.3", + "react-router-dom": "^6.22.3", "tailwind-merge": "^2.0.0", "uuid": "^9.0.1" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index df449b19..33ce1eb4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,58 +1,37 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { InformationCircleIcon } from "@heroicons/react/24/outline"; import { Chat } from "./components/Chat"; import { ChatList } from "./components/ChatList"; import { Layout } from "./components/Layout"; import { NewChat } from "./components/NewChat"; -import { Chat as ChatType, useChatList } from "./hooks/useChatList"; +import { useChatList } from "./hooks/useChatList"; import { useSchemas } from "./hooks/useSchemas"; import { useStreamState } from "./hooks/useStreamState"; -import { useConfigList } from "./hooks/useConfigList"; +import { + useConfigList, + Config as ConfigInterface, +} from "./hooks/useConfigList"; import { Config } from "./components/Config"; import { MessageWithFiles } from "./utils/formTypes.ts"; -import { TYPE_NAME } from "./constants.ts"; +import { useNavigate } from "react-router-dom"; +import { useThreadAndAssistant } from "./hooks/useThreadAndAssistant.ts"; function App() { + const navigate = useNavigate(); const [sidebarOpen, setSidebarOpen] = useState(false); - const { configSchema, configDefaults } = useSchemas(); - const { chats, currentChat, createChat, enterChat } = useChatList(); - const { configs, currentConfig, saveConfig, enterConfig } = useConfigList(); + const { chats, createChat } = useChatList(); + const { configs, saveConfig } = useConfigList(); const { startStream, stopStream, stream } = useStreamState(); - const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] = - useState(false); + const { configSchema, configDefaults } = useSchemas(); - useEffect(() => { - let configurable = null; - if (currentConfig) { - configurable = currentConfig?.config?.configurable; - } - if (currentChat && configs) { - const conf = configs.find( - (c) => c.assistant_id === currentChat.assistant_id, - ); - configurable = conf?.config?.configurable; - } - const agent_type = configurable?.["type"] as TYPE_NAME | null; - if (agent_type === null || agent_type === "chatbot") { - setIsDocumentRetrievalActive(false); - return; - } - if (agent_type === "chat_retrieval") { - setIsDocumentRetrievalActive(true); - return; - } - const tools = - (configurable?.["type==agent/tools"] as { name: string }[]) ?? []; - setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval")); - }, [currentConfig, currentChat, configs]); + const { currentChat, assistantConfig, isLoading } = useThreadAndAssistant(); const startTurn = useCallback( - async (message?: MessageWithFiles, chat: ChatType | null = currentChat) => { - if (!chat) return; - const config = configs?.find( - (c) => c.assistant_id === chat.assistant_id, - )?.config; - if (!config) return; + async ( + message: MessageWithFiles | null, + thread_id: string, + assistant_id: string, + ) => { const files = message?.files || []; if (files.length > 0) { const formData = files.reduce((formData, file) => { @@ -61,7 +40,7 @@ function App() { }, new FormData()); formData.append( "config", - JSON.stringify({ configurable: { thread_id: chat.thread_id } }), + JSON.stringify({ configurable: { thread_id } }), ); await fetch(`/ingest`, { method: "POST", @@ -79,23 +58,20 @@ function App() { }, ] : null, - chat.assistant_id, - chat.thread_id, + assistant_id, + thread_id, ); }, - [currentChat, startStream, configs], + [startStream], ); const startChat = useCallback( - async (message: MessageWithFiles) => { - if (!currentConfig) return; - const chat = await createChat( - message.message, - currentConfig.assistant_id, - ); - return startTurn(message, chat); + async (config: ConfigInterface, message: MessageWithFiles) => { + const chat = await createChat(message.message, config.assistant_id); + navigate(`/thread/${chat.thread_id}`); + return startTurn(message, chat.thread_id, chat.assistant_id); }, - [createChat, startTurn, currentConfig], + [createChat, navigate, startTurn], ); const selectChat = useCallback( @@ -103,69 +79,37 @@ function App() { if (currentChat) { stopStream?.(true); } - enterChat(id); if (!id) { - enterConfig(configs?.[0]?.assistant_id ?? null); + const firstAssistant = configs?.[0]?.assistant_id ?? null; + navigate(firstAssistant ? `/assistant/${firstAssistant}` : "/"); window.scrollTo({ top: 0 }); + } else { + navigate(`/thread/${id}`); } if (sidebarOpen) { setSidebarOpen(false); } }, - [enterChat, stopStream, sidebarOpen, currentChat, enterConfig, configs], + [currentChat, sidebarOpen, stopStream, configs, navigate], ); const selectConfig = useCallback( (id: string | null) => { - enterConfig(id); - enterChat(null); + navigate(id ? `/assistant/${id}` : "/"); }, - [enterConfig, enterChat], - ); - - const content = currentChat ? ( - - ) : currentConfig ? ( - - ) : ( - - ); - - const currentChatConfig = configs?.find( - (c) => c.assistant_id === currentChat?.assistant_id, + [navigate], ); return ( - {currentChatConfig.name} + {assistantConfig.name} { - selectConfig(currentChatConfig.assistant_id); + selectConfig(assistantConfig.assistant_id); }} /> @@ -175,20 +119,36 @@ function App() { setSidebarOpen={setSidebarOpen} sidebar={ { - if (configs === null || chats === null) return null; - return chats.filter((c) => - configs.some((config) => config.assistant_id === c.assistant_id), - ); - }, [chats, configs])} - currentChat={currentChat} + chats={chats} enterChat={selectChat} - currentConfig={currentConfig} enterConfig={selectConfig} /> } > - {configSchema ? content : null} + {currentChat && assistantConfig && ( + + )} + {!currentChat && assistantConfig && ( + + )} + {!currentChat && !assistantConfig && !isLoading && ( + + )} + {isLoading &&
Loading...
}
); } diff --git a/frontend/src/api/assistants.ts b/frontend/src/api/assistants.ts new file mode 100644 index 00000000..74402629 --- /dev/null +++ b/frontend/src/api/assistants.ts @@ -0,0 +1,16 @@ +import { Config } from "../hooks/useConfigList"; + +export async function getAssistant( + assistantId: string, +): Promise { + try { + const response = await fetch(`/assistants/${assistantId}`); + if (!response.ok) { + return null; + } + return (await response.json()) as Config; + } catch (error) { + console.error("Failed to fetch assistant:", error); + return null; + } +} diff --git a/frontend/src/api/threads.ts b/frontend/src/api/threads.ts new file mode 100644 index 00000000..4e4e29c3 --- /dev/null +++ b/frontend/src/api/threads.ts @@ -0,0 +1,14 @@ +import { Chat } from "../hooks/useChatList.ts"; + +export async function getThread(threadId: string): Promise { + try { + const response = await fetch(`/threads/${threadId}`); + if (!response.ok) { + return null; + } + return (await response.json()) as Chat; + } catch (error) { + console.error("Failed to fetch assistant:", error); + return null; + } +} diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx index 1ac94f68..898d23c9 100644 --- a/frontend/src/components/Chat.tsx +++ b/frontend/src/components/Chat.tsx @@ -1,16 +1,19 @@ import { useEffect, useRef } from "react"; -import { Chat as ChatType } from "../hooks/useChatList"; import { StreamStateProps } from "../hooks/useStreamState"; import { useChatMessages } from "../hooks/useChatMessages"; import TypingBox from "./TypingBox"; import { Message } from "./Message"; import { ArrowDownCircleIcon } from "@heroicons/react/24/outline"; import { MessageWithFiles } from "../utils/formTypes.ts"; +import { useParams } from "react-router-dom"; +import { useThreadAndAssistant } from "../hooks/useThreadAndAssistant.ts"; interface ChatProps extends Pick { - chat: ChatType; - startStream: (message?: MessageWithFiles) => Promise; - isDocumentRetrievalActive: boolean; + startStream: ( + message: MessageWithFiles | null, + thread_id: string, + assistant_id: string, + ) => Promise; } function usePrevious(value: T): T | undefined { @@ -22,11 +25,15 @@ function usePrevious(value: T): T | undefined { } export function Chat(props: ChatProps) { + const { chatId } = useParams(); const { messages, resumeable } = useChatMessages( - props.chat.thread_id, + chatId ?? null, props.stream, props.stopStream, ); + + const { currentChat, assistantConfig, isLoading } = useThreadAndAssistant(); + const prevMessages = usePrevious(messages); useEffect(() => { scrollTo({ @@ -38,12 +45,16 @@ export function Chat(props: ChatProps) { }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [messages]); + + if (isLoading) return
Loading...
; + if (!currentChat || !assistantConfig) return
No data.
; + return (
{messages?.map((msg, i) => ( props.startStream()} + onClick={() => + props.startStream( + null, + currentChat.thread_id, + currentChat.assistant_id, + ) + } > Click to continue. @@ -72,12 +89,19 @@ export function Chat(props: ChatProps) { )}
+ props.startStream( + msg, + currentChat.thread_id, + currentChat.assistant_id, + ) + } onInterrupt={ props.stream?.status === "inflight" ? props.stopStream : undefined } inflight={props.stream?.status === "inflight"} - isDocumentRetrievalActive={props.isDocumentRetrievalActive} + currentConfig={assistantConfig} + currentChat={currentChat} />
diff --git a/frontend/src/components/ChatList.tsx b/frontend/src/components/ChatList.tsx index a8764296..16118c23 100644 --- a/frontend/src/components/ChatList.tsx +++ b/frontend/src/components/ChatList.tsx @@ -1,22 +1,22 @@ import { PlusIcon } from "@heroicons/react/24/outline"; import { ChatListProps } from "../hooks/useChatList"; -import { ConfigListProps } from "../hooks/useConfigList"; import { cn } from "../utils/cn"; +import { useThreadAndAssistant } from "../hooks/useThreadAndAssistant.ts"; export function ChatList(props: { chats: ChatListProps["chats"]; - currentChat: ChatListProps["currentChat"]; - enterChat: ChatListProps["enterChat"]; - currentConfig: ConfigListProps["currentConfig"]; - enterConfig: ConfigListProps["enterConfig"]; + enterChat: (id: string | null) => void; + enterConfig: (id: string | null) => void; }) { + const { currentChat, assistantConfig } = useThreadAndAssistant(); + return ( <>
props.enterChat(null)} className={cn( - props.currentChat === null && props.currentConfig !== null + !currentChat && assistantConfig ? "bg-gray-50 text-indigo-600" : "text-gray-700 hover:text-indigo-600 hover:bg-gray-50", "group flex gap-x-3 rounded-md -mx-2 p-2 leading-6 font-semibold cursor-pointer", @@ -24,7 +24,7 @@ export function ChatList(props: { > props.enterConfig(null)} className={cn( - props.currentConfig === null + !assistantConfig ? "bg-gray-50 text-indigo-600" : "text-gray-700 hover:text-indigo-600 hover:bg-gray-50", "mt-1 group flex gap-x-3 rounded-md -mx-2 p-2 leading-6 font-semibold cursor-pointer", @@ -46,7 +46,7 @@ export function ChatList(props: { > props.enterChat(chat.thread_id)} className={cn( - chat === props.currentChat + chat.thread_id === currentChat?.thread_id ? "bg-gray-50 text-indigo-600" : "text-gray-700 hover:text-indigo-600 hover:bg-gray-50", "group flex gap-x-3 rounded-md p-2 leading-6 cursor-pointer", @@ -74,7 +74,7 @@ export function ChatList(props: { > void; }) { const [values, setValues] = useState( props.config?.config ?? props.configDefaults, @@ -579,7 +583,8 @@ export function Config(props: { vals.configurable["type==agent/tools"] = [...selectedTools]; setSelectedTools([]); } - await props.saveConfig(key, vals!, files, isPublic); + const assistantId = await props.saveConfig(key, vals!, files, isPublic); + props.enterConfig(assistantId); setInflight(false); }} > diff --git a/frontend/src/components/ConfigList.tsx b/frontend/src/components/ConfigList.tsx index a2646590..e97d00a9 100644 --- a/frontend/src/components/ConfigList.tsx +++ b/frontend/src/components/ConfigList.tsx @@ -4,15 +4,15 @@ import { cn } from "../utils/cn"; function ConfigItem(props: { config: Config; - currentConfig: ConfigListProps["currentConfig"]; - enterConfig: ConfigListProps["enterConfig"]; + currentConfig: Config | null; + enterConfig: (id: string | null) => void; }) { return (
  • props.enterConfig(props.config.assistant_id)} className={cn( - props.config === props.currentConfig + props.config.assistant_id === props.currentConfig?.assistant_id ? "bg-gray-50 text-indigo-600" : "text-gray-700 hover:text-indigo-600 hover:bg-gray-50", "group flex gap-x-3 rounded-md p-2 leading-6 cursor-pointer", @@ -20,7 +20,7 @@ function ConfigItem(props: { > void; }) { return ( <> diff --git a/frontend/src/components/NewChat.tsx b/frontend/src/components/NewChat.tsx index ba967ad3..4d22e19a 100644 --- a/frontend/src/components/NewChat.tsx +++ b/frontend/src/components/NewChat.tsx @@ -2,50 +2,73 @@ import { ConfigList } from "./ConfigList"; import { Schemas } from "../hooks/useSchemas"; import TypingBox from "./TypingBox"; import { Config } from "./Config"; -import { ConfigListProps } from "../hooks/useConfigList"; +import { + ConfigListProps, + Config as ConfigInterface, +} from "../hooks/useConfigList"; import { cn } from "../utils/cn"; import { MessageWithFiles } from "../utils/formTypes.ts"; +import { useNavigate, useParams } from "react-router-dom"; +import { useThreadAndAssistant } from "../hooks/useThreadAndAssistant.ts"; interface NewChatProps extends ConfigListProps { configSchema: Schemas["configSchema"]; configDefaults: Schemas["configDefaults"]; - startChat: (message: MessageWithFiles) => Promise; - isDocumentRetrievalActive: boolean; + enterConfig: (id: string | null) => void; + startChat: ( + config: ConfigInterface, + message: MessageWithFiles, + ) => Promise; } export function NewChat(props: NewChatProps) { + const navigator = useNavigate(); + const { assistantId } = useParams(); + + const { assistantConfig, isLoading } = useThreadAndAssistant(); + + if (isLoading) return
    Loading...
    ; + if (!assistantConfig) + return
    Could not find assistant with given id.
    ; + return (
    navigator(`/assistant/${id}`)} />
    { + if (assistantConfig) { + await props.startChat(assistantConfig, msg); + } + }} + currentConfig={assistantConfig} + currentChat={null} />
    diff --git a/frontend/src/components/NotFound.tsx b/frontend/src/components/NotFound.tsx new file mode 100644 index 00000000..6a4fe3b4 --- /dev/null +++ b/frontend/src/components/NotFound.tsx @@ -0,0 +1,3 @@ +export function NotFound() { + return
    Page not found.
    ; +} diff --git a/frontend/src/components/TypingBox.tsx b/frontend/src/components/TypingBox.tsx index 6097c87e..c369c9d6 100644 --- a/frontend/src/components/TypingBox.tsx +++ b/frontend/src/components/TypingBox.tsx @@ -7,10 +7,12 @@ import { DocumentIcon, } from "@heroicons/react/20/solid"; import { cn } from "../utils/cn"; -import { Fragment, useCallback, useState } from "react"; +import { Fragment, useCallback, useEffect, useState } from "react"; import { useDropzone } from "react-dropzone"; import { MessageWithFiles } from "../utils/formTypes.ts"; -import { DROPZONE_CONFIG } from "../constants.ts"; +import { DROPZONE_CONFIG, TYPE_NAME } from "../constants.ts"; +import { Config } from "../hooks/useConfigList.ts"; +import { Chat } from "../hooks/useChatList.ts"; function getFileTypeIcon(fileType: string) { switch (fileType) { @@ -43,14 +45,38 @@ function convertBytesToReadableSize(bytes: number) { } export default function TypingBox(props: { - onSubmit: (data: MessageWithFiles) => Promise; + onSubmit: (data: MessageWithFiles) => void; onInterrupt?: () => void; inflight?: boolean; - isDocumentRetrievalActive: boolean; + currentConfig: Config; + currentChat: Chat | null; }) { const [inflight, setInflight] = useState(false); const isInflight = props.inflight || inflight; const [files, setFiles] = useState([]); + const [isDocumentRetrievalActive, setIsDocumentRetrievalActive] = + useState(false); + + const { currentConfig, currentChat } = props; + + useEffect(() => { + let configurable = null; + if (currentConfig) { + configurable = currentConfig.config?.configurable; + } + const agent_type = configurable?.["type"] as TYPE_NAME | null; + if (agent_type === null || agent_type === "chatbot") { + setIsDocumentRetrievalActive(false); + return; + } + if (agent_type === "chat_retrieval") { + setIsDocumentRetrievalActive(true); + return; + } + const tools = + (configurable?.["type==agent/tools"] as { name: string }[]) ?? []; + setIsDocumentRetrievalActive(tools.some((t) => t.name === "Retrieval")); + }, [currentConfig, currentChat]); const onDrop = useCallback((acceptedFiles: File[]) => { setFiles((prevFiles) => { @@ -146,7 +172,7 @@ export default function TypingBox(props: { placeholder="Send a message" readOnly={isInflight} /> - {props.isDocumentRetrievalActive && ( + {isDocumentRetrievalActive && (
    Promise; - enterChat: (id: string | null) => void; } function chatsReducer( @@ -60,7 +58,6 @@ function chatsReducer( export function useChatList(): ChatListProps { const [chats, setChats] = useReducer(chatsReducer, null); - const [current, setCurrent] = useState(null); useEffect(() => { async function fetchChats() { @@ -81,29 +78,23 @@ export function useChatList(): ChatListProps { assistant_id: string, thread_id: string = uuidv4(), ) => { - const saved = await fetch(`/threads/${thread_id}`, { + const response = await fetch(`/threads/${thread_id}`, { method: "PUT", body: JSON.stringify({ assistant_id, name }), headers: { "Content-Type": "application/json", Accept: "application/json", }, - }).then((r) => r.json()); + }); + const saved = await response.json(); setChats(saved); - setCurrent(saved.thread_id); return saved; }, [], ); - const enterChat = useCallback((id: string | null) => { - setCurrent(id); - }, []); - return { chats, - currentChat: chats?.find((c) => c.thread_id === current) || null, createChat, - enterChat, }; } diff --git a/frontend/src/hooks/useConfigList.ts b/frontend/src/hooks/useConfigList.ts index 561da5ab..276056e8 100644 --- a/frontend/src/hooks/useConfigList.ts +++ b/frontend/src/hooks/useConfigList.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useReducer, useState } from "react"; +import { useCallback, useEffect, useReducer } from "react"; import orderBy from "lodash/orderBy"; import { v4 as uuidv4 } from "uuid"; @@ -18,14 +18,12 @@ export interface Config { export interface ConfigListProps { configs: Config[] | null; - currentConfig: Config | null; saveConfig: ( key: string, config: Config["config"], files: File[], isPublic: boolean, - ) => Promise; - enterConfig: (id: string | null) => void; + ) => Promise; } function configsReducer( @@ -45,43 +43,22 @@ function configsReducer( export function useConfigList(): ConfigListProps { const [configs, setConfigs] = useReducer(configsReducer, null); - const [current, setCurrent] = useState(null); useEffect(() => { async function fetchConfigs() { - const searchParams = new URLSearchParams(window.location.search); - const shared_id = searchParams.get("shared_id"); - const [myConfigs, publicConfigs] = await Promise.all([ - fetch("/assistants/", { - headers: { - Accept: "application/json", - }, - }) - .then((r) => r.json()) - .then((li) => li.map((c: Config) => ({ ...c, mine: true }))), - fetch( - "/assistants/public/" + (shared_id ? `?shared_id=${shared_id}` : ""), - { - headers: { - Accept: "application/json", - }, - }, - ).then((r) => r.json()), - ]); - setConfigs(myConfigs.concat(publicConfigs)); - if (publicConfigs.find((a: Config) => a.assistant_id === shared_id)) { - setCurrent(shared_id); - } + const myConfigs = await fetch("/assistants/", { + headers: { + Accept: "application/json", + }, + }) + .then((r) => r.json()) + .then((li) => li.map((c: Config) => ({ ...c, mine: true }))); + setConfigs(myConfigs); } fetchConfigs(); }, []); - const enterConfig = useCallback((key: string | null) => { - setCurrent(key); - window.scrollTo({ top: 0 }); - }, []); - const saveConfig = useCallback( async ( name: string, @@ -89,7 +66,7 @@ export function useConfigList(): ConfigListProps { files: File[], isPublic: boolean, assistant_id: string = uuidv4(), - ) => { + ): Promise => { const formData = files.reduce((formData, file) => { formData.append("files", file); return formData; @@ -115,15 +92,13 @@ export function useConfigList(): ConfigListProps { : Promise.resolve(), ]); setConfigs({ ...saved, mine: true }); - enterConfig(saved.assistant_id); + return saved.assistant_id; }, - [enterConfig], + [], ); return { configs, - currentConfig: configs?.find((c) => c.assistant_id === current) || null, saveConfig, - enterConfig, }; } diff --git a/frontend/src/hooks/useThreadAndAssistant.ts b/frontend/src/hooks/useThreadAndAssistant.ts new file mode 100644 index 00000000..4b48fd9e --- /dev/null +++ b/frontend/src/hooks/useThreadAndAssistant.ts @@ -0,0 +1,37 @@ +import { useQuery } from "react-query"; +import { useParams } from "react-router-dom"; +import { getAssistant } from "../api/assistants"; +import { getThread } from "../api/threads"; + +export function useThreadAndAssistant() { + // Extract route parameters + const { chatId, assistantId } = useParams(); + + // React Query to fetch chat details if chatId is present + const { data: currentChat, isLoading: isLoadingChat } = useQuery( + ["thread", chatId], + () => getThread(chatId as string), + { + enabled: !!chatId, + }, + ); + + // Determine the assistantId to use: either from the chat or the route directly + const effectiveAssistantId = assistantId || currentChat?.assistant_id; + + // React Query to fetch assistant configuration based on the effectiveAssistantId + const { data: assistantConfig, isLoading: isLoadingAssistant } = useQuery( + ["assistant", effectiveAssistantId], + () => getAssistant(effectiveAssistantId as string), + { + enabled: !!effectiveAssistantId, + }, + ); + + // Return both loading states, the chat data, and the assistant configuration + return { + currentChat, + assistantConfig, + isLoading: isLoadingChat || isLoadingAssistant, + }; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 6d36793d..af504278 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,9 +2,28 @@ import ReactDOM from "react-dom/client"; import { v4 as uuidv4 } from "uuid"; import App from "./App.tsx"; import "./index.css"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { StrictMode } from "react"; +import { QueryClient, QueryClientProvider } from "react-query"; +import { NotFound } from "./components/NotFound.tsx"; if (document.cookie.indexOf("user_id") === -1) { - document.cookie = `opengpts_user_id=${uuidv4()}`; + document.cookie = `opengpts_user_id=${uuidv4()}; path=/; SameSite=Lax`; } -ReactDOM.createRoot(document.getElementById("root")!).render(); +const queryClient = new QueryClient(); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + } /> + } /> + } /> + } /> + + + + , +); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3d29aae8..67836c5b 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -191,6 +191,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.23.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2": + version "7.24.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57" + integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz" @@ -456,6 +463,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@remix-run/router@1.15.3": + version "1.15.3" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.15.3.tgz#d2509048d69dbb72d5389a14945339f1430b2d3c" + integrity sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w== + "@tailwindcss/forms@^0.5.6": version "0.5.6" resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz" @@ -754,6 +766,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +big-integer@^1.6.16: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" @@ -774,6 +791,20 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +broadcast-channel@^3.4.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-3.7.0.tgz#2dfa5c7b4289547ac3f6705f9c00af8723889937" + integrity sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg== + dependencies: + "@babel/runtime" "^7.7.2" + detect-node "^2.1.0" + js-sha3 "0.8.0" + microseconds "0.2.0" + nano-time "1.0.0" + oblivious-set "1.0.0" + rimraf "3.0.2" + unload "2.2.0" + browserslist@^4.21.10, browserslist@^4.21.9: version "4.22.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz" @@ -911,6 +942,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +detect-node@^2.0.4, detect-node@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" @@ -1355,6 +1391,11 @@ jiti@^1.19.1: resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" @@ -1470,6 +1511,14 @@ marked@^9.1.5: resolved "https://registry.npmjs.org/marked/-/marked-9.1.5.tgz" integrity sha512-14QG3shv8Kg/xc0Yh6TNkMj90wXH9mmldi5941I2OevfJ/FQAFLEwtwU2/FfgSAOMlWHrEukWSGQf8MiVYNG2A== +match-sorter@^6.0.2: + version "6.3.4" + resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.4.tgz#afa779d8e922c81971fbcb4781c7003ace781be7" + integrity sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg== + dependencies: + "@babel/runtime" "^7.23.8" + remove-accents "0.5.0" + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -1483,6 +1532,11 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +microseconds@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/microseconds/-/microseconds-0.2.0.tgz#233b25f50c62a65d861f978a4a4f8ec18797dc39" + integrity sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA== + mini-svg-data-uri@^1.2.3: version "1.4.4" resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz" @@ -1509,6 +1563,13 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nano-time@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" + integrity sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA== + dependencies: + big-integer "^1.6.16" + nanoid@^3.3.6: version "3.3.6" resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" @@ -1544,6 +1605,11 @@ object-hash@^3.0.0: resolved "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz" integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== +oblivious-set@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/oblivious-set/-/oblivious-set-1.0.0.tgz#c8316f2c2fb6ff7b11b6158db3234c49f733c566" + integrity sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw== + once@^1.3.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" @@ -1741,11 +1807,35 @@ react-is@^16.13.1: resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-query@^3.39.3: + version "3.39.3" + resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35" + integrity sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g== + dependencies: + "@babel/runtime" "^7.5.5" + broadcast-channel "^3.4.1" + match-sorter "^6.0.2" + react-refresh@^0.14.0: version "0.14.0" resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz" integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== +react-router-dom@^6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.22.3.tgz#9781415667fd1361a475146c5826d9f16752a691" + integrity sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw== + dependencies: + "@remix-run/router" "1.15.3" + react-router "6.22.3" + +react-router@6.22.3: + version "6.22.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.22.3.tgz#9d9142f35e08be08c736a2082db5f0c9540a885e" + integrity sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ== + dependencies: + "@remix-run/router" "1.15.3" + react@^18.2.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" @@ -1772,6 +1862,11 @@ regenerator-runtime@^0.14.0: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz" integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== +remove-accents@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" + integrity sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" @@ -1791,7 +1886,7 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -1995,6 +2090,14 @@ typescript@^5.0.2: resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== +unload@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unload/-/unload-2.2.0.tgz#ccc88fdcad345faa06a92039ec0f80b488880ef7" + integrity sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA== + dependencies: + "@babel/runtime" "^7.6.2" + detect-node "^2.0.4" + update-browserslist-db@^1.0.13: version "1.0.13" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz"