allow user to query for HF models
Signed-off-by: James <james@jan.ai>
This commit is contained in:
parent
0bdd3713d4
commit
6f50424917
@ -38,10 +38,20 @@ const deleteModel = async (path) =>
|
||||
}
|
||||
});
|
||||
|
||||
const searchHfModels = async (params) =>
|
||||
new Promise(async (resolve) => {
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI
|
||||
.invokePluginFunc(MODULE_PATH, "searchHfModels", params)
|
||||
.then((res) => resolve(res));
|
||||
}
|
||||
});
|
||||
|
||||
// Register all the above functions and objects with the relevant extension points
|
||||
export function init({ register }) {
|
||||
register("getDownloadedModels", "getDownloadedModels", getDownloadedModels);
|
||||
register("getAvailableModels", "getAvailableModels", getAvailableModels);
|
||||
register("downloadModel", "downloadModel", downloadModel);
|
||||
register("deleteModel", "deleteModel", deleteModel);
|
||||
register("searchHfModels", "searchHfModels", searchHfModels);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const path = require("path");
|
||||
const { readdirSync, lstatSync } = require("fs");
|
||||
const { app } = require("electron");
|
||||
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
|
||||
|
||||
const ALL_MODELS = [
|
||||
{
|
||||
@ -87,6 +88,49 @@ function getDownloadedModels() {
|
||||
return downloadedModels;
|
||||
}
|
||||
|
||||
const searchHfModels = async (params) => {
|
||||
const result = [];
|
||||
|
||||
var index = 0;
|
||||
|
||||
for await (const model of listModels({
|
||||
search: params.search,
|
||||
credentials: params.credentials,
|
||||
})) {
|
||||
const files = await listFilesByName(model.name);
|
||||
result.push({
|
||||
...model,
|
||||
files,
|
||||
});
|
||||
|
||||
index++;
|
||||
if (index === params.limit) break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const listFilesByName = async (modelName) => {
|
||||
const repo = { type: "model", name: modelName };
|
||||
const fileDownloadInfoMap = {};
|
||||
for await (const file of listFiles({
|
||||
repo: repo,
|
||||
})) {
|
||||
if (file.type === "file" && file.path.endsWith(".bin")) {
|
||||
const downloadInfo = await fileDownloadInfo({
|
||||
repo: repo,
|
||||
path: file.path,
|
||||
});
|
||||
fileDownloadInfoMap[file.path] = {
|
||||
...file,
|
||||
...downloadInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fileDownloadInfoMap;
|
||||
};
|
||||
|
||||
function getAvailableModels() {
|
||||
const downloadedModelIds = getDownloadedModels().map((model) => model.id);
|
||||
return ALL_MODELS.filter((model) => {
|
||||
@ -99,4 +143,5 @@ function getAvailableModels() {
|
||||
module.exports = {
|
||||
getDownloadedModels,
|
||||
getAvailableModels,
|
||||
searchHfModels,
|
||||
};
|
||||
|
||||
@ -24,5 +24,11 @@
|
||||
"dist/*",
|
||||
"package.json",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"@huggingface/hub": "^0.8.5"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"@huggingface/hub"
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { ReactNode } from "react";
|
||||
import Welcome from "../WelcomeContainer";
|
||||
import { Preferences } from "../Preferences";
|
||||
import MyModelContainer from "../MyModelContainer";
|
||||
@ -11,17 +10,14 @@ import {
|
||||
getMainViewStateAtom,
|
||||
} from "@/_helpers/atoms/MainView.atom";
|
||||
import EmptyChatContainer from "../EmptyChatContainer";
|
||||
import MainChat from "../MainChat";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function ChatContainer({ children }: Props) {
|
||||
export default function ChatContainer() {
|
||||
const viewState = useAtomValue(getMainViewStateAtom);
|
||||
|
||||
switch (viewState) {
|
||||
case MainViewState.ConversationEmptyModel:
|
||||
return <EmptyChatContainer />
|
||||
return <EmptyChatContainer />;
|
||||
case MainViewState.ExploreModel:
|
||||
return <ExploreModelContainer />;
|
||||
case MainViewState.Setting:
|
||||
@ -32,6 +28,6 @@ export default function ChatContainer({ children }: Props) {
|
||||
case MainViewState.Welcome:
|
||||
return <Welcome />;
|
||||
default:
|
||||
return <div className="flex flex-1 overflow-hidden">{children}</div>;
|
||||
return <MainChat />;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
|
||||
import ExploreModelItem from "../ExploreModelItem";
|
||||
import HeaderTitle from "../HeaderTitle";
|
||||
import SearchBar from "../SearchBar";
|
||||
import SearchBar, { SearchType } from "../SearchBar";
|
||||
import SimpleCheckbox from "../SimpleCheckbox";
|
||||
import SimpleTag, { TagType } from "../SimpleTag";
|
||||
import useGetHuggingFaceModel from "@/_hooks/useGetHuggingFaceModel";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const tags = [
|
||||
"Roleplay",
|
||||
@ -17,14 +20,22 @@ const tags = [
|
||||
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
|
||||
|
||||
const ExploreModelContainer: React.FC = () => {
|
||||
const { allAvailableModels } = useGetAvailableModels();
|
||||
const modelSearch = useAtomValue(modelSearchAtom);
|
||||
const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel();
|
||||
|
||||
useEffect(() => {
|
||||
getHuggingFaceModel(modelSearch);
|
||||
}, [modelSearch]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
|
||||
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
|
||||
<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">
|
||||
<SearchBar
|
||||
type={SearchType.Model}
|
||||
placeholder="Owner name like TheBloke, etc.."
|
||||
/>
|
||||
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
|
||||
<div className="w-64">
|
||||
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
|
||||
<SearchBar placeholder="Filter by tags" />
|
||||
<div className="flex flex-wrap gap-[9px] mt-[14px]">
|
||||
@ -39,16 +50,13 @@ const ExploreModelContainer: React.FC = () => {
|
||||
))}
|
||||
</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) => (
|
||||
<div className="flex-1 pb-4 gap-y-4 overflow-y-auto scroll">
|
||||
{modelList.map((item) => (
|
||||
<ExploreModelItem key={item.id} model={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
import ExploreModelItemHeader from "../ExploreModelItemHeader";
|
||||
import ModelVersionList from "../ModelVersionList";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import SimpleTag, { TagType } from "../SimpleTag";
|
||||
import { displayDate } from "@/_utils/datetime";
|
||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
model: Product;
|
||||
@ -16,8 +16,8 @@ type Props = {
|
||||
|
||||
const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
|
||||
[model.fileName ?? ""]
|
||||
() => atom((get) => get(modelDownloadStateAtom)[model.name ?? ""]),
|
||||
[model.name ?? ""]
|
||||
);
|
||||
const downloadState = useAtomValue(downloadAtom);
|
||||
const { downloadModel } = useDownloadModel();
|
||||
@ -28,9 +28,8 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
<ExploreModelItemHeader
|
||||
name={model.name}
|
||||
status={TagType.Recommended}
|
||||
total={model.totalSize}
|
||||
versions={model.availableVersions}
|
||||
downloadState={downloadState}
|
||||
onDownloadClick={() => downloadModel(model)}
|
||||
/>
|
||||
<div className="flex flex-col px-[26px] py-[22px]">
|
||||
<div className="flex justify-between">
|
||||
@ -87,13 +86,17 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
|
||||
<span className="text-sm font-medium text-gray-500">Tags</span>
|
||||
</div>
|
||||
</div>
|
||||
{show && <ModelVersionList />}
|
||||
{model.availableVersions.length > 0 && (
|
||||
<Fragment>
|
||||
{show && <ModelVersionList model={model} versions={model.availableVersions} />}
|
||||
<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>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,11 +3,13 @@ import PrimaryButton from "../PrimaryButton";
|
||||
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||
import { DownloadState } from "@/_models/DownloadState";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
import { ModelVersion } from "@/_models/Product";
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
total: number;
|
||||
status: TagType;
|
||||
versions: ModelVersion[];
|
||||
size?: number;
|
||||
downloadState?: DownloadState;
|
||||
onDownloadClick?: () => void;
|
||||
};
|
||||
@ -15,30 +17,41 @@ type Props = {
|
||||
const ExploreModelItemHeader: React.FC<Props> = ({
|
||||
name,
|
||||
status,
|
||||
total,
|
||||
size,
|
||||
versions,
|
||||
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 ? (
|
||||
}) => {
|
||||
let downloadButton = (
|
||||
<PrimaryButton
|
||||
title={size ? `Download (${toGigabytes(size)})` : "Download"}
|
||||
onClick={() => onDownloadClick?.()}
|
||||
/>
|
||||
);
|
||||
|
||||
if (downloadState != null) {
|
||||
// downloading
|
||||
downloadButton = (
|
||||
<SecondaryButton
|
||||
disabled
|
||||
title={`Downloading (${formatDownloadPercentage(
|
||||
downloadState.percent
|
||||
)})`}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<PrimaryButton
|
||||
title={total ? `Download (${toGigabytes(total)})` : "Download"}
|
||||
onClick={() => onDownloadClick?.()}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
} else if (versions.length === 0) {
|
||||
downloadButton = <SecondaryButton disabled title="No files available" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
{downloadButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreModelItemHeader;
|
||||
|
||||
@ -18,14 +18,14 @@ const HistoryList: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-grow pt-3 gap-2">
|
||||
<div className="flex flex-col flex-grow pt-3 gap-2 overflow-hidden">
|
||||
<ExpandableHeader
|
||||
title="CHAT HISTORY"
|
||||
expanded={expand}
|
||||
onClick={() => setExpand(!expand)}
|
||||
/>
|
||||
<div
|
||||
className={`flex flex-col gap-1 mt-1 ${!expand ? "hidden " : "block"}`}
|
||||
className={`flex flex-col gap-1 mt-1 overflow-y-auto scroll ${!expand ? "hidden " : "block"}`}
|
||||
>
|
||||
{conversations.length > 0 ? (
|
||||
conversations
|
||||
|
||||
@ -6,7 +6,7 @@ import HistoryList from "../HistoryList";
|
||||
import NewChatButton from "../NewChatButton";
|
||||
|
||||
const LeftContainer: React.FC = () => (
|
||||
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<div className="w-[323px] flex-shrink-0 py-3 h-screen border-r border-gray-200 flex flex-col">
|
||||
<SidebarHeader />
|
||||
<NewChatButton />
|
||||
<HistoryList />
|
||||
|
||||
@ -1,25 +1,57 @@
|
||||
import React from "react";
|
||||
import { toGigabytes } from "@/_utils/converter";
|
||||
import React, { useMemo } from "react";
|
||||
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
|
||||
import Image from "next/image";
|
||||
import { ModelVersion, Product } from "@/_models/Product";
|
||||
import useDownloadModel from "@/_hooks/useDownloadModel";
|
||||
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
totalSizeInByte: number;
|
||||
model: Product;
|
||||
modelVersion: ModelVersion;
|
||||
};
|
||||
|
||||
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">
|
||||
const ModelVersionItem: React.FC<Props> = ({ model, modelVersion }) => {
|
||||
const { downloadHfModel } = useDownloadModel();
|
||||
const downloadAtom = useMemo(
|
||||
() => atom((get) => get(modelDownloadStateAtom)[modelVersion.path ?? ""]),
|
||||
[modelVersion.path ?? ""]
|
||||
);
|
||||
const downloadState = useAtomValue(downloadAtom);
|
||||
|
||||
const onDownloadClick = () => {
|
||||
downloadHfModel(model, modelVersion);
|
||||
};
|
||||
|
||||
let downloadButton = (
|
||||
<button
|
||||
className="text-indigo-600 text-sm font-medium"
|
||||
onClick={onDownloadClick}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
);
|
||||
|
||||
if (downloadState) {
|
||||
downloadButton = (
|
||||
<div>{formatDownloadPercentage(downloadState.percent)}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-between items-center gap-4 pl-3 pt-3 pr-4 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>
|
||||
<span className="font-sm text-gray-900">{modelVersion.path}</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 className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
|
||||
{toGigabytes(modelVersion.size)}
|
||||
</div>
|
||||
<button className="text-indigo-600 text-sm font-medium">Download</button>
|
||||
{downloadButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelVersionItem;
|
||||
|
||||
@ -1,38 +1,21 @@
|
||||
import React from "react";
|
||||
import ModelVersionItem from "../ModelVersionItem";
|
||||
import { ModelVersion, Product } from "@/_models/Product";
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
{
|
||||
name: "Q4_K_M.gguf",
|
||||
total: 5600,
|
||||
},
|
||||
];
|
||||
type Props = {
|
||||
model: Product;
|
||||
versions: ModelVersion[];
|
||||
};
|
||||
|
||||
const ModelVersionList: React.FC = () => {
|
||||
return (
|
||||
const ModelVersionList: React.FC<Props> = ({ model, versions }) => (
|
||||
<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="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}
|
||||
/>
|
||||
{versions.map((item) => (
|
||||
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelVersionList;
|
||||
|
||||
@ -32,7 +32,7 @@ const NewChatButton: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
|
||||
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5 mx-3" />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ const PrimaryButton: React.FC<Props> = ({
|
||||
<button
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
className={`rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 ${
|
||||
className={`rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 line-clamp-1 flex-shrink-0 ${
|
||||
fullWidth ? "flex-1 " : ""
|
||||
}}`}
|
||||
>
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
import ChatContainer from "../ChatContainer";
|
||||
import MainChat from "../MainChat";
|
||||
import MonitorBar from "../MonitorBar";
|
||||
|
||||
const RightContainer = () => (
|
||||
<div className="flex flex-col flex-1 h-screen">
|
||||
<ChatContainer>
|
||||
<MainChat />
|
||||
</ChatContainer>
|
||||
<ChatContainer />
|
||||
<MonitorBar />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,14 +1,34 @@
|
||||
import { searchAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
|
||||
export enum SearchType {
|
||||
Model = "model",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type?: SearchType;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
||||
const setText = useSetAtom(searchAtom);
|
||||
const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const setModelSearch = useSetAtom(modelSearchAtom);
|
||||
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
if (type === SearchType.Model) {
|
||||
setModelSearch(searchValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative mt-3 flex items-center">
|
||||
<div className="absolute top-0 left-2 h-full flex items-center">
|
||||
@ -23,8 +43,10 @@ const SearchBar: React.FC<Props> = ({ placeholder }) => {
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
value={searchValue}
|
||||
placeholder={placeholderText}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
type Props = {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
@ -15,7 +15,7 @@ const SecondaryButton: React.FC<Props> = ({
|
||||
disabled={disabled}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
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 ${className}`}
|
||||
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 ${className} flex-shrink-0 line-clamp-1`}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import SecondaryButton from "../SecondaryButton";
|
||||
|
||||
const SidebarFooter: React.FC = () => (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<div className="flex justify-between items-center gap-2 mx-3">
|
||||
<SecondaryButton
|
||||
title={"Discord"}
|
||||
onClick={() =>
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const SidebarHeader: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-[10px]">
|
||||
const SidebarHeader: React.FC = () => (
|
||||
<div className="flex flex-col gap-[10px] px-3">
|
||||
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarHeader;
|
||||
export default React.memo(SidebarHeader);
|
||||
|
||||
@ -21,7 +21,7 @@ const menu = [
|
||||
];
|
||||
|
||||
const SidebarMenu: React.FC = () => (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col mx-3">
|
||||
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
|
||||
Your Configurations
|
||||
</div>
|
||||
|
||||
@ -19,3 +19,5 @@ export const appDownloadProgress = atom<number>(-1);
|
||||
export const searchingModelText = atom<string>("");
|
||||
|
||||
export const searchAtom = atom<string>("");
|
||||
|
||||
export const modelSearchAtom = atom<string>("");
|
||||
|
||||
@ -19,12 +19,12 @@ const useCreateConversation = () => {
|
||||
const addNewConvoState = useSetAtom(addNewConversationStateAtom);
|
||||
|
||||
const requestCreateConvo = async (model: Product) => {
|
||||
const conversationName = model.name;
|
||||
const conv: Conversation = {
|
||||
image: undefined,
|
||||
model_id: model.id,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
name: "Conversation",
|
||||
name: conversationName,
|
||||
};
|
||||
const id = await executeSerial(DataService.CREATE_CONVERSATION, conv);
|
||||
await initModel(model);
|
||||
@ -32,7 +32,7 @@ const useCreateConversation = () => {
|
||||
const mappedConvo: Conversation = {
|
||||
id,
|
||||
model_id: model.id,
|
||||
name: "Conversation",
|
||||
name: conversationName,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { executeSerial } from "@/_services/pluginService";
|
||||
import { DataService, ModelManagementService } from "../../shared/coreService";
|
||||
import { Product } from "@/_models/Product";
|
||||
import { ModelVersion, Product } from "@/_models/Product";
|
||||
|
||||
export default function useDownloadModel() {
|
||||
const downloadModel = async (model: Product) => {
|
||||
@ -11,7 +11,28 @@ export default function useDownloadModel() {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadHfModel = async (
|
||||
model: Product,
|
||||
modelVersion: ModelVersion
|
||||
) => {
|
||||
const hfModel: Product = {
|
||||
...model,
|
||||
id: `${model.author}.${modelVersion.path}`,
|
||||
slug: `${model.author}.${modelVersion.path}`,
|
||||
name: `${model.name} - ${modelVersion.path}`,
|
||||
fileName: modelVersion.path,
|
||||
totalSize: modelVersion.size,
|
||||
downloadUrl: modelVersion.downloadUrl,
|
||||
};
|
||||
await executeSerial(DataService.STORE_MODEL, hfModel);
|
||||
await executeSerial(ModelManagementService.DOWNLOAD_MODEL, {
|
||||
downloadUrl: hfModel.downloadUrl,
|
||||
fileName: hfModel.fileName,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
downloadModel,
|
||||
downloadHfModel,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { Product } from "@/_models/Product";
|
||||
import { ModelVersion, Product, ProductType } from "@/_models/Product";
|
||||
import { useEffect, useState } from "react";
|
||||
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
||||
import { DataService, ModelManagementService } from "../../shared/coreService";
|
||||
import { SearchModelParamHf } from "@/_models/hf/SearchModelParam.hf";
|
||||
import { ListModelOutputHf } from "@/_models/hf/ListModelOutput.hf";
|
||||
|
||||
export function useGetDownloadedModels() {
|
||||
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
|
||||
@ -28,3 +30,53 @@ export async function getModelFiles(): Promise<Product[]> {
|
||||
);
|
||||
return downloadedModels ?? [];
|
||||
}
|
||||
|
||||
export async function searchHfModels(
|
||||
params: SearchModelParamHf
|
||||
): Promise<Product[]> {
|
||||
const result: ListModelOutputHf[] = await executeSerial(
|
||||
ModelManagementService.SEARCH_HF_MODELS,
|
||||
params
|
||||
);
|
||||
|
||||
const products: Product[] = result.map((model) => {
|
||||
const modelVersions: ModelVersion[] = [];
|
||||
|
||||
for (const [, file] of Object.entries(model.files)) {
|
||||
const modelVersion: ModelVersion = {
|
||||
path: file.path,
|
||||
type: file.type,
|
||||
downloadUrl: file.downloadLink,
|
||||
size: file.size,
|
||||
};
|
||||
modelVersions.push(modelVersion);
|
||||
}
|
||||
|
||||
const p = {
|
||||
id: model.id,
|
||||
slug: model.name,
|
||||
name: model.name,
|
||||
description: model.name,
|
||||
avatarUrl: "",
|
||||
longDescription: model.name,
|
||||
technicalDescription: model.name,
|
||||
author: model.name.split("/")[0],
|
||||
version: "1.0.0",
|
||||
modelUrl: "https://google.com",
|
||||
nsfw: false,
|
||||
greeting: "Hello there",
|
||||
type: ProductType.LLM,
|
||||
createdAt: -1,
|
||||
accelerated: true,
|
||||
totalSize: -1,
|
||||
format: "",
|
||||
status: "Not downloaded",
|
||||
releaseDate: -1,
|
||||
availableVersions: modelVersions,
|
||||
};
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
25
web/app/_hooks/useGetHuggingFaceModel.ts
Normal file
25
web/app/_hooks/useGetHuggingFaceModel.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useState } from "react";
|
||||
import { searchHfModels } from "./useGetDownloadedModels";
|
||||
import { SearchModelParamHf } from "@/_models/hf/SearchModelParam.hf";
|
||||
import { Product } from "@/_models/Product";
|
||||
|
||||
export default function useGetHuggingFaceModel() {
|
||||
const [modelList, setModelList] = useState<Product[]>([]);
|
||||
|
||||
const getHuggingFaceModel = async (owner?: string) => {
|
||||
if (!owner) {
|
||||
setModelList([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParams: SearchModelParamHf = {
|
||||
search: { owner },
|
||||
limit: 5,
|
||||
};
|
||||
const result = await searchHfModels(searchParams);
|
||||
console.debug("result", JSON.stringify(result));
|
||||
setModelList(result);
|
||||
};
|
||||
|
||||
return { modelList, getHuggingFaceModel };
|
||||
}
|
||||
@ -33,4 +33,28 @@ export interface Product {
|
||||
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
|
||||
|
||||
availableVersions: ModelVersion[];
|
||||
}
|
||||
|
||||
export interface ModelVersion {
|
||||
/**
|
||||
* Act as the id of the model version
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* currently, we only have `file` type
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* The download url for the model version
|
||||
*/
|
||||
downloadUrl: string;
|
||||
|
||||
/**
|
||||
* File size in bytes
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
24
web/app/_models/hf/ListModelOutput.hf.ts
Normal file
24
web/app/_models/hf/ListModelOutput.hf.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Task } from "./SearchModelParam.hf";
|
||||
|
||||
export type ListModelOutputHf = {
|
||||
id: string;
|
||||
name: string;
|
||||
private: boolean;
|
||||
task: Task;
|
||||
downloads: number;
|
||||
gated: boolean;
|
||||
likes: number;
|
||||
updatedAt?: number | null;
|
||||
|
||||
files: Map<string, FileInfo>;
|
||||
};
|
||||
|
||||
export type FileInfo = {
|
||||
type: string;
|
||||
oid: string;
|
||||
size: number;
|
||||
lfs: Map<string, unknown>;
|
||||
path: string;
|
||||
etag: string;
|
||||
downloadLink: string;
|
||||
};
|
||||
53
web/app/_models/hf/SearchModelParam.hf.ts
Normal file
53
web/app/_models/hf/SearchModelParam.hf.ts
Normal file
@ -0,0 +1,53 @@
|
||||
export type SearchModelParamHf = {
|
||||
search?: {
|
||||
owner?: string;
|
||||
task?: Task;
|
||||
};
|
||||
credentials?: {
|
||||
accessToken: string;
|
||||
};
|
||||
limit: number;
|
||||
};
|
||||
|
||||
export type Task =
|
||||
| "text-classification"
|
||||
| "token-classification"
|
||||
| "table-question-answering"
|
||||
| "question-answering"
|
||||
| "zero-shot-classification"
|
||||
| "translation"
|
||||
| "summarization"
|
||||
| "conversational"
|
||||
| "feature-extraction"
|
||||
| "text-generation"
|
||||
| "text2text-generation"
|
||||
| "fill-mask"
|
||||
| "sentence-similarity"
|
||||
| "text-to-speech"
|
||||
| "automatic-speech-recognition"
|
||||
| "audio-to-audio"
|
||||
| "audio-classification"
|
||||
| "voice-activity-detection"
|
||||
| "depth-estimation"
|
||||
| "image-classification"
|
||||
| "object-detection"
|
||||
| "image-segmentation"
|
||||
| "text-to-image"
|
||||
| "image-to-text"
|
||||
| "image-to-image"
|
||||
| "unconditional-image-generation"
|
||||
| "video-classification"
|
||||
| "reinforcement-learning"
|
||||
| "robotics"
|
||||
| "tabular-classification"
|
||||
| "tabular-regression"
|
||||
| "tabular-to-text"
|
||||
| "table-to-text"
|
||||
| "multiple-choice"
|
||||
| "text-retrieval"
|
||||
| "time-series-forecasting"
|
||||
| "visual-question-answering"
|
||||
| "document-question-answering"
|
||||
| "zero-shot-image-classification"
|
||||
| "graph-ml"
|
||||
| "other";
|
||||
@ -3,7 +3,9 @@ export const isToday = (timestamp: number) => {
|
||||
return today.setHours(0, 0, 0, 0) == new Date(timestamp).setHours(0, 0, 0, 0);
|
||||
};
|
||||
|
||||
export const displayDate = (timestamp: number) => {
|
||||
export const displayDate = (timestamp?: number) => {
|
||||
if (!timestamp) return "N/A";
|
||||
|
||||
let displayDate = new Date(timestamp).toLocaleString();
|
||||
if (isToday(timestamp)) {
|
||||
displayDate = new Date(timestamp).toLocaleTimeString(undefined, {
|
||||
|
||||
@ -37,6 +37,7 @@ export enum ModelManagementService {
|
||||
GET_AVAILABLE_MODELS = "getAvailableModels",
|
||||
DELETE_MODEL = "deleteModel",
|
||||
DOWNLOAD_MODEL = "downloadModel",
|
||||
SEARCH_HF_MODELS = "searchHfModels",
|
||||
}
|
||||
|
||||
export enum PreferenceService {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user