diff --git a/web/app/_components/ActiveModelTable/index.tsx b/web/app/_components/ActiveModelTable/index.tsx index 88e10a66a..e2e3e791e 100644 --- a/web/app/_components/ActiveModelTable/index.tsx +++ b/web/app/_components/ActiveModelTable/index.tsx @@ -1,7 +1,7 @@ -import { currentProductAtom } from "@/_helpers/JotaiWrapper"; import { useAtomValue } from "jotai"; import React, { Fragment } from "react"; import ModelTable from "../ModelTable"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; const ActiveModelTable: React.FC = () => { const activeModel = useAtomValue(currentProductAtom); diff --git a/web/app/_components/AvailableModelCard/index.tsx b/web/app/_components/AvailableModelCard/index.tsx index 64fc0f979..9f6a25741 100644 --- a/web/app/_components/AvailableModelCard/index.tsx +++ b/web/app/_components/AvailableModelCard/index.tsx @@ -2,9 +2,8 @@ import { Product } from "@/_models/Product"; import DownloadModelContent from "../DownloadModelContent"; import ModelDownloadButton from "../ModelDownloadButton"; import ModelDownloadingButton from "../ModelDownloadingButton"; -import ViewModelDetailButton from "../ViewModelDetailButton"; import { useAtomValue } from "jotai"; -import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper"; +import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; type Props = { product: Product; @@ -36,8 +35,6 @@ const AvailableModelCard: React.FC = ({ } } - const handleViewDetails = () => {}; - const downloadButton = isDownloading ? (
diff --git a/web/app/_components/BasicPromptAccessories/index.tsx b/web/app/_components/BasicPromptAccessories/index.tsx index 6d57bdfde..332b9625e 100644 --- a/web/app/_components/BasicPromptAccessories/index.tsx +++ b/web/app/_components/BasicPromptAccessories/index.tsx @@ -1,9 +1,9 @@ "use client"; -import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper"; import { useSetAtom } from "jotai"; import SecondaryButton from "../SecondaryButton"; import SendButton from "../SendButton"; +import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom"; const BasicPromptAccessories: React.FC = () => { const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom); diff --git a/web/app/_components/BasicPromptButton/index.tsx b/web/app/_components/BasicPromptButton/index.tsx index 3a332e022..898c38d10 100644 --- a/web/app/_components/BasicPromptButton/index.tsx +++ b/web/app/_components/BasicPromptButton/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useSetAtom } from "jotai"; import { ChevronLeftIcon } from "@heroicons/react/24/outline"; -import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper"; +import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom"; const BasicPromptButton: React.FC = () => { const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom); diff --git a/web/app/_components/BasicPromptInput/index.tsx b/web/app/_components/BasicPromptInput/index.tsx index ad23c3f95..7ff99d540 100644 --- a/web/app/_components/BasicPromptInput/index.tsx +++ b/web/app/_components/BasicPromptInput/index.tsx @@ -1,22 +1,45 @@ "use client"; import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; +import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom"; +import { selectedModelAtom } from "@/_helpers/atoms/Model.atom"; +import useCreateConversation from "@/_hooks/useCreateConversation"; +import useInitModel from "@/_hooks/useInitModel"; import useSendChatMessage from "@/_hooks/useSendChatMessage"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; +import { ChangeEvent } from "react"; const BasicPromptInput: React.FC = () => { + const activeConversationId = useAtomValue(getActiveConvoIdAtom); + const selectedModel = useAtomValue(selectedModelAtom); const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom); const { sendChatMessage } = useSendChatMessage(); + const { requestCreateConvo } = useCreateConversation(); - const handleMessageChange = (event: any) => { + const { initModel } = useInitModel(); + + const handleMessageChange = (event: ChangeEvent) => { setCurrentPrompt(event.target.value); }; - const handleKeyDown = (event: any) => { + const handleKeyDown = async ( + event: React.KeyboardEvent + ) => { if (event.key === "Enter") { if (!event.shiftKey) { - event.preventDefault(); - sendChatMessage(); + if (activeConversationId) { + event.preventDefault(); + sendChatMessage(); + } else { + if (!selectedModel) { + console.log("No model selected"); + return; + } + + await requestCreateConvo(selectedModel); + await initModel(selectedModel); + sendChatMessage(); + } } } }; diff --git a/web/app/_components/ChatBody/index.tsx b/web/app/_components/ChatBody/index.tsx index 4ec776e78..16f739f77 100644 --- a/web/app/_components/ChatBody/index.tsx +++ b/web/app/_components/ChatBody/index.tsx @@ -4,14 +4,12 @@ import React, { useCallback, useRef, useState } from "react"; import ChatItem from "../ChatItem"; import { ChatMessage } from "@/_models/ChatMessage"; import useChatMessages from "@/_hooks/useChatMessages"; -import { - chatMessages, - getActiveConvoIdAtom, - showingTyping, -} from "@/_helpers/JotaiWrapper"; +import { showingTyping } from "@/_helpers/JotaiWrapper"; import { useAtomValue } from "jotai"; import { selectAtom } from "jotai/utils"; import LoadingIndicator from "../LoadingIndicator"; +import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom"; +import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom"; const ChatBody: React.FC = () => { const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ""; diff --git a/web/app/_components/ChatBody/renderChatMessage.tsx b/web/app/_components/ChatBody/renderChatMessage.tsx index cebaa62ee..f0b640cd5 100644 --- a/web/app/_components/ChatBody/renderChatMessage.tsx +++ b/web/app/_components/ChatBody/renderChatMessage.tsx @@ -4,7 +4,7 @@ import SimpleTextMessage from "../SimpleTextMessage"; import { ChatMessage, MessageType } from "@/_models/ChatMessage"; import StreamTextMessage from "../StreamTextMessage"; import { useAtomValue } from "jotai"; -import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper"; +import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom"; export default function renderChatMessage({ id, diff --git a/web/app/_components/ChatContainer/index.tsx b/web/app/_components/ChatContainer/index.tsx index db37ed9c0..9a002ab13 100644 --- a/web/app/_components/ChatContainer/index.tsx +++ b/web/app/_components/ChatContainer/index.tsx @@ -1,12 +1,16 @@ "use client"; import { useAtomValue } from "jotai"; -import { MainViewState, getMainViewStateAtom } from "@/_helpers/JotaiWrapper"; import { ReactNode } from "react"; import Welcome from "../WelcomeContainer"; import { Preferences } from "../Preferences"; import MyModelContainer from "../MyModelContainer"; import ExploreModelContainer from "../ExploreModelContainer"; +import { + MainViewState, + getMainViewStateAtom, +} from "@/_helpers/atoms/MainView.atom"; +import EmptyChatContainer from "../EmptyChatContainer"; type Props = { children: ReactNode; @@ -16,6 +20,8 @@ export default function ChatContainer({ children }: Props) { const viewState = useAtomValue(getMainViewStateAtom); switch (viewState) { + case MainViewState.ConversationEmptyModel: + return case MainViewState.ExploreModel: return ; case MainViewState.Setting: diff --git a/web/app/_components/CompactLogo/index.tsx b/web/app/_components/CompactLogo/index.tsx index 5c20183d0..0035d004f 100644 --- a/web/app/_components/CompactLogo/index.tsx +++ b/web/app/_components/CompactLogo/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import JanImage from "../JanImage"; -import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper"; import { useSetAtom } from "jotai"; +import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom"; const CompactLogo: React.FC = () => { const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); diff --git a/web/app/_components/ConfirmDeleteConversationModal/index.tsx b/web/app/_components/ConfirmDeleteConversationModal/index.tsx index baa7ac471..4904bc454 100644 --- a/web/app/_components/ConfirmDeleteConversationModal/index.tsx +++ b/web/app/_components/ConfirmDeleteConversationModal/index.tsx @@ -1,4 +1,4 @@ -import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper"; +import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom"; import useDeleteConversation from "@/_hooks/useDeleteConversation"; import { Dialog, Transition } from "@headlessui/react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; diff --git a/web/app/_components/ConfirmDeleteModelModal/index.tsx b/web/app/_components/ConfirmDeleteModelModal/index.tsx index b635de23d..451eddb60 100644 --- a/web/app/_components/ConfirmDeleteModelModal/index.tsx +++ b/web/app/_components/ConfirmDeleteModelModal/index.tsx @@ -1,8 +1,8 @@ import React, { Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; -import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper"; import { useAtom } from "jotai"; +import { showConfirmDeleteModalAtom } from "@/_helpers/atoms/Modal.atom"; const ConfirmDeleteModelModal: React.FC = () => { const [show, setShow] = useAtom(showConfirmDeleteModalAtom); diff --git a/web/app/_components/ConfirmSignOutModal/index.tsx b/web/app/_components/ConfirmSignOutModal/index.tsx index 13a2c8687..2acc8acd9 100644 --- a/web/app/_components/ConfirmSignOutModal/index.tsx +++ b/web/app/_components/ConfirmSignOutModal/index.tsx @@ -1,9 +1,9 @@ import React, { Fragment } from "react"; import { Dialog, Transition } from "@headlessui/react"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; -import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper"; import { useAtom } from "jotai"; import useSignOut from "@/_hooks/useSignOut"; +import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom"; const ConfirmSignOutModal: React.FC = () => { const [show, setShow] = useAtom(showConfirmSignOutModalAtom); diff --git a/web/app/_components/EmptyChatContainer/index.tsx b/web/app/_components/EmptyChatContainer/index.tsx new file mode 100644 index 000000000..caeff5655 --- /dev/null +++ b/web/app/_components/EmptyChatContainer/index.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import SelectModels from "../ModelSelector"; +import InputToolbar from "../InputToolbar"; + +const EmptyChatContainer: React.FC = () => ( +
+
+ +
+ +
+); + +export default EmptyChatContainer; diff --git a/web/app/_components/ExploreModelItem/index.tsx b/web/app/_components/ExploreModelItem/index.tsx index 3a1939ec0..35d1de650 100644 --- a/web/app/_components/ExploreModelItem/index.tsx +++ b/web/app/_components/ExploreModelItem/index.tsx @@ -7,8 +7,8 @@ import { Product } from "@/_models/Product"; import SimpleTag, { TagType } from "../SimpleTag"; import { displayDate } from "@/_utils/datetime"; import useDownloadModel from "@/_hooks/useDownloadModel"; -import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper"; import { atom, useAtomValue } from "jotai"; +import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; type Props = { model: Product; diff --git a/web/app/_components/HamburgerButton/index.tsx b/web/app/_components/HamburgerButton/index.tsx index 2efd41bf8..d3e775631 100644 --- a/web/app/_components/HamburgerButton/index.tsx +++ b/web/app/_components/HamburgerButton/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper"; +import { showingMobilePaneAtom } from "@/_helpers/atoms/Modal.atom"; import { Bars3Icon } from "@heroicons/react/24/outline"; import { useSetAtom } from "jotai"; import React from "react"; diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 2a708c113..f243fbd32 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -1,18 +1,20 @@ import React from "react"; import JanImage from "../JanImage"; -import { - MainViewState, - conversationStatesAtom, - currentProductAtom, - getActiveConvoIdAtom, - setActiveConvoIdAtom, - setMainViewStateAtom, -} from "@/_helpers/JotaiWrapper"; import { useAtomValue, useSetAtom } from "jotai"; import Image from "next/image"; import { Conversation } from "@/_models/Conversation"; -import { DataService, InfereceService } from "../../../shared/coreService"; +import { DataService } from "../../../shared/coreService"; import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager"; +import { + conversationStatesAtom, + getActiveConvoIdAtom, + setActiveConvoIdAtom, +} from "@/_helpers/atoms/Conversation.atom"; +import { + setMainViewStateAtom, + MainViewState, +} from "@/_helpers/atoms/MainView.atom"; +import useInitModel from "@/_hooks/useInitModel"; type Props = { conversation: Conversation; @@ -33,7 +35,7 @@ const HistoryItem: React.FC = ({ const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const isSelected = activeConvoId === conversation.id; - const setActiveProduct = useSetAtom(currentProductAtom); + const { initModel } = useInitModel(); const onClick = async () => { const model = await executeSerial( @@ -45,10 +47,7 @@ const HistoryItem: React.FC = ({ `Model ${conversation.model_id} not found! Please re-download the model first.` ); } else { - setActiveProduct(model); - executeSerial(InfereceService.INIT_MODEL, model) - .then(() => console.info(`Init model success`)) - .catch((err) => console.log(`Init model error ${err}`)); + initModel(model); } if (activeConvoId !== conversation.id) { setMainViewState(MainViewState.Conversation); diff --git a/web/app/_components/HistoryList/index.tsx b/web/app/_components/HistoryList/index.tsx index e3f79cbf7..391b5504b 100644 --- a/web/app/_components/HistoryList/index.tsx +++ b/web/app/_components/HistoryList/index.tsx @@ -2,9 +2,10 @@ import HistoryItem from "../HistoryItem"; import { useEffect, useState } from "react"; import ExpandableHeader from "../ExpandableHeader"; import { useAtomValue } from "jotai"; -import { searchAtom, userConversationsAtom } from "@/_helpers/JotaiWrapper"; +import { searchAtom } from "@/_helpers/JotaiWrapper"; import useGetUserConversations from "@/_hooks/useGetUserConversations"; import SidebarEmptyHistory from "../SidebarEmptyHistory"; +import { userConversationsAtom } from "@/_helpers/atoms/Conversation.atom"; const HistoryList: React.FC = () => { const conversations = useAtomValue(userConversationsAtom); diff --git a/web/app/_components/InputToolbar/index.tsx b/web/app/_components/InputToolbar/index.tsx index 8fbafff0d..d8c48b764 100644 --- a/web/app/_components/InputToolbar/index.tsx +++ b/web/app/_components/InputToolbar/index.tsx @@ -2,8 +2,8 @@ import BasicPromptInput from "../BasicPromptInput"; import BasicPromptAccessories from "../BasicPromptAccessories"; -import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper"; import { useAtomValue } from "jotai"; +import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom"; const InputToolbar: React.FC = () => { const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); diff --git a/web/app/_components/JanLogo/index.tsx b/web/app/_components/JanLogo/index.tsx index 93f68bfa9..3fc20f5e4 100644 --- a/web/app/_components/JanLogo/index.tsx +++ b/web/app/_components/JanLogo/index.tsx @@ -1,4 +1,4 @@ -import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper"; +import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom"; import { useSetAtom } from "jotai"; import Image from "next/image"; import React from "react"; diff --git a/web/app/_components/LeftContainer/index.tsx b/web/app/_components/LeftContainer/index.tsx index a7b8a856c..df525ce49 100644 --- a/web/app/_components/LeftContainer/index.tsx +++ b/web/app/_components/LeftContainer/index.tsx @@ -3,14 +3,12 @@ import SidebarFooter from "../SidebarFooter"; import SidebarHeader from "../SidebarHeader"; import SidebarMenu from "../SidebarMenu"; import HistoryList from "../HistoryList"; -import SecondaryButton from "../SecondaryButton"; +import NewChatButton from "../NewChatButton"; const LeftContainer: React.FC = () => (
-
- {}} /> -
+ diff --git a/web/app/_components/MenuHeader/index.tsx b/web/app/_components/MenuHeader/index.tsx index 95e0be017..851af41c1 100644 --- a/web/app/_components/MenuHeader/index.tsx +++ b/web/app/_components/MenuHeader/index.tsx @@ -3,7 +3,7 @@ import { Popover, Transition } from "@headlessui/react"; import { Fragment } from "react"; // import useGetCurrentUser from "@/_hooks/useGetCurrentUser"; import { useSetAtom } from "jotai"; -import { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper"; +import { showConfirmSignOutModalAtom } from "@/_helpers/atoms/Modal.atom"; export const MenuHeader: React.FC = () => { const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom); diff --git a/web/app/_components/MobileMenuPane/index.tsx b/web/app/_components/MobileMenuPane/index.tsx index 4394e0daa..27dff1a52 100644 --- a/web/app/_components/MobileMenuPane/index.tsx +++ b/web/app/_components/MobileMenuPane/index.tsx @@ -2,8 +2,8 @@ import React, { useRef } from "react"; import { Dialog } from "@headlessui/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; -import { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper"; import { useAtom } from "jotai"; +import { showingMobilePaneAtom } from "@/_helpers/atoms/Modal.atom"; const MobileMenuPane: React.FC = () => { const [show, setShow] = useAtom(showingMobilePaneAtom); diff --git a/web/app/_components/ModelMenu/index.tsx b/web/app/_components/ModelMenu/index.tsx index f8113e001..006042317 100644 --- a/web/app/_components/ModelMenu/index.tsx +++ b/web/app/_components/ModelMenu/index.tsx @@ -1,12 +1,10 @@ "use client"; import { useAtomValue, useSetAtom } from "jotai"; -import { - currentProductAtom, - showConfirmDeleteConversationModalAtom, -} from "@/_helpers/JotaiWrapper"; import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import useCreateConversation from "@/_hooks/useCreateConversation"; +import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; const ModelMenu: React.FC = () => { const currentProduct = useAtomValue(currentProductAtom); diff --git a/web/app/_components/ModelRow/index.tsx b/web/app/_components/ModelRow/index.tsx index 1ce2effce..7c98b6edd 100644 --- a/web/app/_components/ModelRow/index.tsx +++ b/web/app/_components/ModelRow/index.tsx @@ -4,10 +4,10 @@ import Image from "next/image"; import { ModelStatus, ModelStatusComponent } from "../ModelStatusComponent"; import ModelActionMenu from "../ModelActionMenu"; import { useAtomValue } from "jotai"; -import { currentProductAtom } from "@/_helpers/JotaiWrapper"; import ModelActionButton, { ModelActionType } from "../ModelActionButton"; import useStartStopModel from "@/_hooks/useStartStopModel"; import useDeleteModel from "@/_hooks/useDeleteModel"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; type Props = { model: Product; diff --git a/web/app/_components/ModelSelector/index.tsx b/web/app/_components/ModelSelector/index.tsx new file mode 100644 index 000000000..5564788bf --- /dev/null +++ b/web/app/_components/ModelSelector/index.tsx @@ -0,0 +1,118 @@ +import { Fragment, useEffect } from "react"; +import { Listbox, Transition } from "@headlessui/react"; +import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; +import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; +import { Product } from "@/_models/Product"; +import { useAtom } from "jotai"; +import { selectedModelAtom } from "@/_helpers/atoms/Model.atom"; + +function classNames(...classes: any) { + return classes.filter(Boolean).join(" "); +} + +const SelectModels: React.FC = () => { + const { downloadedModels } = useGetDownloadedModels(); + const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom); + + useEffect(() => { + if (downloadedModels && downloadedModels.length > 0) { + onModelSelected(downloadedModels[0]); + } + }, [downloadedModels]); + + const onModelSelected = (model: Product) => { + setSelectedModel(model); + }; + + if (!selectedModel) { + return
You have not downloaded any model!
; + } + + return ( + + {({ open }) => ( +
+ + Select a Model: + +
+ + + + + {selectedModel.name} + + + + + + + + + {downloadedModels.map((model) => ( + + classNames( + active ? "bg-indigo-600 text-white" : "text-gray-900", + "relative cursor-default select-none py-2 pl-3 pr-9" + ) + } + value={model} + > + {({ selected, active }) => ( + <> +
+ + + {model.name} + +
+ + {selected ? ( + + + ) : null} + + )} +
+ ))} +
+
+
+
+ )} +
+ ); +}; + +export default SelectModels; diff --git a/web/app/_components/MonitorBar/index.tsx b/web/app/_components/MonitorBar/index.tsx index 39b884849..35fd6d820 100644 --- a/web/app/_components/MonitorBar/index.tsx +++ b/web/app/_components/MonitorBar/index.tsx @@ -1,14 +1,12 @@ import ProgressBar from "../ProgressBar"; import SystemItem from "../SystemItem"; import { useAtomValue } from "jotai"; -import { - appDownloadProgress, - currentProductAtom, - getSystemBarVisibilityAtom, -} from "@/_helpers/JotaiWrapper"; +import { appDownloadProgress } from "@/_helpers/JotaiWrapper"; import { useEffect, useState } from "react"; import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager"; import { SystemMonitoringService } from "../../../shared/coreService"; +import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; const MonitorBar: React.FC = () => { const show = useAtomValue(getSystemBarVisibilityAtom); diff --git a/web/app/_components/NewChatButton/index.tsx b/web/app/_components/NewChatButton/index.tsx new file mode 100644 index 000000000..3fc7de8e8 --- /dev/null +++ b/web/app/_components/NewChatButton/index.tsx @@ -0,0 +1,39 @@ +"use client"; + +import React from "react"; +import SecondaryButton from "../SecondaryButton"; +import { useAtomValue, useSetAtom } from "jotai"; +import { + MainViewState, + setMainViewStateAtom, +} from "@/_helpers/atoms/MainView.atom"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import useCreateConversation from "@/_hooks/useCreateConversation"; +import useInitModel from "@/_hooks/useInitModel"; +import { Product } from "@/_models/Product"; + +const NewChatButton: React.FC = () => { + const activeModel = useAtomValue(currentProductAtom); + const setMainView = useSetAtom(setMainViewStateAtom); + const { requestCreateConvo } = useCreateConversation(); + const { initModel } = useInitModel(); + + const onClick = () => { + if (!activeModel) { + setMainView(MainViewState.ConversationEmptyModel); + } else { + createConversationAndInitModel(activeModel); + } + }; + + const createConversationAndInitModel = async (model: Product) => { + await requestCreateConvo(model); + await initModel(model); + }; + + return ( + + ); +}; + +export default NewChatButton; diff --git a/web/app/_components/SecondaryButton/index.tsx b/web/app/_components/SecondaryButton/index.tsx index dc1cd80e9..b4abda6cd 100644 --- a/web/app/_components/SecondaryButton/index.tsx +++ b/web/app/_components/SecondaryButton/index.tsx @@ -2,14 +2,20 @@ type Props = { title: string; onClick: () => void; disabled?: boolean; + className?: string; }; -const SecondaryButton: React.FC = ({ title, onClick, disabled }) => ( +const SecondaryButton: React.FC = ({ + title, + onClick, + disabled, + className, +}) => ( diff --git a/web/app/_components/SendButton/index.tsx b/web/app/_components/SendButton/index.tsx index 07c41fdf0..8b6cac21b 100644 --- a/web/app/_components/SendButton/index.tsx +++ b/web/app/_components/SendButton/index.tsx @@ -1,7 +1,5 @@ -import { - currentConvoStateAtom, - currentPromptAtom, -} from "@/_helpers/JotaiWrapper"; +import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; +import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom"; import useSendChatMessage from "@/_hooks/useSendChatMessage"; import { useAtom, useAtomValue } from "jotai"; import Image from "next/image"; diff --git a/web/app/_components/SidebarButton/index.tsx b/web/app/_components/SidebarButton/index.tsx deleted file mode 100644 index 8db7a386b..000000000 --- a/web/app/_components/SidebarButton/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import Image from "next/image"; - -type Props = { - callback?: () => void; - className?: string; - icon: string; - width: number; - height: number; - title: string; -}; - -export const SidebarButton: React.FC = ({ - callback, - height, - icon, - className, - width, - title, -}) => ( - -); diff --git a/web/app/_components/SidebarEmptyHistory/index.tsx b/web/app/_components/SidebarEmptyHistory/index.tsx index af1da8998..ca6cea543 100644 --- a/web/app/_components/SidebarEmptyHistory/index.tsx +++ b/web/app/_components/SidebarEmptyHistory/index.tsx @@ -1,28 +1,56 @@ import Image from "next/image"; -import { SidebarButton } from "../SidebarButton"; -import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager"; -import { DataService } from "../../../shared/coreService"; import useCreateConversation from "@/_hooks/useCreateConversation"; +import PrimaryButton from "../PrimaryButton"; +import { useAtomValue, useSetAtom } from "jotai"; +import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; +import { useEffect, useState } from "react"; +import { + MainViewState, + setMainViewStateAtom, +} from "@/_helpers/atoms/MainView.atom"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; +import useInitModel from "@/_hooks/useInitModel"; +import { Product } from "@/_models/Product"; + +enum ActionButton { + DownloadModel = "Download a Model", + StartChat = "Start a Conversation", +} const SidebarEmptyHistory: React.FC = () => { + const { downloadedModels } = useGetDownloadedModels(); + const activeModel = useAtomValue(currentProductAtom); + const setMainView = useSetAtom(setMainViewStateAtom); const { requestCreateConvo } = useCreateConversation(); - const startChat = async () => { - // Host - if (window && !window.electronAPI) { - // requestCreateConvo(); // TODO: get model id from somewhere - } - // Electron - const downloadedModels = await executeSerial( - DataService.GET_FINISHED_DOWNLOAD_MODELS - ); - if (!downloadedModels || downloadedModels?.length === 0) { - alert( - "Seems like there is no model downloaded yet. Please download a model first." - ); + const [action, setAction] = useState(ActionButton.DownloadModel); + + const { initModel } = useInitModel(); + + useEffect(() => { + if (downloadedModels.length > 0) { + setAction(ActionButton.StartChat); } else { - requestCreateConvo(downloadedModels[0]); + setAction(ActionButton.DownloadModel); + } + }, [downloadedModels]); + + const onClick = () => { + if (action === ActionButton.DownloadModel) { + setMainView(MainViewState.ExploreModel); + } else { + if (!activeModel) { + setMainView(MainViewState.ConversationEmptyModel); + } else { + createConversationAndInitModel(activeModel); + } } }; + + const createConversationAndInitModel = async (model: Product) => { + await requestCreateConvo(model); + await initModel(model); + }; + return (
{ alt="" />
-
-
- No Chat History -
-
- Get started by creating a new chat. -
+
No Chat History
+
+ Get started by creating a new chat.
- +
); diff --git a/web/app/_components/SidebarFooter/index.tsx b/web/app/_components/SidebarFooter/index.tsx index 44bcc9f49..97bab2748 100644 --- a/web/app/_components/SidebarFooter/index.tsx +++ b/web/app/_components/SidebarFooter/index.tsx @@ -1,27 +1,21 @@ import React from "react"; -import { SidebarButton } from "../SidebarButton"; +import SecondaryButton from "../SecondaryButton"; const SidebarFooter: React.FC = () => (
- { - window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N"); - }} + + window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N") + } + className="flex-1" /> - { - window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai"); - }} + + window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai") + } + className="flex-1" />
); diff --git a/web/app/_components/SidebarMenu/index.tsx b/web/app/_components/SidebarMenu/index.tsx index f465a7271..2e772d427 100644 --- a/web/app/_components/SidebarMenu/index.tsx +++ b/web/app/_components/SidebarMenu/index.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { MainViewState } from "@/_helpers/JotaiWrapper"; import SidebarMenuItem from "../SidebarMenuItem"; +import { MainViewState } from "@/_helpers/atoms/MainView.atom"; const menu = [ { diff --git a/web/app/_components/SidebarMenuItem/index.tsx b/web/app/_components/SidebarMenuItem/index.tsx index 68960fabd..2fbc50a5d 100644 --- a/web/app/_components/SidebarMenuItem/index.tsx +++ b/web/app/_components/SidebarMenuItem/index.tsx @@ -1,11 +1,11 @@ import React from "react"; +import { useAtomValue, useSetAtom } from "jotai"; +import Image from "next/image"; import { MainViewState, getMainViewStateAtom, setMainViewStateAtom, -} from "@/_helpers/JotaiWrapper"; -import { useAtomValue, useSetAtom } from "jotai"; -import Image from "next/image"; +} from "@/_helpers/atoms/MainView.atom"; type Props = { title: string; diff --git a/web/app/_components/StreamTextMessage/index.tsx b/web/app/_components/StreamTextMessage/index.tsx index 0dd621cce..e313e12aa 100644 --- a/web/app/_components/StreamTextMessage/index.tsx +++ b/web/app/_components/StreamTextMessage/index.tsx @@ -4,7 +4,7 @@ import { TextCode } from "../TextCode"; import { getMessageCode } from "@/_utils/message"; import Image from "next/image"; import { useAtomValue } from "jotai"; -import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper"; +import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom"; type Props = { id: string; diff --git a/web/app/_components/UserToolbar/index.tsx b/web/app/_components/UserToolbar/index.tsx index 7cbf3430d..bc1b7e845 100644 --- a/web/app/_components/UserToolbar/index.tsx +++ b/web/app/_components/UserToolbar/index.tsx @@ -1,6 +1,6 @@ "use client"; -import { currentConversationAtom } from "@/_helpers/JotaiWrapper"; +import { currentConversationAtom } from "@/_helpers/atoms/Conversation.atom"; import { useAtomValue } from "jotai"; import Image from "next/image"; diff --git a/web/app/_components/WelcomeContainer/index.tsx b/web/app/_components/WelcomeContainer/index.tsx index 7124a0dfd..4ab1097c1 100644 --- a/web/app/_components/WelcomeContainer/index.tsx +++ b/web/app/_components/WelcomeContainer/index.tsx @@ -1,7 +1,10 @@ import Image from "next/image"; -import { SidebarButton } from "../SidebarButton"; import { useSetAtom } from "jotai"; -import { MainViewState, setMainViewStateAtom } from "@/_helpers/JotaiWrapper"; +import { + setMainViewStateAtom, + MainViewState, +} from "@/_helpers/atoms/MainView.atom"; +import SecondaryButton from "../SecondaryButton"; const Welcome: React.FC = () => { const setMainViewState = useSetAtom(setMainViewStateAtom); @@ -15,13 +18,9 @@ const Welcome: React.FC = () => {
let’s download your first model - setMainViewState(MainViewState.ExploreModel)} - className="flex flex-row-reverse items-center rounded-lg gap-2 px-3 py-2 text-xs font-medium border border-gray-200" - icon={"icons/app_icon.svg"} - title="Explore models" - height={16} - width={16} + setMainViewState(MainViewState.ExploreModel)} />
diff --git a/web/app/_helpers/EventListenerWrapper.tsx b/web/app/_helpers/EventListenerWrapper.tsx index 74c4b9c8c..8f334d50f 100644 --- a/web/app/_helpers/EventListenerWrapper.tsx +++ b/web/app/_helpers/EventListenerWrapper.tsx @@ -1,15 +1,15 @@ "use client"; -import { useAtom, useSetAtom } from "jotai"; +import { useSetAtom } from "jotai"; import { ReactNode, useEffect } from "react"; -import { - appDownloadProgress, - setDownloadStateAtom, - setDownloadStateSuccessAtom, -} from "./JotaiWrapper"; +import { appDownloadProgress } from "./JotaiWrapper"; import { DownloadState } from "@/_models/DownloadState"; import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager"; import { DataService } from "../../shared/coreService"; +import { + setDownloadStateAtom, + setDownloadStateSuccessAtom, +} from "./atoms/DownloadState.atom"; type Props = { children: ReactNode; diff --git a/web/app/_helpers/JotaiWrapper.tsx b/web/app/_helpers/JotaiWrapper.tsx index 362a06c87..63e4a321c 100644 --- a/web/app/_helpers/JotaiWrapper.tsx +++ b/web/app/_helpers/JotaiWrapper.tsx @@ -1,9 +1,5 @@ "use client"; -import { ChatMessage, MessageStatus } from "@/_models/ChatMessage"; -import { Conversation, ConversationState } from "@/_models/Conversation"; -import { DownloadState } from "@/_models/DownloadState"; -import { Product } from "@/_models/Product"; import { Provider, atom } from "jotai"; import { ReactNode } from "react"; @@ -15,286 +11,11 @@ export default function JotaiWrapper({ children }: Props) { return {children}; } -const activeConversationIdAtom = atom(undefined); -export const getActiveConvoIdAtom = atom((get) => - get(activeConversationIdAtom) -); -export const setActiveConvoIdAtom = atom( - null, - (_get, set, convoId: string | undefined) => { - if (convoId) { - console.log(`set active convo id to ${convoId}`); - set(setMainViewStateAtom, MainViewState.Conversation); - } - set(activeConversationIdAtom, convoId); - } -); - export const currentPromptAtom = atom(""); -export const showingAdvancedPromptAtom = atom(false); -export const showingProductDetailAtom = atom(false); -export const showingMobilePaneAtom = atom(false); export const showingTyping = atom(false); export const appDownloadProgress = atom(-1); export const searchingModelText = atom(""); -/** - * Stores all conversations for the current user - */ -export const userConversationsAtom = atom([]); -export const currentConversationAtom = atom((get) => - get(userConversationsAtom).find((c) => c.id === get(activeConversationIdAtom)) -); -export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => { - const convo = get(userConversationsAtom).find((c) => c.id === convoId); - if (!convo) return; - const newConvo: Conversation = { - ...convo, - updated_at: new Date().toISOString(), - }; - const newConversations: Conversation[] = get(userConversationsAtom).map((c) => - c.id === convoId ? newConvo : c - ); - - set(userConversationsAtom, newConversations); -}); - -export const currentStreamingMessageAtom = atom( - undefined -); - -export const setConvoLastImageAtom = atom( - null, - (get, set, convoId: string, lastImageUrl: string) => { - const convo = get(userConversationsAtom).find((c) => c.id === convoId); - if (!convo) return; - const newConvo: Conversation = { ...convo }; - const newConversations: Conversation[] = get(userConversationsAtom).map( - (c) => (c.id === convoId ? newConvo : c) - ); - - set(userConversationsAtom, newConversations); - } -); - -/** - * Stores all conversation states for the current user - */ -export const conversationStatesAtom = atom>( - {} -); -export const currentConvoStateAtom = atom( - (get) => { - const activeConvoId = get(activeConversationIdAtom); - if (!activeConvoId) { - console.log("active convo id is undefined"); - return undefined; - } - - return get(conversationStatesAtom)[activeConvoId]; - } -); -export const addNewConversationStateAtom = atom( - null, - (get, set, conversationId: string, state: ConversationState) => { - const currentState = { ...get(conversationStatesAtom) }; - currentState[conversationId] = state; - set(conversationStatesAtom, currentState); - } -); -export const updateConversationWaitingForResponseAtom = atom( - null, - (get, set, conversationId: string, waitingForResponse: boolean) => { - const currentState = { ...get(conversationStatesAtom) }; - currentState[conversationId] = { - ...currentState[conversationId], - waitingForResponse, - }; - set(conversationStatesAtom, currentState); - } -); -export const updateConversationHasMoreAtom = atom( - null, - (get, set, conversationId: string, hasMore: boolean) => { - const currentState = { ...get(conversationStatesAtom) }; - currentState[conversationId] = { ...currentState[conversationId], hasMore }; - set(conversationStatesAtom, currentState); - } -); - -/** - * Stores all chat messages for all conversations - */ -export const chatMessages = atom>({}); -export const currentChatMessagesAtom = atom((get) => { - const activeConversationId = get(activeConversationIdAtom); - if (!activeConversationId) return []; - return get(chatMessages)[activeConversationId] ?? []; -}); - -export const addOldMessagesAtom = atom( - null, - (get, set, newMessages: ChatMessage[]) => { - const currentConvoId = get(activeConversationIdAtom); - if (!currentConvoId) return; - - const currentMessages = get(chatMessages)[currentConvoId] ?? []; - const updatedMessages = [...currentMessages, ...newMessages]; - - const newData: Record = { - ...get(chatMessages), - }; - newData[currentConvoId] = updatedMessages; - set(chatMessages, newData); - } -); -export const addNewMessageAtom = atom( - null, - (get, set, newMessage: ChatMessage) => { - const currentConvoId = get(activeConversationIdAtom); - if (!currentConvoId) return; - - const currentMessages = get(chatMessages)[currentConvoId] ?? []; - const updatedMessages = [newMessage, ...currentMessages]; - - const newData: Record = { - ...get(chatMessages), - }; - newData[currentConvoId] = updatedMessages; - set(chatMessages, newData); - } -); - -export const deleteConversationMessage = atom(null, (get, set, id: string) => { - const newData: Record = { - ...get(chatMessages), - }; - newData[id] = []; - set(chatMessages, newData); -}); - -export const updateMessageAtom = atom( - null, - (get, set, id: string, conversationId: string, text: string) => { - const messages = get(chatMessages)[conversationId] ?? []; - const message = messages.find((e) => e.id === id); - if (message) { - message.text = text; - const updatedMessages = [...messages]; - - const newData: Record = { - ...get(chatMessages), - }; - newData[conversationId] = updatedMessages; - set(chatMessages, newData); - } - } -); -/** - * For updating the status of the last AI message that is pending - */ -export const updateLastMessageAsReadyAtom = atom( - null, - (get, set, id, text: string) => { - const currentConvoId = get(activeConversationIdAtom); - if (!currentConvoId) return; - - const currentMessages = get(chatMessages)[currentConvoId] ?? []; - const messageToUpdate = currentMessages.find((e) => e.id === id); - - // if message is not found, do nothing - if (!messageToUpdate) return; - - const index = currentMessages.indexOf(messageToUpdate); - const updatedMsg: ChatMessage = { - ...messageToUpdate, - status: MessageStatus.Ready, - text: text, - }; - - currentMessages[index] = updatedMsg; - const newData: Record = { - ...get(chatMessages), - }; - newData[currentConvoId] = currentMessages; - set(chatMessages, newData); - } -); - -export const currentProductAtom = atom(undefined); - export const searchAtom = atom(""); - -// modal atoms -export const showConfirmDeleteConversationModalAtom = atom(false); -export const showConfirmSignOutModalAtom = atom(false); -export const showConfirmDeleteModalAtom = atom(false); - -export type FileDownloadStates = { - [key: string]: DownloadState; -}; - -// main view state -export enum MainViewState { - Welcome, - ExploreModel, - MyModel, - ResourceMonitor, - Setting, - Conversation, -} - -const systemBarVisibilityAtom = atom(true); -export const getSystemBarVisibilityAtom = atom((get) => - get(systemBarVisibilityAtom) -); - -const currentMainViewStateAtom = atom(MainViewState.Welcome); -export const getMainViewStateAtom = atom((get) => - get(currentMainViewStateAtom) -); - -export const setMainViewStateAtom = atom( - null, - (get, set, state: MainViewState) => { - if (get(getMainViewStateAtom) === state) return; - if (state !== MainViewState.Conversation) { - set(activeConversationIdAtom, undefined); - } - const showSystemBar = state !== MainViewState.Conversation; - set(systemBarVisibilityAtom, showSystemBar); - set(currentMainViewStateAtom, state); - } -); - -// download states -export const modelDownloadStateAtom = atom({}); - -export const setDownloadStateAtom = atom( - null, - (get, set, state: DownloadState) => { - const currentState = { ...get(modelDownloadStateAtom) }; - console.debug( - `current download state for ${state.fileName} is ${JSON.stringify(state)}` - ); - currentState[state.fileName] = state; - set(modelDownloadStateAtom, currentState); - } -); - -export const setDownloadStateSuccessAtom = atom( - null, - (get, set, fileName: string) => { - const currentState = { ...get(modelDownloadStateAtom) }; - const state = currentState[fileName]; - if (!state) { - console.error(`Cannot find download state for ${fileName}`); - return; - } - - delete currentState[fileName]; - set(modelDownloadStateAtom, currentState); - } -); diff --git a/web/app/_helpers/atoms/ChatMessage.atom.ts b/web/app/_helpers/atoms/ChatMessage.atom.ts new file mode 100644 index 000000000..08ec0a367 --- /dev/null +++ b/web/app/_helpers/atoms/ChatMessage.atom.ts @@ -0,0 +1,109 @@ +import { ChatMessage, MessageStatus } from "@/_models/ChatMessage"; +import { atom } from "jotai"; +import { getActiveConvoIdAtom } from "./Conversation.atom"; + +/** + * Stores all chat messages for all conversations + */ +export const chatMessages = atom>({}); + +export const currentChatMessagesAtom = atom((get) => { + const activeConversationId = get(getActiveConvoIdAtom); + if (!activeConversationId) return []; + return get(chatMessages)[activeConversationId] ?? []; +}); + +export const addOldMessagesAtom = atom( + null, + (get, set, newMessages: ChatMessage[]) => { + const currentConvoId = get(getActiveConvoIdAtom); + if (!currentConvoId) return; + + const currentMessages = get(chatMessages)[currentConvoId] ?? []; + const updatedMessages = [...currentMessages, ...newMessages]; + + const newData: Record = { + ...get(chatMessages), + }; + newData[currentConvoId] = updatedMessages; + set(chatMessages, newData); + } +); + +export const addNewMessageAtom = atom( + null, + (get, set, newMessage: ChatMessage) => { + const currentConvoId = get(getActiveConvoIdAtom); + if (!currentConvoId) return; + + const currentMessages = get(chatMessages)[currentConvoId] ?? []; + const updatedMessages = [newMessage, ...currentMessages]; + + const newData: Record = { + ...get(chatMessages), + }; + newData[currentConvoId] = updatedMessages; + set(chatMessages, newData); + } +); + +export const deleteConversationMessage = atom(null, (get, set, id: string) => { + const newData: Record = { + ...get(chatMessages), + }; + newData[id] = []; + set(chatMessages, newData); +}); + +export const updateMessageAtom = atom( + null, + (get, set, id: string, conversationId: string, text: string) => { + const messages = get(chatMessages)[conversationId] ?? []; + const message = messages.find((e) => e.id === id); + if (message) { + message.text = text; + const updatedMessages = [...messages]; + + const newData: Record = { + ...get(chatMessages), + }; + newData[conversationId] = updatedMessages; + set(chatMessages, newData); + } + } +); + +/** + * For updating the status of the last AI message that is pending + */ +export const updateLastMessageAsReadyAtom = atom( + null, + (get, set, id, text: string) => { + const currentConvoId = get(getActiveConvoIdAtom); + if (!currentConvoId) return; + + const currentMessages = get(chatMessages)[currentConvoId] ?? []; + const messageToUpdate = currentMessages.find((e) => e.id === id); + + // if message is not found, do nothing + if (!messageToUpdate) return; + + const index = currentMessages.indexOf(messageToUpdate); + const updatedMsg: ChatMessage = { + ...messageToUpdate, + status: MessageStatus.Ready, + text: text, + }; + + currentMessages[index] = updatedMsg; + const newData: Record = { + ...get(chatMessages), + }; + newData[currentConvoId] = currentMessages; + set(chatMessages, newData); + } +); + +export const currentStreamingMessageAtom = atom( + undefined +); diff --git a/web/app/_helpers/atoms/Conversation.atom.ts b/web/app/_helpers/atoms/Conversation.atom.ts new file mode 100644 index 000000000..7f1b312c9 --- /dev/null +++ b/web/app/_helpers/atoms/Conversation.atom.ts @@ -0,0 +1,104 @@ +import { atom } from "jotai"; +import { MainViewState, setMainViewStateAtom } from "./MainView.atom"; +import { Conversation, ConversationState } from "@/_models/Conversation"; + +/** + * Stores the current active conversation id. + */ +const activeConversationIdAtom = atom(undefined); + +export const getActiveConvoIdAtom = atom((get) => + get(activeConversationIdAtom) +); + +export const setActiveConvoIdAtom = atom( + null, + (_get, set, convoId: string | undefined) => { + if (convoId) { + console.debug(`Set active conversation id: ${convoId}`); + set(setMainViewStateAtom, MainViewState.Conversation); + } + + set(activeConversationIdAtom, convoId); + } +); + +/** + * Stores all conversation states for the current user + */ +export const conversationStatesAtom = atom>( + {} +); +export const currentConvoStateAtom = atom( + (get) => { + const activeConvoId = get(activeConversationIdAtom); + if (!activeConvoId) { + console.log("active convo id is undefined"); + return undefined; + } + + return get(conversationStatesAtom)[activeConvoId]; + } +); +export const addNewConversationStateAtom = atom( + null, + (get, set, conversationId: string, state: ConversationState) => { + const currentState = { ...get(conversationStatesAtom) }; + currentState[conversationId] = state; + set(conversationStatesAtom, currentState); + } +); +export const updateConversationWaitingForResponseAtom = atom( + null, + (get, set, conversationId: string, waitingForResponse: boolean) => { + const currentState = { ...get(conversationStatesAtom) }; + currentState[conversationId] = { + ...currentState[conversationId], + waitingForResponse, + }; + set(conversationStatesAtom, currentState); + } +); +export const updateConversationHasMoreAtom = atom( + null, + (get, set, conversationId: string, hasMore: boolean) => { + const currentState = { ...get(conversationStatesAtom) }; + currentState[conversationId] = { ...currentState[conversationId], hasMore }; + set(conversationStatesAtom, currentState); + } +); + +/** + * Stores all conversations for the current user + */ +export const userConversationsAtom = atom([]); +export const currentConversationAtom = atom((get) => + get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom)) +); +export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => { + const convo = get(userConversationsAtom).find((c) => c.id === convoId); + if (!convo) return; + const newConvo: Conversation = { + ...convo, + updated_at: new Date().toISOString(), + }; + const newConversations: Conversation[] = get(userConversationsAtom).map((c) => + c.id === convoId ? newConvo : c + ); + + set(userConversationsAtom, newConversations); +}); + +export const setConvoLastImageAtom = atom( + null, + (get, set, convoId: string, lastImageUrl: string) => { + const convo = get(userConversationsAtom).find((c) => c.id === convoId); + if (!convo) return; + const newConvo: Conversation = { ...convo }; + const newConversations: Conversation[] = get(userConversationsAtom).map( + (c) => (c.id === convoId ? newConvo : c) + ); + + set(userConversationsAtom, newConversations); + } +); diff --git a/web/app/_helpers/atoms/DownloadState.atom.ts b/web/app/_helpers/atoms/DownloadState.atom.ts new file mode 100644 index 000000000..d0491b454 --- /dev/null +++ b/web/app/_helpers/atoms/DownloadState.atom.ts @@ -0,0 +1,32 @@ +import { DownloadState } from "@/_models/DownloadState"; +import { atom } from "jotai"; + +// download states +export const modelDownloadStateAtom = atom>({}); + +export const setDownloadStateAtom = atom( + null, + (get, set, state: DownloadState) => { + const currentState = { ...get(modelDownloadStateAtom) }; + console.debug( + `current download state for ${state.fileName} is ${JSON.stringify(state)}` + ); + currentState[state.fileName] = state; + set(modelDownloadStateAtom, currentState); + } +); + +export const setDownloadStateSuccessAtom = atom( + null, + (get, set, fileName: string) => { + const currentState = { ...get(modelDownloadStateAtom) }; + const state = currentState[fileName]; + if (!state) { + console.error(`Cannot find download state for ${fileName}`); + return; + } + + delete currentState[fileName]; + set(modelDownloadStateAtom, currentState); + } +); diff --git a/web/app/_helpers/atoms/MainView.atom.ts b/web/app/_helpers/atoms/MainView.atom.ts new file mode 100644 index 000000000..45d267542 --- /dev/null +++ b/web/app/_helpers/atoms/MainView.atom.ts @@ -0,0 +1,54 @@ +import { atom } from "jotai"; +import { setActiveConvoIdAtom } from "./Conversation.atom"; +import { systemBarVisibilityAtom } from "./SystemBar.atom"; + +export enum MainViewState { + Welcome, + ExploreModel, + MyModel, + ResourceMonitor, + Setting, + Conversation, + + /** + * When user wants to create new conversation but haven't selected a model yet. + */ + ConversationEmptyModel, +} + +/** + * Stores the current main view state. Default is Welcome. + */ +const currentMainViewStateAtom = atom(MainViewState.Welcome); + +/** + * Getter for current main view state. + */ +export const getMainViewStateAtom = atom((get) => + get(currentMainViewStateAtom) +); + +/** + * Setter for current main view state. + */ +export const setMainViewStateAtom = atom( + null, + (get, set, state: MainViewState) => { + // return if the state is already set + if (get(getMainViewStateAtom) === state) return; + + if (state !== MainViewState.Conversation) { + // clear active conversation id if main view state is not Conversation + set(setActiveConvoIdAtom, undefined); + } + + const showSystemBar = + state !== MainViewState.Conversation && + state !== MainViewState.ConversationEmptyModel; + + // show system bar if state is not Conversation nor ConversationEmptyModel + set(systemBarVisibilityAtom, showSystemBar); + + set(currentMainViewStateAtom, state); + } +); diff --git a/web/app/_helpers/atoms/Modal.atom.ts b/web/app/_helpers/atoms/Modal.atom.ts new file mode 100644 index 000000000..54a8336f3 --- /dev/null +++ b/web/app/_helpers/atoms/Modal.atom.ts @@ -0,0 +1,8 @@ +import { atom } from "jotai"; + +export const showConfirmDeleteConversationModalAtom = atom(false); +export const showConfirmSignOutModalAtom = atom(false); +export const showConfirmDeleteModalAtom = atom(false); +export const showingAdvancedPromptAtom = atom(false); +export const showingProductDetailAtom = atom(false); +export const showingMobilePaneAtom = atom(false); diff --git a/web/app/_helpers/atoms/Model.atom.ts b/web/app/_helpers/atoms/Model.atom.ts new file mode 100644 index 000000000..053f03ac6 --- /dev/null +++ b/web/app/_helpers/atoms/Model.atom.ts @@ -0,0 +1,6 @@ +import { Product } from "@/_models/Product"; +import { atom } from "jotai"; + +export const currentProductAtom = atom(undefined); + +export const selectedModelAtom = atom(undefined); diff --git a/web/app/_helpers/atoms/SystemBar.atom.ts b/web/app/_helpers/atoms/SystemBar.atom.ts new file mode 100644 index 000000000..fc1b777d4 --- /dev/null +++ b/web/app/_helpers/atoms/SystemBar.atom.ts @@ -0,0 +1,7 @@ +import { atom } from "jotai"; + +export const systemBarVisibilityAtom = atom(true); + +export const getSystemBarVisibilityAtom = atom((get) => + get(systemBarVisibilityAtom) +); diff --git a/web/app/_hooks/useChatMessages.ts b/web/app/_hooks/useChatMessages.ts index 8400e6ea3..073a57ea4 100644 --- a/web/app/_hooks/useChatMessages.ts +++ b/web/app/_hooks/useChatMessages.ts @@ -1,14 +1,14 @@ -import { - addOldMessagesAtom, - conversationStatesAtom, - currentConversationAtom, - updateConversationHasMoreAtom, -} from "@/_helpers/JotaiWrapper"; import { ChatMessage, RawMessage, toChatMessage } from "@/_models/ChatMessage"; import { executeSerial } from "@/_services/pluginService"; import { useAtomValue, useSetAtom } from "jotai"; import { useEffect, useState } from "react"; import { DataService } from "../../shared/coreService"; +import { addOldMessagesAtom } from "@/_helpers/atoms/ChatMessage.atom"; +import { + currentConversationAtom, + conversationStatesAtom, + updateConversationHasMoreAtom, +} from "@/_helpers/atoms/Conversation.atom"; /** * Custom hooks to get chat messages for current(active) conversation diff --git a/web/app/_hooks/useCreateConversation.ts b/web/app/_hooks/useCreateConversation.ts index 9ff798ba5..fcff74956 100644 --- a/web/app/_hooks/useCreateConversation.ts +++ b/web/app/_hooks/useCreateConversation.ts @@ -1,23 +1,22 @@ -// import useGetCurrentUser from "./useGetCurrentUser"; import { useAtom, useSetAtom } from "jotai"; -import { - addNewConversationStateAtom, - currentProductAtom, - setActiveConvoIdAtom, - userConversationsAtom, -} from "@/_helpers/JotaiWrapper"; import { Conversation } from "@/_models/Conversation"; import { executeSerial } from "@/_services/pluginService"; -import { DataService, InfereceService } from "../../shared/coreService"; +import { DataService } from "../../shared/coreService"; import { Product } from "@/_models/Product"; +import { + userConversationsAtom, + setActiveConvoIdAtom, + addNewConversationStateAtom, +} from "@/_helpers/atoms/Conversation.atom"; +import useInitModel from "./useInitModel"; const useCreateConversation = () => { + const { initModel } = useInitModel(); const [userConversations, setUserConversations] = useAtom( userConversationsAtom ); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const addNewConvoState = useSetAtom(addNewConversationStateAtom); - const setActiveProduct = useSetAtom(currentProductAtom); const requestCreateConvo = async (model: Product) => { const conv: Conversation = { @@ -28,8 +27,7 @@ const useCreateConversation = () => { name: "Conversation", }; const id = await executeSerial(DataService.CREATE_CONVERSATION, conv); - await executeSerial(InfereceService.INIT_MODEL, model); - setActiveProduct(model); + await initModel(model); const mappedConvo: Conversation = { id, diff --git a/web/app/_hooks/useDeleteConversation.ts b/web/app/_hooks/useDeleteConversation.ts index 79fdad93c..df1cbd96c 100644 --- a/web/app/_hooks/useDeleteConversation.ts +++ b/web/app/_hooks/useDeleteConversation.ts @@ -1,15 +1,17 @@ -import { - currentPromptAtom, - deleteConversationMessage, - getActiveConvoIdAtom, - setActiveConvoIdAtom, - showingAdvancedPromptAtom, - showingProductDetailAtom, - userConversationsAtom, -} from "@/_helpers/JotaiWrapper"; +import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { execute } from "@/_services/pluginService"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { DataService } from "../../shared/coreService"; +import { deleteConversationMessage } from "@/_helpers/atoms/ChatMessage.atom"; +import { + userConversationsAtom, + getActiveConvoIdAtom, + setActiveConvoIdAtom, +} from "@/_helpers/atoms/Conversation.atom"; +import { + showingProductDetailAtom, + showingAdvancedPromptAtom, +} from "@/_helpers/atoms/Modal.atom"; export default function useDeleteConversation() { const [userConversations, setUserConversations] = useAtom( diff --git a/web/app/_hooks/useGetAvailableModels.ts b/web/app/_hooks/useGetAvailableModels.ts index 9b487c397..b10cc617b 100644 --- a/web/app/_hooks/useGetAvailableModels.ts +++ b/web/app/_hooks/useGetAvailableModels.ts @@ -3,8 +3,8 @@ import { executeSerial } from "@/_services/pluginService"; import { ModelManagementService } from "../../shared/coreService"; import { useEffect, useState } from "react"; import { getModelFiles } from "./useGetDownloadedModels"; -import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper"; import { useAtomValue } from "jotai"; +import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom"; export default function useGetAvailableModels() { const downloadState = useAtomValue(modelDownloadStateAtom); diff --git a/web/app/_hooks/useGetUserConversations.ts b/web/app/_hooks/useGetUserConversations.ts index f315e197a..e939395ba 100644 --- a/web/app/_hooks/useGetUserConversations.ts +++ b/web/app/_hooks/useGetUserConversations.ts @@ -1,11 +1,11 @@ import { Conversation, ConversationState } from "@/_models/Conversation"; import { useSetAtom } from "jotai"; +import { executeSerial } from "@/_services/pluginService"; +import { DataService } from "../../shared/coreService"; import { conversationStatesAtom, userConversationsAtom, -} from "@/_helpers/JotaiWrapper"; -import { executeSerial } from "@/_services/pluginService"; -import { DataService } from "../../shared/coreService"; +} from "@/_helpers/atoms/Conversation.atom"; const useGetUserConversations = () => { const setConversationStates = useSetAtom(conversationStatesAtom); diff --git a/web/app/_hooks/useInitModel.ts b/web/app/_hooks/useInitModel.ts new file mode 100644 index 000000000..18f8d7b87 --- /dev/null +++ b/web/app/_hooks/useInitModel.ts @@ -0,0 +1,25 @@ +import { Product } from "@/_models/Product"; +import { executeSerial } from "@/_services/pluginService"; +import { InfereceService } from "../../shared/coreService"; +import { useAtom } from "jotai"; +import { currentProductAtom } from "@/_helpers/atoms/Model.atom"; + +export default function useInitModel() { + const [activeModel, setActiveModel] = useAtom(currentProductAtom); + + const initModel = async (model: Product) => { + if (activeModel && activeModel.id === model.id) { + console.debug(`Model ${model.id} is already init. Ignore..`); + return; + } + try { + await executeSerial(InfereceService.INIT_MODEL, model); + console.debug(`Init model ${model.name} successfully!`); + setActiveModel(model); + } catch (err) { + console.error(`Init model ${model.name} failed: ${err}`); + } + }; + + return { initModel }; +} diff --git a/web/app/_hooks/useSendChatMessage.ts b/web/app/_hooks/useSendChatMessage.ts index f5bca3f96..ccc037d2f 100644 --- a/web/app/_hooks/useSendChatMessage.ts +++ b/web/app/_hooks/useSendChatMessage.ts @@ -1,14 +1,4 @@ -import { - addNewMessageAtom, - chatMessages, - currentConversationAtom, - currentPromptAtom, - currentStreamingMessageAtom, - getActiveConvoIdAtom, - showingTyping, - updateMessageAtom, -} from "@/_helpers/JotaiWrapper"; - +import { currentPromptAtom, showingTyping } from "@/_helpers/JotaiWrapper"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { selectAtom } from "jotai/utils"; import { DataService, InfereceService } from "../../shared/coreService"; @@ -19,6 +9,16 @@ import { } from "@/_models/ChatMessage"; import { executeSerial } from "@/_services/pluginService"; import { useCallback } from "react"; +import { + addNewMessageAtom, + updateMessageAtom, + chatMessages, + currentStreamingMessageAtom, +} from "@/_helpers/atoms/ChatMessage.atom"; +import { + currentConversationAtom, + getActiveConvoIdAtom, +} from "@/_helpers/atoms/Conversation.atom"; export default function useSendChatMessage() { const currentConvo = useAtomValue(currentConversationAtom); @@ -50,9 +50,9 @@ export default function useSendChatMessage() { const newChatMessage = await toChatMessage(newMessage); addNewMessage(newChatMessage); - + const messageHistory = chatMessagesHistory ?? []; const recentMessages = [ - ...chatMessagesHistory.sort((a, b) => parseInt(a.id) - parseInt(b.id)), + ...messageHistory.sort((a, b) => parseInt(a.id) - parseInt(b.id)), newChatMessage, ] .slice(-10) diff --git a/web/app/_hooks/useStartStopModel.ts b/web/app/_hooks/useStartStopModel.ts index e7e3cb40a..2201e8fce 100644 --- a/web/app/_hooks/useStartStopModel.ts +++ b/web/app/_hooks/useStartStopModel.ts @@ -1,20 +1,16 @@ -import { currentProductAtom } from "@/_helpers/JotaiWrapper"; import { executeSerial } from "@/_services/pluginService"; -import { DataService, InfereceService } from "../../shared/coreService"; -import { useSetAtom } from "jotai"; +import { DataService } from "../../shared/coreService"; +import useInitModel from "./useInitModel"; export default function useStartStopModel() { - const setActiveModel = useSetAtom(currentProductAtom); + const { initModel } = useInitModel(); const startModel = async (modelId: string) => { const model = await executeSerial(DataService.GET_MODEL_BY_ID, modelId); if (!model) { alert(`Model ${modelId} not found! Please re-download the model first.`); } else { - setActiveModel(model); - executeSerial(InfereceService.INIT_MODEL, model) - .then(() => console.info(`Init model success`)) - .catch((err) => console.log(`Init model error ${err}`)); + await initModel(model); } };