Add model screen and refactoring (#242)
* Add model screen and refactoring Signed-off-by: James <james@jan.ai>
This commit is contained in:
parent
b043383ce1
commit
26f732d541
@ -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,
|
||||
|
||||
19
web/app/_components/ActiveModelTable/index.tsx
Normal file
19
web/app/_components/ActiveModelTable/index.tsx
Normal file
@ -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 (
|
||||
<Fragment>
|
||||
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
|
||||
<ModelTable models={[activeModel]} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActiveModelTable;
|
||||
@ -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 <ModelManagement />;
|
||||
return <ExploreModelContainer />;
|
||||
case MainViewState.Setting:
|
||||
return <Preferences />;
|
||||
case MainViewState.ResourceMonitor:
|
||||
case MainViewState.MyModel:
|
||||
return <MyModelContainer />;
|
||||
case MainViewState.Welcome:
|
||||
return <Welcome />;
|
||||
default:
|
||||
|
||||
@ -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 (
|
||||
<Transition.Root show={show} as={Fragment}>
|
||||
@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
|
||||
onClick={onLogOutClick}
|
||||
onClick={onConfirmDelete}
|
||||
>
|
||||
Log out
|
||||
</button>
|
||||
|
||||
@ -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<Props> = ({
|
||||
isRecommend,
|
||||
required,
|
||||
onDeleteClick,
|
||||
}) => {
|
||||
|
||||
const handleViewDetails = () => {};
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={product.author}
|
||||
description={product.description}
|
||||
isRecommend={isRecommend}
|
||||
name={product.name}
|
||||
type={product.type}
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
<button onClick={() => onDeleteClick?.(product)}>Delete</button>
|
||||
</div>
|
||||
}) => (
|
||||
<div className="border rounded-lg border-gray-200">
|
||||
<div className="flex justify-between py-4 px-3 gap-[10px]">
|
||||
<DownloadModelContent
|
||||
required={required}
|
||||
author={product.author}
|
||||
description={product.description}
|
||||
isRecommend={isRecommend}
|
||||
name={product.name}
|
||||
type={product.type}
|
||||
/>
|
||||
<div className="flex flex-col justify-center">
|
||||
<button onClick={() => onDeleteClick?.(product)}>Delete</button>
|
||||
</div>
|
||||
{/* <ViewModelDetailButton callback={handleViewDetails} /> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DownloadedModelCard;
|
||||
|
||||
20
web/app/_components/DownloadedModelTable/index.tsx
Normal file
20
web/app/_components/DownloadedModelTable/index.tsx
Normal file
@ -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 (
|
||||
<Fragment>
|
||||
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
|
||||
<div className="py-5 w-[568px]">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<ModelTable models={downloadedModels} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadedModelTable;
|
||||
55
web/app/_components/ExploreModelContainer/index.tsx
Normal file
55
web/app/_components/ExploreModelContainer/index.tsx
Normal file
@ -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 (
|
||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||
<HeaderTitle title="Explore Models" />
|
||||
<SearchBar placeholder="Search or HuggingFace URL" />
|
||||
<div className="flex gap-x-14 mt-[38px]">
|
||||
<div className="flex-1 flex-shrink-0">
|
||||
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
|
||||
<SearchBar placeholder="Filter by tags" />
|
||||
<div className="flex flex-wrap gap-[9px] mt-[14px]">
|
||||
{tags.map((item) => (
|
||||
<SimpleTag key={item} title={item} type={item as TagType} />
|
||||
))}
|
||||
</div>
|
||||
<hr className="my-10" />
|
||||
<fieldset>
|
||||
{checkboxs.map((item) => (
|
||||
<SimpleCheckbox key={item} name={item} />
|
||||
))}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div className="flex-[3_3_0%]">
|
||||
<h2 className="font-semibold text-xs mb-[18px]">Results</h2>
|
||||
<div className="flex flex-col gap-[31px]">
|
||||
{allAvailableModels.map((item) => (
|
||||
<ExploreModelItem key={item.id} model={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreModelContainer;
|
||||
101
web/app/_components/ExploreModelItem/index.tsx
Normal file
101
web/app/_components/ExploreModelItem/index.tsx
Normal file
@ -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<Props> = ({ 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 (
|
||||
<div className="flex flex-col border border-gray-200 rounded-[5px]">
|
||||
<ExploreModelItemHeader
|
||||
name={model.name}
|
||||
status={TagType.Recommended}
|
||||
total={model.totalSize}
|
||||
downloadState={downloadState}
|
||||
onDownloadClick={() => downloadModel(model)}
|
||||
/>
|
||||
<div className="flex flex-col px-[26px] py-[22px]">
|
||||
<div className="flex justify-between">
|
||||
<div className="flex-1 flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
Model Format
|
||||
</div>
|
||||
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
|
||||
GGUF
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
Hardware Compatibility
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<SimpleTag
|
||||
clickable={false}
|
||||
title={TagType.Compatible}
|
||||
type={TagType.Compatible}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-8">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
Release Date
|
||||
</div>
|
||||
<div className="text-sm font-normal text-gray-900">
|
||||
{displayDate(model.releaseDate)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-medium text-gray-500">
|
||||
Expected Performance
|
||||
</div>
|
||||
<SimpleTag
|
||||
title={TagType.Medium}
|
||||
type={TagType.Medium}
|
||||
clickable={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 mt-[26px]">
|
||||
<span className="text-sm font-medium text-gray-500">About</span>
|
||||
<span className="text-sm font-normal text-gray-500">
|
||||
{model.longDescription}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-gray-500">Tags</span>
|
||||
</div>
|
||||
</div>
|
||||
{show && <ModelVersionList />}
|
||||
<button
|
||||
onClick={() => setShow(!show)}
|
||||
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
|
||||
>
|
||||
{!show ? "+ Show Available Versions" : "- Collapse"}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreModelItem;
|
||||
44
web/app/_components/ExploreModelItemHeader/index.tsx
Normal file
44
web/app/_components/ExploreModelItemHeader/index.tsx
Normal file
@ -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<Props> = ({
|
||||
name,
|
||||
status,
|
||||
total,
|
||||
downloadState,
|
||||
onDownloadClick,
|
||||
}) => (
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{name}</span>
|
||||
<SimpleTag title={status} type={status} clickable={false} />
|
||||
</div>
|
||||
{downloadState != null ? (
|
||||
<SecondaryButton
|
||||
disabled
|
||||
title={`Downloading (${formatDownloadPercentage(
|
||||
downloadState.percent
|
||||
)})`}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
title={total ? `Download (${toGigabytes(total)})` : "Download"}
|
||||
onClick={() => onDownloadClick?.()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ExploreModelItemHeader;
|
||||
@ -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<Props> = ({
|
||||
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);
|
||||
|
||||
@ -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 = () => (
|
||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<SidebarHeader />
|
||||
<div className="h-5" />
|
||||
<SecondaryButton title={"New Chat"} onClick={() => {}} />
|
||||
<div className="h-6" />
|
||||
<HistoryList />
|
||||
<SidebarMenu />
|
||||
<SidebarFooter />
|
||||
|
||||
46
web/app/_components/ModelActionButton/index.tsx
Normal file
46
web/app/_components/ModelActionButton/index.tsx
Normal file
@ -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, ModelActionStyle> = {
|
||||
[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<Props> = ({ type, onActionClick }) => {
|
||||
const styles = modelActionMapper[type];
|
||||
const onClick = () => {
|
||||
onActionClick(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<td className="whitespace-nowrap px-6 py-4 text-sm">
|
||||
<PrimaryButton title={styles.title} onClick={onClick} />
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelActionButton;
|
||||
35
web/app/_components/ModelActionMenu/index.tsx
Normal file
35
web/app/_components/ModelActionMenu/index.tsx
Normal file
@ -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<Props> = ({ onDeleteClick }) => {
|
||||
return (
|
||||
<Menu as="div" className="relative flex-none">
|
||||
<Menu.Button className="block text-gray-500 hover:text-gray-900">
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
<button onClick={onDeleteClick}>Delete</button>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelActionMenu;
|
||||
@ -1,3 +1,5 @@
|
||||
import { toGigabytes } from "@/_utils/converter";
|
||||
|
||||
type Props = {
|
||||
total: number;
|
||||
value: number;
|
||||
@ -18,16 +20,4 @@ const ModelDownloadingButton: React.FC<Props> = ({ 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;
|
||||
|
||||
@ -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<Product[]>([]);
|
||||
const [availableModels, setAvailableModels] = useState<Product[]>([]);
|
||||
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 (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||
<div className="pb-5 flex flex-col gap-2">
|
||||
<Title title="Downloaded models" />
|
||||
{downloadedModels
|
||||
|
||||
78
web/app/_components/ModelRow/index.tsx
Normal file
78
web/app/_components/ModelRow/index.tsx
Normal file
@ -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;
|
||||
46
web/app/_components/ModelStatusComponent/index.tsx
Normal file
46
web/app/_components/ModelStatusComponent/index.tsx
Normal file
@ -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>
|
||||
);
|
||||
};
|
||||
34
web/app/_components/ModelTable/index.tsx
Normal file
34
web/app/_components/ModelTable/index.tsx
Normal file
@ -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);
|
||||
16
web/app/_components/ModelTableHeader/index.tsx
Normal file
16
web/app/_components/ModelTableHeader/index.tsx
Normal file
@ -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);
|
||||
25
web/app/_components/ModelVersionItem/index.tsx
Normal file
25
web/app/_components/ModelVersionItem/index.tsx
Normal file
@ -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;
|
||||
38
web/app/_components/ModelVersionList/index.tsx
Normal file
38
web/app/_components/ModelVersionList/index.tsx
Normal file
@ -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;
|
||||
@ -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>
|
||||
|
||||
13
web/app/_components/MyModelContainer/index.tsx
Normal file
13
web/app/_components/MyModelContainer/index.tsx
Normal file
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
41
web/app/_components/SidebarMenuItem/index.tsx
Normal file
41
web/app/_components/SidebarMenuItem/index.tsx
Normal file
@ -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;
|
||||
22
web/app/_components/SimpleCheckbox/index.tsx
Normal file
22
web/app/_components/SimpleCheckbox/index.tsx
Normal file
@ -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;
|
||||
89
web/app/_components/SimpleTag/index.tsx
Normal file
89
web/app/_components/SimpleTag/index.tsx
Normal file
@ -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);
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
12
web/app/_hooks/useDeleteModel.ts
Normal file
12
web/app/_hooks/useDeleteModel.ts
Normal file
@ -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 };
|
||||
}
|
||||
17
web/app/_hooks/useDownloadModel.ts
Normal file
17
web/app/_hooks/useDownloadModel.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
54
web/app/_hooks/useGetAvailableModels.ts
Normal file
54
web/app/_hooks/useGetAvailableModels.ts
Normal file
@ -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 ?? [];
|
||||
}
|
||||
30
web/app/_hooks/useGetDownloadedModels.ts
Normal file
30
web/app/_hooks/useGetDownloadedModels.ts
Normal file
@ -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 ?? [];
|
||||
}
|
||||
24
web/app/_hooks/useStartStopModel.ts
Normal file
24
web/app/_hooks/useStartStopModel.ts
Normal file
@ -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 };
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
15
web/app/_utils/converter.ts
Normal file
15
web/app/_utils/converter.ts
Normal file
@ -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) + "%";
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user