-
-
-
-
+}) => (
+
+
+
+
+
- {/*
*/}
- );
-};
+
+);
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) => (
+
+ ))}
+
+
+
+
+
+
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 (
+
+ );
+};
+
+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
= ({ 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 (
+
+ |
+ {model.name}
+ {model.version}
+ |
+
+
+ {model.format}
+ {model.accelerated && (
+
+
+ GPU Accelerated
+
+ )}
+
+ |
+
+ {model.totalSize}
+ |
+
+
+ |
+
+
+
+ |
+
+ );
+};
+
+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.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 = ({ status }) => {
+ const statusType = ModelStatusMapper[status];
+ return (
+
+ {statusType.title}
+
+ );
+};
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 = ({ models }) => (
+
+
+
+
+ {tableHeaders.map((item) => (
+
+ ))}
+ |
+ Edit
+ |
+
+
+
+ {models.map((model) => (
+
+ ))}
+
+
+
+);
+
+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 = ({ title }) => (
+
+ {title}
+ |
+);
+
+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 = ({ title, totalSizeInByte }) => (
+
+
+
+ {title}
+
+
+
+ {toGigabytes(totalSizeInByte)}
+
+
+
+
+);
+
+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 (
+
+
+ Available Versions
+
+
+ {data.map((item, index) => (
+
+ ))}
+
+
+ );
+};
+
+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(0);
const [gpu, setGPU] = useState(0);
const [cpu, setCPU] = useState(0);
const [version, setVersion] = useState("");
- 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 = () => {
- {modelName && modelName.length > 0 && (
-
+ {activeModel && (
+
)}
v{version}
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 = () => (
+
+);
+
+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 = () => (
-
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
= ({ placeholder }) => {
+ const setText = useSetAtom(searchAtom);
+ let placeholderText = placeholder ? placeholder : "Search (⌘K)";
return (
-
+
{
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 = ({ 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}
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 (
-
-
- Your Configurations
-
-
- {menu.map((item) => (
- -
-
-
- ))}
-
+const SidebarMenu: React.FC = () => (
+
+
+ Your Configurations
- );
-};
+
+ {menu.map((item) => (
+
+ ))}
+
+
+);
-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
= ({ 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 (
+
+
+
+ );
+};
+
+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 = ({ name }) => (
+
+);
+
+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.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 = ({
+ onClick,
+ clickable = true,
+ title,
+ type,
+}) => {
+ if (!clickable) {
+ return (
+
+ {title}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+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(false);
export const showingTyping = atom(false);
export const appDownloadProgress = atom(-1);
-export const activeModel = atom(undefined);
export const searchingModelText = atom("");
/**
@@ -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([]);
+ const [availableModels, setAvailableModels] = useState([]);
+ const [downloadedModels, setDownloadedModels] = useState([]);
+
+ 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 {
+ 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([]);
+
+ useEffect(() => {
+ getDownloadedModels().then((downloadedModels) => {
+ setDownloadedModels(downloadedModels);
+ });
+ }, []);
+
+ return { downloadedModels };
+}
+
+export async function getDownloadedModels(): Promise {
+ const downloadedModels: Product[] = await executeSerial(
+ DataService.GET_FINISHED_DOWNLOAD_MODELS
+ );
+ return downloadedModels ?? [];
+}
+
+export async function getModelFiles(): Promise {
+ 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) + "%";
+};