From 26f732d5411df9cea657647937fbb0d417c14a79 Mon Sep 17 00:00:00 2001 From: NamH Date: Mon, 2 Oct 2023 10:10:32 -0700 Subject: [PATCH] Add model screen and refactoring (#242) * Add model screen and refactoring Signed-off-by: James --- electron/core/plugins/data-plugin/module.ts | 27 ++++- .../_components/ActiveModelTable/index.tsx | 19 ++++ web/app/_components/ChatContainer/index.tsx | 6 +- .../ConfirmDeleteModelModal/index.tsx | 8 +- .../_components/DownloadedModelCard/index.tsx | 39 +++---- .../DownloadedModelTable/index.tsx | 20 ++++ .../ExploreModelContainer/index.tsx | 55 ++++++++++ .../_components/ExploreModelItem/index.tsx | 101 +++++++++++++++++ .../ExploreModelItemHeader/index.tsx | 44 ++++++++ web/app/_components/HistoryItem/index.tsx | 18 ++-- web/app/_components/LeftContainer/index.tsx | 4 + .../_components/ModelActionButton/index.tsx | 46 ++++++++ web/app/_components/ModelActionMenu/index.tsx | 35 ++++++ .../ModelDownloadingButton/index.tsx | 14 +-- .../_components/ModelListContainer/index.tsx | 88 +++------------ web/app/_components/ModelRow/index.tsx | 78 ++++++++++++++ .../ModelStatusComponent/index.tsx | 46 ++++++++ web/app/_components/ModelTable/index.tsx | 34 ++++++ .../_components/ModelTableHeader/index.tsx | 16 +++ .../_components/ModelVersionItem/index.tsx | 25 +++++ .../_components/ModelVersionList/index.tsx | 38 +++++++ web/app/_components/MonitorBar/index.tsx | 26 +---- .../_components/MyModelContainer/index.tsx | 13 +++ web/app/_components/RightContainer/index.tsx | 2 - web/app/_components/SearchBar/index.tsx | 12 ++- web/app/_components/SecondaryButton/index.tsx | 2 +- web/app/_components/SidebarMenu/index.tsx | 102 +++++++----------- web/app/_components/SidebarMenuItem/index.tsx | 41 +++++++ web/app/_components/SimpleCheckbox/index.tsx | 22 ++++ web/app/_components/SimpleTag/index.tsx | 89 +++++++++++++++ web/app/_helpers/JotaiWrapper.tsx | 4 +- web/app/_hooks/useCreateConversation.ts | 3 - web/app/_hooks/useDeleteModel.ts | 12 +++ web/app/_hooks/useDownloadModel.ts | 17 +++ web/app/_hooks/useGetAvailableModels.ts | 54 ++++++++++ web/app/_hooks/useGetDownloadedModels.ts | 30 ++++++ web/app/_hooks/useStartStopModel.ts | 24 +++++ web/app/_models/Product.ts | 6 ++ web/app/_utils/converter.ts | 15 +++ 39 files changed, 1008 insertions(+), 227 deletions(-) create mode 100644 web/app/_components/ActiveModelTable/index.tsx create mode 100644 web/app/_components/DownloadedModelTable/index.tsx create mode 100644 web/app/_components/ExploreModelContainer/index.tsx create mode 100644 web/app/_components/ExploreModelItem/index.tsx create mode 100644 web/app/_components/ExploreModelItemHeader/index.tsx create mode 100644 web/app/_components/ModelActionButton/index.tsx create mode 100644 web/app/_components/ModelActionMenu/index.tsx create mode 100644 web/app/_components/ModelRow/index.tsx create mode 100644 web/app/_components/ModelStatusComponent/index.tsx create mode 100644 web/app/_components/ModelTable/index.tsx create mode 100644 web/app/_components/ModelTableHeader/index.tsx create mode 100644 web/app/_components/ModelVersionItem/index.tsx create mode 100644 web/app/_components/ModelVersionList/index.tsx create mode 100644 web/app/_components/MyModelContainer/index.tsx create mode 100644 web/app/_components/SidebarMenuItem/index.tsx create mode 100644 web/app/_components/SimpleCheckbox/index.tsx create mode 100644 web/app/_components/SimpleTag/index.tsx create mode 100644 web/app/_hooks/useDeleteModel.ts create mode 100644 web/app/_hooks/useDownloadModel.ts create mode 100644 web/app/_hooks/useGetAvailableModels.ts create mode 100644 web/app/_hooks/useGetDownloadedModels.ts create mode 100644 web/app/_hooks/useStartStopModel.ts create mode 100644 web/app/_utils/converter.ts diff --git a/electron/core/plugins/data-plugin/module.ts b/electron/core/plugins/data-plugin/module.ts index ce0e64770..00faa5f63 100644 --- a/electron/core/plugins/data-plugin/module.ts +++ b/electron/core/plugins/data-plugin/module.ts @@ -168,7 +168,7 @@ function getFinishedDownloadModels() { const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; db.all(query, (err: Error, row: any) => { - res(row); + res(row.map((item: any) => parseToProduct(item))); }); db.close(); }); @@ -373,6 +373,31 @@ function getConversationMessages(conversation_id: any) { }); } +function parseToProduct(row: any) { + const product = { + id: row.id, + slug: row.slug, + name: row.name, + description: row.description, + avatarUrl: row.avatar_url, + longDescription: row.long_description, + technicalDescription: row.technical_description, + author: row.author, + version: row.version, + modelUrl: row.model_url, + nsfw: row.nsfw, + greeting: row.greeting, + type: row.type, + inputs: row.inputs, + outputs: row.outputs, + createdAt: new Date(row.created_at), + updatedAt: new Date(row.updated_at), + fileName: row.file_name, + downloadUrl: row.download_url, + }; + return product; +} + module.exports = { init, getConversations, diff --git a/web/app/_components/ActiveModelTable/index.tsx b/web/app/_components/ActiveModelTable/index.tsx new file mode 100644 index 000000000..88e10a66a --- /dev/null +++ b/web/app/_components/ActiveModelTable/index.tsx @@ -0,0 +1,19 @@ +import { currentProductAtom } from "@/_helpers/JotaiWrapper"; +import { useAtomValue } from "jotai"; +import React, { Fragment } from "react"; +import ModelTable from "../ModelTable"; + +const ActiveModelTable: React.FC = () => { + const activeModel = useAtomValue(currentProductAtom); + + if (!activeModel) return null; + + return ( + +

Active Model(s)

+ +
+ ); +}; + +export default ActiveModelTable; diff --git a/web/app/_components/ChatContainer/index.tsx b/web/app/_components/ChatContainer/index.tsx index 46e740f90..db37ed9c0 100644 --- a/web/app/_components/ChatContainer/index.tsx +++ b/web/app/_components/ChatContainer/index.tsx @@ -3,9 +3,10 @@ import { useAtomValue } from "jotai"; import { MainViewState, getMainViewStateAtom } from "@/_helpers/JotaiWrapper"; import { ReactNode } from "react"; -import ModelManagement from "../ModelManagement"; import Welcome from "../WelcomeContainer"; import { Preferences } from "../Preferences"; +import MyModelContainer from "../MyModelContainer"; +import ExploreModelContainer from "../ExploreModelContainer"; type Props = { children: ReactNode; @@ -16,11 +17,12 @@ export default function ChatContainer({ children }: Props) { switch (viewState) { case MainViewState.ExploreModel: - return ; + return ; case MainViewState.Setting: return ; case MainViewState.ResourceMonitor: case MainViewState.MyModel: + return ; case MainViewState.Welcome: return ; default: diff --git a/web/app/_components/ConfirmDeleteModelModal/index.tsx b/web/app/_components/ConfirmDeleteModelModal/index.tsx index 76c862b0f..b635de23d 100644 --- a/web/app/_components/ConfirmDeleteModelModal/index.tsx +++ b/web/app/_components/ConfirmDeleteModelModal/index.tsx @@ -3,15 +3,11 @@ import { Dialog, Transition } from "@headlessui/react"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; import { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper"; import { useAtom } from "jotai"; -import useSignOut from "@/_hooks/useSignOut"; const ConfirmDeleteModelModal: React.FC = () => { const [show, setShow] = useAtom(showConfirmDeleteModalAtom); - const { signOut } = useSignOut(); - const onLogOutClick = () => { - signOut().then(() => setShow(false)); - }; + const onConfirmDelete = () => {}; return ( @@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => { diff --git a/web/app/_components/DownloadedModelCard/index.tsx b/web/app/_components/DownloadedModelCard/index.tsx index 5aa7a7625..94d4526d3 100644 --- a/web/app/_components/DownloadedModelCard/index.tsx +++ b/web/app/_components/DownloadedModelCard/index.tsx @@ -1,8 +1,5 @@ import { Product } from "@/_models/Product"; import DownloadModelContent from "../DownloadModelContent"; -import ViewModelDetailButton from "../ViewModelDetailButton"; -import { executeSerial } from "@/_services/pluginService"; -import { InfereceService } from "../../../shared/coreService"; type Props = { product: Product; @@ -17,28 +14,22 @@ const DownloadedModelCard: React.FC = ({ isRecommend, required, onDeleteClick, -}) => { - - const handleViewDetails = () => {}; - - return ( -
-
- -
- -
+}) => ( +
+
+ +
+
- {/* */}
- ); -}; +
+); export default DownloadedModelCard; diff --git a/web/app/_components/DownloadedModelTable/index.tsx b/web/app/_components/DownloadedModelTable/index.tsx new file mode 100644 index 000000000..72ab2350a --- /dev/null +++ b/web/app/_components/DownloadedModelTable/index.tsx @@ -0,0 +1,20 @@ +import React, { Fragment } from "react"; +import SearchBar from "../SearchBar"; +import ModelTable from "../ModelTable"; +import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels"; + +const DownloadedModelTable: React.FC = () => { + const { downloadedModels } = useGetDownloadedModels(); + + return ( + +

Downloaded Models

+
+ +
+ +
+ ); +}; + +export default DownloadedModelTable; diff --git a/web/app/_components/ExploreModelContainer/index.tsx b/web/app/_components/ExploreModelContainer/index.tsx new file mode 100644 index 000000000..d07745ffa --- /dev/null +++ b/web/app/_components/ExploreModelContainer/index.tsx @@ -0,0 +1,55 @@ +import useGetAvailableModels from "@/_hooks/useGetAvailableModels"; +import ExploreModelItem from "../ExploreModelItem"; +import HeaderTitle from "../HeaderTitle"; +import SearchBar from "../SearchBar"; +import SimpleCheckbox from "../SimpleCheckbox"; +import SimpleTag, { TagType } from "../SimpleTag"; + +const tags = [ + "Roleplay", + "Llama", + "Story", + "Casual", + "Professional", + "CodeLlama", + "Coding", +]; +const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"]; + +const ExploreModelContainer: React.FC = () => { + const { allAvailableModels } = useGetAvailableModels(); + + return ( +
+ + +
+
+

Tags

+ +
+ {tags.map((item) => ( + + ))} +
+
+
+ {checkboxs.map((item) => ( + + ))} +
+
+
+

Results

+
+ {allAvailableModels.map((item) => ( + + ))} +
+
+
+
+ ); +}; + +export default ExploreModelContainer; diff --git a/web/app/_components/ExploreModelItem/index.tsx b/web/app/_components/ExploreModelItem/index.tsx new file mode 100644 index 000000000..3a1939ec0 --- /dev/null +++ b/web/app/_components/ExploreModelItem/index.tsx @@ -0,0 +1,101 @@ +"use client"; + +import ExploreModelItemHeader from "../ExploreModelItemHeader"; +import ModelVersionList from "../ModelVersionList"; +import { useMemo, useState } from "react"; +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"; + +type Props = { + model: Product; +}; + +const ExploreModelItem: React.FC = ({ model }) => { + const downloadAtom = useMemo( + () => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]), + [model.fileName ?? ""] + ); + const downloadState = useAtomValue(downloadAtom); + const { downloadModel } = useDownloadModel(); + const [show, setShow] = useState(false); + + return ( +
+ downloadModel(model)} + /> +
+
+
+
+
+ Model Format +
+
+ GGUF +
+
+
+
+ Hardware Compatibility +
+
+ +
+
+
+
+
+
+ Release Date +
+
+ {displayDate(model.releaseDate)} +
+
+
+
+ Expected Performance +
+ +
+
+
+
+ About + + {model.longDescription} + +
+
+ Tags +
+
+ {show && } + +
+ ); +}; + +export default ExploreModelItem; diff --git a/web/app/_components/ExploreModelItemHeader/index.tsx b/web/app/_components/ExploreModelItemHeader/index.tsx new file mode 100644 index 000000000..83a73d97d --- /dev/null +++ b/web/app/_components/ExploreModelItemHeader/index.tsx @@ -0,0 +1,44 @@ +import SimpleTag, { TagType } from "../SimpleTag"; +import PrimaryButton from "../PrimaryButton"; +import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter"; +import { DownloadState } from "@/_models/DownloadState"; +import SecondaryButton from "../SecondaryButton"; + +type Props = { + name: string; + total: number; + status: TagType; + downloadState?: DownloadState; + onDownloadClick?: () => void; +}; + +const ExploreModelItemHeader: React.FC = ({ + name, + status, + total, + downloadState, + onDownloadClick, +}) => ( +
+
+ {name} + +
+ {downloadState != null ? ( + {}} + /> + ) : ( + onDownloadClick?.()} + /> + )} +
+); + +export default ExploreModelItemHeader; diff --git a/web/app/_components/HistoryItem/index.tsx b/web/app/_components/HistoryItem/index.tsx index 2402dcaa0..2a708c113 100644 --- a/web/app/_components/HistoryItem/index.tsx +++ b/web/app/_components/HistoryItem/index.tsx @@ -2,7 +2,6 @@ import React from "react"; import JanImage from "../JanImage"; import { MainViewState, - activeModel, conversationStatesAtom, currentProductAtom, getActiveConvoIdAtom, @@ -13,10 +12,7 @@ import { useAtomValue, useSetAtom } from "jotai"; import Image from "next/image"; import { Conversation } from "@/_models/Conversation"; import { DataService, InfereceService } from "../../../shared/coreService"; -import { - execute, - executeSerial, -} from "../../../../electron/core/plugin-manager/execution/extension-manager"; +import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager"; type Props = { conversation: Conversation; @@ -36,23 +32,23 @@ const HistoryItem: React.FC = ({ const activeConvoId = useAtomValue(getActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const isSelected = activeConvoId === conversation.id; - const setActiveModel = useSetAtom(activeModel); + const setActiveProduct = useSetAtom(currentProductAtom); + const onClick = async () => { - const convoModel = await executeSerial( + const model = await executeSerial( DataService.GET_MODEL_BY_ID, conversation.model_id ); - if (!convoModel) { + if (!model) { alert( `Model ${conversation.model_id} not found! Please re-download the model first.` ); } else { - setActiveProduct(convoModel) - executeSerial(InfereceService.INIT_MODEL, convoModel) + setActiveProduct(model); + executeSerial(InfereceService.INIT_MODEL, model) .then(() => console.info(`Init model success`)) .catch((err) => console.log(`Init model error ${err}`)); - setActiveModel(convoModel.name); } if (activeConvoId !== conversation.id) { setMainViewState(MainViewState.Conversation); diff --git a/web/app/_components/LeftContainer/index.tsx b/web/app/_components/LeftContainer/index.tsx index be48a75be..a7b8a856c 100644 --- a/web/app/_components/LeftContainer/index.tsx +++ b/web/app/_components/LeftContainer/index.tsx @@ -3,10 +3,14 @@ import SidebarFooter from "../SidebarFooter"; import SidebarHeader from "../SidebarHeader"; import SidebarMenu from "../SidebarMenu"; import HistoryList from "../HistoryList"; +import SecondaryButton from "../SecondaryButton"; const LeftContainer: React.FC = () => (
+
+ {}} /> +
diff --git a/web/app/_components/ModelActionButton/index.tsx b/web/app/_components/ModelActionButton/index.tsx new file mode 100644 index 000000000..394b3ef2a --- /dev/null +++ b/web/app/_components/ModelActionButton/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import PrimaryButton from "../PrimaryButton"; + +export enum ModelActionType { + Start = "Start", + Stop = "Stop", +} + +type ModelActionStyle = { + title: string; + backgroundColor: string; + textColor: string; +}; + +const modelActionMapper: Record = { + [ModelActionType.Start]: { + title: "Start", + backgroundColor: "bg-blue-500 hover:bg-blue-600", + textColor: "text-white", + }, + [ModelActionType.Stop]: { + title: "Stop", + backgroundColor: "bg-red-500 hover:bg-red-600", + textColor: "text-white", + }, +}; + +type Props = { + type: ModelActionType; + onActionClick: (type: ModelActionType) => void; +}; + +const ModelActionButton: React.FC = ({ type, onActionClick }) => { + const styles = modelActionMapper[type]; + const onClick = () => { + onActionClick(type); + }; + + return ( + + + + ); +}; + +export default ModelActionButton; diff --git a/web/app/_components/ModelActionMenu/index.tsx b/web/app/_components/ModelActionMenu/index.tsx new file mode 100644 index 000000000..8fc3df274 --- /dev/null +++ b/web/app/_components/ModelActionMenu/index.tsx @@ -0,0 +1,35 @@ +import { Menu, Transition } from "@headlessui/react"; +import { EllipsisVerticalIcon } from "@heroicons/react/20/solid"; +import { Fragment } from "react"; + +type Props = { + onDeleteClick: () => void; +}; + +const ModelActionMenu: React.FC = ({ onDeleteClick }) => { + return ( + + + Open options + + + + + + + + + + ); +}; + +export default ModelActionMenu; diff --git a/web/app/_components/ModelDownloadingButton/index.tsx b/web/app/_components/ModelDownloadingButton/index.tsx index ff30e6642..d01b58022 100644 --- a/web/app/_components/ModelDownloadingButton/index.tsx +++ b/web/app/_components/ModelDownloadingButton/index.tsx @@ -1,3 +1,5 @@ +import { toGigabytes } from "@/_utils/converter"; + type Props = { total: number; value: number; @@ -18,16 +20,4 @@ const ModelDownloadingButton: React.FC = ({ total, value }) => { ); }; -const toGigabytes = (input: number) => { - if (input > 1024 ** 3) { - return (input / 1000 ** 3).toFixed(2) + "GB"; - } else if (input > 1024 ** 2) { - return (input / 1000 ** 2).toFixed(2) + "MB"; - } else if (input > 1024) { - return (input / 1000).toFixed(2) + "KB"; - } else { - return input + "B"; - } -}; - export default ModelDownloadingButton; diff --git a/web/app/_components/ModelListContainer/index.tsx b/web/app/_components/ModelListContainer/index.tsx index 27b3f0a0f..52de3a2cf 100644 --- a/web/app/_components/ModelListContainer/index.tsx +++ b/web/app/_components/ModelListContainer/index.tsx @@ -1,92 +1,36 @@ "use client"; -import { useEffect, useState } from "react"; -import { execute, executeSerial } from "@/_services/pluginService"; -import { - DataService, - ModelManagementService, -} from "../../../shared/coreService"; import { useAtomValue } from "jotai"; -import { - modelDownloadStateAtom, - searchingModelText, -} from "@/_helpers/JotaiWrapper"; +import { searchingModelText } from "@/_helpers/JotaiWrapper"; import { Product } from "@/_models/Product"; import DownloadedModelCard from "../DownloadedModelCard"; import AvailableModelCard from "../AvailableModelCard"; +import useDeleteModel from "@/_hooks/useDeleteModel"; +import useGetAvailableModels from "@/_hooks/useGetAvailableModels"; +import useDownloadModel from "@/_hooks/useDownloadModel"; const ModelListContainer: React.FC = () => { - const [downloadedModels, setDownloadedModels] = useState([]); - const [availableModels, setAvailableModels] = useState([]); - const downloadState = useAtomValue(modelDownloadStateAtom); const searchText = useAtomValue(searchingModelText); + const { deleteModel } = useDeleteModel(); + const { downloadModel } = useDownloadModel(); - useEffect(() => { - const getDownloadedModels = async () => { - const avails = await executeSerial( - ModelManagementService.GET_AVAILABLE_MODELS - ); - - const downloaded: Product[] = await executeSerial( - ModelManagementService.GET_DOWNLOADED_MODELS - ); - - const downloadedSucessfullyModels: Product[] = []; - const availableOrDownloadingModels: Product[] = avails; - - downloaded.forEach((item) => { - if (item.fileName && downloadState[item.fileName] == null) { - downloadedSucessfullyModels.push(item); - } else { - availableOrDownloadingModels.push(item); - } - }); - - setAvailableModels(availableOrDownloadingModels); - setDownloadedModels(downloadedSucessfullyModels); - }; - getDownloadedModels(); - }, [downloadState]); + const { + availableModels, + downloadedModels, + getAvailableModelExceptDownloaded, + } = useGetAvailableModels(); const onDeleteClick = async (product: Product) => { - execute(DataService.DELETE_DOWNLOAD_MODEL, product.id); - await executeSerial(ModelManagementService.DELETE_MODEL, product.fileName); - const getDownloadedModels = async () => { - const avails = await executeSerial( - ModelManagementService.GET_AVAILABLE_MODELS - ); - - const downloaded: Product[] = await executeSerial( - ModelManagementService.GET_DOWNLOADED_MODELS - ); - - const downloadedSucessfullyModels: Product[] = []; - const availableOrDownloadingModels: Product[] = avails; - - downloaded.forEach((item) => { - if (item.fileName && downloadState[item.fileName] == null) { - downloadedSucessfullyModels.push(item); - } else { - availableOrDownloadingModels.push(item); - } - }); - - setAvailableModels(availableOrDownloadingModels); - setDownloadedModels(downloadedSucessfullyModels); - }; - getDownloadedModels(); + await deleteModel(product); + await getAvailableModelExceptDownloaded(); }; - const onDownloadClick = async (product: Product) => { - await executeSerial(DataService.STORE_MODEL, product); - await executeSerial(ModelManagementService.DOWNLOAD_MODEL, { - downloadUrl: product.downloadUrl, - fileName: product.fileName, - }); + const onDownloadClick = async (model: Product) => { + await downloadModel(model); }; return ( -
+
{downloadedModels diff --git a/web/app/_components/ModelRow/index.tsx b/web/app/_components/ModelRow/index.tsx new file mode 100644 index 000000000..1ce2effce --- /dev/null +++ b/web/app/_components/ModelRow/index.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import { Product } from "@/_models/Product"; +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"; + +type Props = { + model: Product; +}; + +const ModelRow: React.FC<Props> = ({ model }) => { + const { startModel } = useStartStopModel(); + const activeModel = useAtomValue(currentProductAtom); + const { deleteModel } = useDeleteModel(); + + let status = ModelStatus.Installed; + if (activeModel && activeModel.id === model.id) { + status = ModelStatus.Active; + } + + let actionButtonType = ModelActionType.Start; + if (activeModel && activeModel.id === model.id) { + actionButtonType = ModelActionType.Stop; + } + + const onModelActionClick = (action: ModelActionType) => { + if (action === ModelActionType.Start) { + startModel(model.id); + } + }; + + const onDeleteClick = () => { + deleteModel(model); + }; + + return ( + <tr + className="border-b border-gray-200 last:border-b-0 last:rounded-lg" + key={model.id} + > + <td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900"> + {model.name} + <span className="text-gray-500 font-normal">{model.version}</span> + </td> + <td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> + <div className="flex flex-col justify-start"> + <span>{model.format}</span> + {model.accelerated && ( + <span className="flex items-center text-gray-500 text-sm font-normal gap-0.5"> + <Image src={"/icons/flash.svg"} width={20} height={20} alt="" /> + GPU Accelerated + </span> + )} + </div> + </td> + <td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> + {model.totalSize} + </td> + <td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> + <ModelStatusComponent status={status} /> + </td> + <ModelActionButton + type={actionButtonType} + onActionClick={onModelActionClick} + /> + <td className="relative whitespace-nowrap px-6 py-4 w-fit text-right text-sm font-medium"> + <ModelActionMenu onDeleteClick={onDeleteClick} /> + </td> + </tr> + ); +}; + +export default ModelRow; diff --git a/web/app/_components/ModelStatusComponent/index.tsx b/web/app/_components/ModelStatusComponent/index.tsx new file mode 100644 index 000000000..e13ce2020 --- /dev/null +++ b/web/app/_components/ModelStatusComponent/index.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +export type ModelStatusType = { + title: string; + textColor: string; + backgroundColor: string; +}; + +export enum ModelStatus { + Installed, + Active, + RunningInNitro, +} + +export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = { + [ModelStatus.Installed]: { + title: "Installed", + textColor: "text-black", + backgroundColor: "bg-gray-100", + }, + [ModelStatus.Active]: { + title: "Active", + textColor: "text-black", + backgroundColor: "bg-green-100", + }, + [ModelStatus.RunningInNitro]: { + title: "Running in Nitro", + textColor: "text-black", + backgroundColor: "bg-green-100", + }, +}; + +type Props = { + status: ModelStatus; +}; + +export const ModelStatusComponent: React.FC<Props> = ({ status }) => { + const statusType = ModelStatusMapper[status]; + return ( + <div + className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`} + > + {statusType.title} + </div> + ); +}; diff --git a/web/app/_components/ModelTable/index.tsx b/web/app/_components/ModelTable/index.tsx new file mode 100644 index 000000000..688ef6c34 --- /dev/null +++ b/web/app/_components/ModelTable/index.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Product } from "@/_models/Product"; +import ModelRow from "../ModelRow"; +import ModelTableHeader from "../ModelTableHeader"; + +type Props = { + models: Product[]; +}; + +const tableHeaders = ["MODEL", "FORMAT", "SIZE", "STATUS", "ACTIONS"]; + +const ModelTable: React.FC<Props> = ({ models }) => ( + <div className="flow-root inline-block border rounded-lg border-gray-200 min-w-full align-middle shadow-lg"> + <table className="min-w-full"> + <thead className="bg-gray-50 border-b border-gray-200"> + <tr className="rounded-t-lg"> + {tableHeaders.map((item) => ( + <ModelTableHeader key={item} title={item} /> + ))} + <th scope="col" className="relative px-6 py-3 w-fit"> + <span className="sr-only">Edit</span> + </th> + </tr> + </thead> + <tbody> + {models.map((model) => ( + <ModelRow key={model.id} model={model} /> + ))} + </tbody> + </table> + </div> +); + +export default React.memo(ModelTable); diff --git a/web/app/_components/ModelTableHeader/index.tsx b/web/app/_components/ModelTableHeader/index.tsx new file mode 100644 index 000000000..b335888ee --- /dev/null +++ b/web/app/_components/ModelTableHeader/index.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +type Props = { + title: string; +}; + +const ModelTableHeader: React.FC<Props> = ({ title }) => ( + <th + scope="col" + className="px-6 py-3 text-left first:rounded-tl-lg last:rounded-tr-lg text-xs font-medium uppercase tracking-wide text-gray-500" + > + {title} + </th> +); + +export default React.memo(ModelTableHeader); diff --git a/web/app/_components/ModelVersionItem/index.tsx b/web/app/_components/ModelVersionItem/index.tsx new file mode 100644 index 000000000..355128bba --- /dev/null +++ b/web/app/_components/ModelVersionItem/index.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { toGigabytes } from "@/_utils/converter"; +import Image from "next/image"; + +type Props = { + title: string; + totalSizeInByte: number; +}; + +const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => ( + <div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0"> + <div className="flex items-center gap-4"> + <Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" /> + <span className="font-sm text-gray-900">{title}</span> + </div> + <div className="flex items-center gap-4"> + <div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded"> + {toGigabytes(totalSizeInByte)} + </div> + <button className="text-indigo-600 text-sm font-medium">Download</button> + </div> + </div> +); + +export default ModelVersionItem; diff --git a/web/app/_components/ModelVersionList/index.tsx b/web/app/_components/ModelVersionList/index.tsx new file mode 100644 index 000000000..f70d38a76 --- /dev/null +++ b/web/app/_components/ModelVersionList/index.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import ModelVersionItem from "../ModelVersionItem"; + +const data = [ + { + name: "Q4_K_M.gguf", + total: 5600, + }, + { + name: "Q4_K_M.gguf", + total: 5600, + }, + { + name: "Q4_K_M.gguf", + total: 5600, + }, +]; + +const ModelVersionList: React.FC = () => { + return ( + <div className="px-4 py-5 border-t border-gray-200"> + <div className="text-sm font-medium text-gray-500"> + Available Versions + </div> + <div className="border border-gray-200 rounded-lg overflow-hidden"> + {data.map((item, index) => ( + <ModelVersionItem + key={index} + title={item.name} + totalSizeInByte={item.total} + /> + ))} + </div> + </div> + ); +}; + +export default ModelVersionList; diff --git a/web/app/_components/MonitorBar/index.tsx b/web/app/_components/MonitorBar/index.tsx index 5f98f6add..39b884849 100644 --- a/web/app/_components/MonitorBar/index.tsx +++ b/web/app/_components/MonitorBar/index.tsx @@ -2,8 +2,8 @@ import ProgressBar from "../ProgressBar"; import SystemItem from "../SystemItem"; import { useAtomValue } from "jotai"; import { - activeModel, appDownloadProgress, + currentProductAtom, getSystemBarVisibilityAtom, } from "@/_helpers/JotaiWrapper"; import { useEffect, useState } from "react"; @@ -13,30 +13,12 @@ import { SystemMonitoringService } from "../../../shared/coreService"; const MonitorBar: React.FC = () => { const show = useAtomValue(getSystemBarVisibilityAtom); const progress = useAtomValue(appDownloadProgress); - const modelName = useAtomValue(activeModel); + const activeModel = useAtomValue(currentProductAtom); const [ram, setRam] = useState<number>(0); const [gpu, setGPU] = useState<number>(0); const [cpu, setCPU] = useState<number>(0); const [version, setVersion] = useState<string>(""); - const data = [ - { - name: "CPU", - total: 1400, - used: 750, - }, - { - name: "Ram", - total: 16000, - used: 4500, - }, - { - name: "VRAM", - total: 1400, - used: 1300, - }, - ]; - useEffect(() => { const getSystemResources = async () => { const resourceInfor = await executeSerial( @@ -77,8 +59,8 @@ const MonitorBar: React.FC = () => { <SystemItem name="CPU" value={`${cpu}%`} /> <SystemItem name="Mem" value={`${ram}%`} /> - {modelName && modelName.length > 0 && ( - <SystemItem name="Active Models" value={"1"} /> + {activeModel && ( + <SystemItem name={`Active model: ${activeModel.name}`} value={"1"} /> )} <span className="text-gray-900 text-sm">v{version}</span> </div> diff --git a/web/app/_components/MyModelContainer/index.tsx b/web/app/_components/MyModelContainer/index.tsx new file mode 100644 index 000000000..905df627c --- /dev/null +++ b/web/app/_components/MyModelContainer/index.tsx @@ -0,0 +1,13 @@ +import HeaderTitle from "../HeaderTitle"; +import DownloadedModelTable from "../DownloadedModelTable"; +import ActiveModelTable from "../ActiveModelTable"; + +const MyModelContainer: React.FC = () => ( + <div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]"> + <HeaderTitle title="My Models" /> + <ActiveModelTable /> + <DownloadedModelTable /> + </div> +); + +export default MyModelContainer; diff --git a/web/app/_components/RightContainer/index.tsx b/web/app/_components/RightContainer/index.tsx index 61aa47b6b..adf5d4b62 100644 --- a/web/app/_components/RightContainer/index.tsx +++ b/web/app/_components/RightContainer/index.tsx @@ -1,11 +1,9 @@ import ChatContainer from "../ChatContainer"; -import Header from "../Header"; import MainChat from "../MainChat"; import MonitorBar from "../MonitorBar"; const RightContainer = () => ( <div className="flex flex-col flex-1 h-screen"> - <Header /> <ChatContainer> <MainChat /> </ChatContainer> diff --git a/web/app/_components/SearchBar/index.tsx b/web/app/_components/SearchBar/index.tsx index 6932ec1ca..a6354549e 100644 --- a/web/app/_components/SearchBar/index.tsx +++ b/web/app/_components/SearchBar/index.tsx @@ -2,11 +2,15 @@ import { searchAtom } from "@/_helpers/JotaiWrapper"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { useSetAtom } from "jotai"; -const SearchBar: React.FC = () => { - const setText = useSetAtom(searchAtom); +type Props = { + placeholder?: string; +}; +const SearchBar: React.FC<Props> = ({ placeholder }) => { + const setText = useSetAtom(searchAtom); + let placeholderText = placeholder ? placeholder : "Search (⌘K)"; return ( - <div className="relative mx-3 mt-3 flex items-center"> + <div className="relative mt-3 flex items-center"> <div className="absolute top-0 left-2 h-full flex items-center"> <MagnifyingGlassIcon width={16} @@ -19,7 +23,7 @@ const SearchBar: React.FC = () => { type="text" name="search" id="search" - placeholder="Search (⌘K)" + placeholder={placeholderText} onChange={(e) => setText(e.target.value)} className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" /> diff --git a/web/app/_components/SecondaryButton/index.tsx b/web/app/_components/SecondaryButton/index.tsx index 8de14302d..dc1cd80e9 100644 --- a/web/app/_components/SecondaryButton/index.tsx +++ b/web/app/_components/SecondaryButton/index.tsx @@ -9,7 +9,7 @@ const SecondaryButton: React.FC<Props> = ({ title, onClick, disabled }) => ( disabled={disabled} type="button" onClick={onClick} - className="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" + className="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" > {title} </button> diff --git a/web/app/_components/SidebarMenu/index.tsx b/web/app/_components/SidebarMenu/index.tsx index 8ea9f9de7..f465a7271 100644 --- a/web/app/_components/SidebarMenu/index.tsx +++ b/web/app/_components/SidebarMenu/index.tsx @@ -1,69 +1,41 @@ -import { - MainViewState, - getMainViewStateAtom, - setMainViewStateAtom, -} from "@/_helpers/JotaiWrapper"; -import classNames from "classnames"; -import { useAtomValue, useSetAtom } from "jotai"; -import Image from "next/image"; +import React from "react"; +import { MainViewState } from "@/_helpers/JotaiWrapper"; +import SidebarMenuItem from "../SidebarMenuItem"; -const SidebarMenu: React.FC = () => { - const currentState = useAtomValue(getMainViewStateAtom); - const setMainViewState = useSetAtom(setMainViewStateAtom); +const menu = [ + { + name: "Explore Models", + icon: "Search_gray", + state: MainViewState.ExploreModel, + }, + { + name: "My Models", + icon: "ViewGrid", + state: MainViewState.MyModel, + }, + { + name: "Settings", + icon: "Cog", + state: MainViewState.Setting, + }, +]; - const menu = [ - { - name: "Explore Models", - icon: "Search_gray", - state: MainViewState.ExploreModel, - }, - { - name: "My Models", - icon: "ViewGrid", - state: MainViewState.MyModel, - }, - { - name: "Settings", - icon: "Cog", - state: MainViewState.Setting, - }, - ]; - - const onMenuClick = (state: MainViewState) => { - if (state === currentState) return; - setMainViewState(state); - }; - - return ( - <div className="flex flex-col"> - <div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3"> - Your Configurations - </div> - <ul role="list" className="-mx-2 mt-2 space-y-1"> - {menu.map((item) => ( - <li key={item.name}> - <button - onClick={() => onMenuClick(item.state)} - className={classNames( - currentState === item.state - ? "bg-gray-50 text-indigo-600" - : "text-gray-600 hover:text-indigo-600 hover:bg-gray-50", - "group flex gap-x-3 rounded-md text-base py-2 px-3 w-full" - )} - > - <Image - src={`icons/${item.icon}.svg`} - width={24} - height={24} - alt="" - /> - <span className="truncate">{item.name}</span> - </button> - </li> - ))} - </ul> +const SidebarMenu: React.FC = () => ( + <div className="flex flex-col"> + <div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3"> + Your Configurations </div> - ); -}; + <ul role="list" className="-mx-2 mt-2 space-y-1 mb-2"> + {menu.map((item) => ( + <SidebarMenuItem + title={item.name} + viewState={item.state} + iconName={item.icon} + key={item.name} + /> + ))} + </ul> + </div> +); -export default SidebarMenu; +export default React.memo(SidebarMenu); diff --git a/web/app/_components/SidebarMenuItem/index.tsx b/web/app/_components/SidebarMenuItem/index.tsx new file mode 100644 index 000000000..68960fabd --- /dev/null +++ b/web/app/_components/SidebarMenuItem/index.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { + MainViewState, + getMainViewStateAtom, + setMainViewStateAtom, +} from "@/_helpers/JotaiWrapper"; +import { useAtomValue, useSetAtom } from "jotai"; +import Image from "next/image"; + +type Props = { + title: string; + viewState: MainViewState; + iconName: string; +}; + +const SidebarMenuItem: React.FC<Props> = ({ title, viewState, iconName }) => { + const currentState = useAtomValue(getMainViewStateAtom); + const setMainViewState = useSetAtom(setMainViewStateAtom); + + let className = + "text-gray-600 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full"; + if (currentState == viewState) { + className = + "bg-gray-100 text-indigo-600 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full"; + } + + const onClick = () => { + setMainViewState(viewState); + }; + + return ( + <li key={title}> + <button onClick={onClick} className={className}> + <Image src={`icons/${iconName}.svg`} width={24} height={24} alt="" /> + <span className="truncate">{title}</span> + </button> + </li> + ); +}; + +export default SidebarMenuItem; diff --git a/web/app/_components/SimpleCheckbox/index.tsx b/web/app/_components/SimpleCheckbox/index.tsx new file mode 100644 index 000000000..1a313ac9f --- /dev/null +++ b/web/app/_components/SimpleCheckbox/index.tsx @@ -0,0 +1,22 @@ +type Props = { + name: string; +}; + +const SimpleCheckbox: React.FC<Props> = ({ name }) => ( + <div className="relative flex items-center gap-[11px]"> + <div className="flex h-6 items-center"> + <input + id="offers" + aria-describedby="offers-description" + name="offers" + type="checkbox" + className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" + /> + </div> + <div className="text-xs"> + <label htmlFor="offers">{name}</label> + </div> + </div> +); + +export default SimpleCheckbox; diff --git a/web/app/_components/SimpleTag/index.tsx b/web/app/_components/SimpleTag/index.tsx new file mode 100644 index 000000000..7bd3f5e4d --- /dev/null +++ b/web/app/_components/SimpleTag/index.tsx @@ -0,0 +1,89 @@ +import React from "react"; + +export enum TagType { + Roleplay = "Roleplay", + Llama = "Llama", + Story = "Story", + Casual = "Casual", + Professional = "Professional", + CodeLlama = "CodeLlama", + Coding = "Coding", + + // Positive + Recommended = "Recommended", + Compatible = "Compatible", + + // Neutral + SlowOnDevice = "This model will be slow on your device", + + // Negative + InsufficientRam = "Insufficient RAM", + Incompatible = "Incompatible with your device", + TooLarge = "This model is too large for your device", + + // Performance + Medium = "Medium", + BalancedQuality = "Balanced Quality", +} + +const tagStyleMapper: Record<TagType, string> = { + [TagType.Roleplay]: "bg-red-100 text-red-800", + [TagType.Llama]: "bg-green-100 text-green-800", + [TagType.Story]: "bg-blue-100 text-blue-800", + [TagType.Casual]: "bg-yellow-100 text-yellow-800", + [TagType.Professional]: "text-indigo-800 bg-indigo-100", + [TagType.CodeLlama]: "bg-pink-100 text-pink-800", + [TagType.Coding]: "text-purple-800 bg-purple-100", + + [TagType.Recommended]: + "text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50", + [TagType.Compatible]: + "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10", + + [TagType.SlowOnDevice]: + "bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20", + + [TagType.Incompatible]: + "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10", + [TagType.InsufficientRam]: + "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10", + [TagType.TooLarge]: "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10", + + [TagType.Medium]: "bg-yellow-100 text-yellow-800", + [TagType.BalancedQuality]: "bg-yellow-100 text-yellow-800", +}; + +type Props = { + title: string; + type: TagType; + clickable?: boolean; + onClick?: () => void; +}; + +const SimpleTag: React.FC<Props> = ({ + onClick, + clickable = true, + title, + type, +}) => { + if (!clickable) { + return ( + <div + className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`} + > + {title} + </div> + ); + } + + return ( + <button + onClick={onClick} + className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`} + > + {title} x + </button> + ); +}; + +export default React.memo(SimpleTag); diff --git a/web/app/_helpers/JotaiWrapper.tsx b/web/app/_helpers/JotaiWrapper.tsx index f840e59ee..362a06c87 100644 --- a/web/app/_helpers/JotaiWrapper.tsx +++ b/web/app/_helpers/JotaiWrapper.tsx @@ -38,7 +38,6 @@ export const showingMobilePaneAtom = atom<boolean>(false); export const showingTyping = atom<boolean>(false); export const appDownloadProgress = atom<number>(-1); -export const activeModel = atom<string | undefined>(undefined); export const searchingModelText = atom<string>(""); /** @@ -259,7 +258,8 @@ export const getMainViewStateAtom = atom((get) => export const setMainViewStateAtom = atom( null, - (_get, set, state: MainViewState) => { + (get, set, state: MainViewState) => { + if (get(getMainViewStateAtom) === state) return; if (state !== MainViewState.Conversation) { set(activeConversationIdAtom, undefined); } diff --git a/web/app/_hooks/useCreateConversation.ts b/web/app/_hooks/useCreateConversation.ts index ffe68ba49..9ff798ba5 100644 --- a/web/app/_hooks/useCreateConversation.ts +++ b/web/app/_hooks/useCreateConversation.ts @@ -1,7 +1,6 @@ // import useGetCurrentUser from "./useGetCurrentUser"; import { useAtom, useSetAtom } from "jotai"; import { - activeModel, addNewConversationStateAtom, currentProductAtom, setActiveConvoIdAtom, @@ -18,7 +17,6 @@ const useCreateConversation = () => { ); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const addNewConvoState = useSetAtom(addNewConversationStateAtom); - const setActiveModel = useSetAtom(activeModel); const setActiveProduct = useSetAtom(currentProductAtom); const requestCreateConvo = async (model: Product) => { @@ -32,7 +30,6 @@ const useCreateConversation = () => { const id = await executeSerial(DataService.CREATE_CONVERSATION, conv); await executeSerial(InfereceService.INIT_MODEL, model); setActiveProduct(model); - setActiveModel(model.name); const mappedConvo: Conversation = { id, diff --git a/web/app/_hooks/useDeleteModel.ts b/web/app/_hooks/useDeleteModel.ts new file mode 100644 index 000000000..450806bd2 --- /dev/null +++ b/web/app/_hooks/useDeleteModel.ts @@ -0,0 +1,12 @@ +import { execute, executeSerial } from "@/_services/pluginService"; +import { DataService, ModelManagementService } from "../../shared/coreService"; +import { Product } from "@/_models/Product"; + +export default function useDeleteModel() { + const deleteModel = async (model: Product) => { + execute(DataService.DELETE_DOWNLOAD_MODEL, model.id); + await executeSerial(ModelManagementService.DELETE_MODEL, model.fileName); + }; + + return { deleteModel }; +} diff --git a/web/app/_hooks/useDownloadModel.ts b/web/app/_hooks/useDownloadModel.ts new file mode 100644 index 000000000..d29697a7d --- /dev/null +++ b/web/app/_hooks/useDownloadModel.ts @@ -0,0 +1,17 @@ +import { executeSerial } from "@/_services/pluginService"; +import { DataService, ModelManagementService } from "../../shared/coreService"; +import { Product } from "@/_models/Product"; + +export default function useDownloadModel() { + const downloadModel = async (model: Product) => { + await executeSerial(DataService.STORE_MODEL, model); + await executeSerial(ModelManagementService.DOWNLOAD_MODEL, { + downloadUrl: model.downloadUrl, + fileName: model.fileName, + }); + }; + + return { + downloadModel, + }; +} diff --git a/web/app/_hooks/useGetAvailableModels.ts b/web/app/_hooks/useGetAvailableModels.ts new file mode 100644 index 000000000..9b487c397 --- /dev/null +++ b/web/app/_hooks/useGetAvailableModels.ts @@ -0,0 +1,54 @@ +import { Product } from "@/_models/Product"; +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"; + +export default function useGetAvailableModels() { + const downloadState = useAtomValue(modelDownloadStateAtom); + const [allAvailableModels, setAllAvailableModels] = useState<Product[]>([]); + const [availableModels, setAvailableModels] = useState<Product[]>([]); + const [downloadedModels, setDownloadedModels] = useState<Product[]>([]); + + const getAvailableModelExceptDownloaded = async () => { + const avails = await getAvailableModels(); + const downloaded = await getModelFiles(); + + setAllAvailableModels(avails); + const availableOrDownloadingModels: Product[] = avails; + const successfullDownloadModels: Product[] = []; + + downloaded.forEach((item) => { + if (item.fileName && downloadState[item.fileName] == null) { + // if not downloading, consider as downloaded + successfullDownloadModels.push(item); + } else { + availableOrDownloadingModels.push(item); + } + }); + + setAvailableModels(availableOrDownloadingModels); + setDownloadedModels(successfullDownloadModels); + }; + + useEffect(() => { + getAvailableModelExceptDownloaded(); + }, []); + + return { + allAvailableModels, + availableModels, + downloadedModels, + getAvailableModelExceptDownloaded, + }; +} + +export async function getAvailableModels(): Promise<Product[]> { + const avails: Product[] = await executeSerial( + ModelManagementService.GET_AVAILABLE_MODELS + ); + + return avails ?? []; +} diff --git a/web/app/_hooks/useGetDownloadedModels.ts b/web/app/_hooks/useGetDownloadedModels.ts new file mode 100644 index 000000000..90cfa00a4 --- /dev/null +++ b/web/app/_hooks/useGetDownloadedModels.ts @@ -0,0 +1,30 @@ +import { Product } from "@/_models/Product"; +import { useEffect, useState } from "react"; +import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager"; +import { DataService, ModelManagementService } from "../../shared/coreService"; + +export function useGetDownloadedModels() { + const [downloadedModels, setDownloadedModels] = useState<Product[]>([]); + + useEffect(() => { + getDownloadedModels().then((downloadedModels) => { + setDownloadedModels(downloadedModels); + }); + }, []); + + return { downloadedModels }; +} + +export async function getDownloadedModels(): Promise<Product[]> { + const downloadedModels: Product[] = await executeSerial( + DataService.GET_FINISHED_DOWNLOAD_MODELS + ); + return downloadedModels ?? []; +} + +export async function getModelFiles(): Promise<Product[]> { + const downloadedModels: Product[] = await executeSerial( + ModelManagementService.GET_DOWNLOADED_MODELS + ); + return downloadedModels ?? []; +} diff --git a/web/app/_hooks/useStartStopModel.ts b/web/app/_hooks/useStartStopModel.ts new file mode 100644 index 000000000..e7e3cb40a --- /dev/null +++ b/web/app/_hooks/useStartStopModel.ts @@ -0,0 +1,24 @@ +import { currentProductAtom } from "@/_helpers/JotaiWrapper"; +import { executeSerial } from "@/_services/pluginService"; +import { DataService, InfereceService } from "../../shared/coreService"; +import { useSetAtom } from "jotai"; + +export default function useStartStopModel() { + const setActiveModel = useSetAtom(currentProductAtom); + + 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}`)); + } + }; + + const stopModel = async (modelId: string) => {}; + + return { startModel, stopModel }; +} diff --git a/web/app/_models/Product.ts b/web/app/_models/Product.ts index 1845b6e36..dad227bd3 100644 --- a/web/app/_models/Product.ts +++ b/web/app/_models/Product.ts @@ -27,4 +27,10 @@ export interface Product { updatedAt?: number; fileName?: string; downloadUrl?: string; + + accelerated: boolean; // TODO: add this in the database + totalSize: number; // TODO: add this in the database + format: string; // TODO: add this in the database // GGUF or something else + status: string; // TODO: add this in the database // Downloaded, Active + releaseDate: number; // TODO: add this in the database } diff --git a/web/app/_utils/converter.ts b/web/app/_utils/converter.ts new file mode 100644 index 000000000..8db0ce9b7 --- /dev/null +++ b/web/app/_utils/converter.ts @@ -0,0 +1,15 @@ +export const toGigabytes = (input: number) => { + if (input > 1024 ** 3) { + return (input / 1000 ** 3).toFixed(2) + "GB"; + } else if (input > 1024 ** 2) { + return (input / 1000 ** 2).toFixed(2) + "MB"; + } else if (input > 1024) { + return (input / 1000).toFixed(2) + "KB"; + } else { + return input + "B"; + } +}; + +export const formatDownloadPercentage = (input: number) => { + return (input * 100).toFixed(2) + "%"; +};