Signed-off-by: James <james@jan.ai>
This commit is contained in:
James 2023-10-05 11:33:24 -07:00 committed by Louis
parent e47d19e6e4
commit 9a79c3effa
53 changed files with 596 additions and 2794 deletions

View File

@ -23,7 +23,7 @@ async function initModel(product) {
console.error(
"A subprocess is already running. Attempt to kill then reinit."
);
killSubprocess();
dispose();
}
let binaryFolder = path.join(__dirname, "nitro"); // Current directory by default

View File

@ -57,7 +57,8 @@
"pacote": "^17.0.4",
"react-intersection-observer": "^9.5.2",
"request": "^2.88.2",
"request-progress": "^3.0.0"
"request-progress": "^3.0.0",
"use-debounce": "^9.0.4"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",

1614
node_modules/.yarn-integrity generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ const AvailableModelCard: React.FC<Props> = ({
return (
<div className="border rounded-lg border-gray-200">
<div className="flex justify-between py-4 px-3 gap-[10px]">
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}

View File

@ -11,7 +11,6 @@ export default function renderChatMessage({
senderName,
createdAt,
imageUrls,
htmlText,
text,
}: ChatMessage): React.ReactNode {
switch (messageType) {
@ -45,7 +44,7 @@ export default function renderChatMessage({
senderName={senderName}
createdAt={createdAt}
senderType={messageSenderType}
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
text={text}
/>
);
default:

View File

@ -32,7 +32,7 @@ const ConversationalCard: React.FC<Props> = ({ product }) => {
{description}
</span>
</div>
<span className="flex text-xs leading-5 text-gray-500 items-center gap-[2px]">
<span className="flex text-xs leading-5 text-gray-500 items-center gap-0.5">
<Image src={"icons/play.svg"} width={16} height={16} alt="" />
32.2k runs
</span>

View File

@ -18,19 +18,19 @@ const DownloadModelContent: React.FC<Props> = ({
type,
}) => {
return (
<div className="w-4/5 flex flex-col gap-[10px]">
<div className="w-4/5 flex flex-col gap-2.5">
<div className="flex items-center gap-1">
<h2 className="font-medium text-xl leading-[25px] tracking-[-0.4px] text-gray-900">
{name}
</h2>
<DownloadModelTitle title={type} />
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] font-semibold text-purple-800">
{author}
</span>
</div>
{required && (
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] text-[#11192899]">
Required{" "}
</span>
@ -44,7 +44,7 @@ const DownloadModelContent: React.FC<Props> = ({
<div
className={`${
isRecommend ? "flex" : "hidden"
} w-fit justify-center items-center bg-green-50 rounded-full px-[10px] py-[2px] gap-2`}
} w-fit justify-center items-center bg-green-50 rounded-full px-2.5 py-0.5 gap-2`}
>
<div className="w-3 h-3 rounded-full bg-green-400"></div>
<span className="text-green-600 font-medium text-xs leading-18px">

View File

@ -3,7 +3,7 @@ type Props = {
};
export const DownloadModelTitle: React.FC<Props> = ({ title }) => (
<div className="py-[2px] px-[10px] bg-purple-100 rounded-md text-center">
<div className="py-0.5 px-2.5 bg-purple-100 rounded-md text-center">
<span className="text-xs leading-[18px] font-medium text-purple-800">
{title}
</span>

View File

@ -16,7 +16,7 @@ const DownloadedModelCard: React.FC<Props> = ({
onDeleteClick,
}) => (
<div className="border rounded-lg border-gray-200">
<div className="flex justify-between py-4 px-3 gap-[10px]">
<div className="flex justify-between py-4 px-3 gap-2.5">
<DownloadModelContent
required={required}
author={product.author}

View File

@ -1,4 +1,4 @@
import React, { Fragment } from "react";
import React from "react";
import SearchBar from "../SearchBar";
import ModelTable from "../ModelTable";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
@ -6,6 +6,8 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
if (!downloadedModels || downloadedModels.length === 0) return null;
return (
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>

View File

@ -8,7 +8,7 @@ type Props = {
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => (
<button onClick={onClick} className="flex items-center justify-between px-2">
<h2 className="text-gray-400 font-bold text-[12px] leading-[12px] pl-1">
<h2 className="text-gray-400 font-bold text-xs leading-[12px] pl-1">
{title}
</h2>
<div className="mr-2">

View File

@ -1,19 +1,7 @@
import HeaderTitle from "../HeaderTitle";
import SearchBar, { SearchType } from "../SearchBar";
import SimpleCheckbox from "../SimpleCheckbox";
import SimpleTag, { TagType } from "../SimpleTag";
import ExploreModelList from "../ExploreModelList";
const tags = [
"Roleplay",
"Llama",
"Story",
"Casual",
"Professional",
"CodeLlama",
"Coding",
];
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
import ExploreModelFilter from "../ExploreModelFilter";
const ExploreModelContainer: React.FC = () => (
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
@ -23,21 +11,7 @@ const ExploreModelContainer: React.FC = () => (
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]">
{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>
<ExploreModelFilter />
<ExploreModelList />
</div>
</div>

View File

@ -0,0 +1,40 @@
import React from "react";
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 ExploreModelFilter: React.FC = () => {
const enabled = false;
if (!enabled) return null;
return (
<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]">
{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>
);
};
export default ExploreModelFilter;

View File

@ -1,3 +1,5 @@
/* eslint-disable react/display-name */
"use client";
import ExploreModelItemHeader from "../ExploreModelItemHeader";
@ -11,9 +13,7 @@ type Props = {
model: Product;
};
export type Ref = HTMLDivElement;
const ExploreModelItem = forwardRef<Ref, Props>(({ model }, ref) => {
const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const [show, setShow] = useState(false);
return (
@ -33,7 +33,7 @@ const ExploreModelItem = forwardRef<Ref, Props>(({ model }, ref) => {
<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">
<div className="px-2.5 py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
GGUF
</div>
</div>

View File

@ -66,7 +66,7 @@ const HistoryItem: React.FC<Props> = ({
return (
<button
className={`flex flex-row mx-1 items-center gap-[10px] rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
className={`flex flex-row mx-1 items-center gap-2.5 rounded-lg p-2 ${backgroundColor} hover:bg-hover-light`}
onClick={onClick}
>
<Image
@ -79,7 +79,7 @@ const HistoryItem: React.FC<Props> = ({
<div className="flex flex-col justify-between text-sm leading-[20px] w-full">
<div className="flex flex-row items-center justify-between">
<span className="text-gray-900 text-left">{name}</span>
<span className="text-[11px] leading-[13px] tracking-[-0.4px] text-gray-400">
<span className="text-xs leading-[13px] tracking-[-0.4px] text-gray-400">
{updatedAt && new Date(updatedAt).toDateString()}
</span>
</div>

View File

@ -7,7 +7,7 @@ const JanLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
return (
<button
className="p-3 flex gap-[2px] items-center"
className="p-3 flex gap-0.5 items-center"
onClick={() => setActiveConvoId(undefined)}
>
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />

View File

@ -12,7 +12,7 @@ import {
import EmptyChatContainer from "../EmptyChatContainer";
import MainChat from "../MainChat";
export default function ChatContainer() {
const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom);
switch (viewState) {
@ -30,4 +30,6 @@ export default function ChatContainer() {
default:
return <MainChat />;
}
}
};
export default MainView;

View File

@ -38,7 +38,7 @@ const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
return (
<td className="whitespace-nowrap px-6 py-4 text-sm">
<PrimaryButton title={styles.title} onClick={onClick} />
<PrimaryButton title={styles.title} onClick={onClick} className={styles.backgroundColor} />
</td>
);
};

View File

@ -6,30 +6,37 @@ 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>
);
};
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
<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>
{({ active }) => (
<button
className={`${
active ? "bg-violet-500 text-white" : "text-gray-900"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={onDeleteClick}
>
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
export default ModelActionMenu;

View File

@ -11,7 +11,7 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
<button className="py-2 px-3 flex gap-2 border text-xs leading-[18px] border-gray-200 rounded-lg">
Downloading...
</button>
<div className="py-[2px] px-[10px] bg-gray-200 rounded">
<div className="py-0.5 px-2.5 bg-gray-200 rounded">
<span className="text-xs font-medium text-gray-800">
{toGigabytes(value)} / {toGigabytes(total)}
</span>

View File

@ -1,86 +0,0 @@
"use client";
import { useAtomValue } from "jotai";
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 searchText = useAtomValue(searchingModelText);
const { deleteModel } = useDeleteModel();
const { downloadModel } = useDownloadModel();
const {
availableModels,
downloadedModels,
getAvailableModelExceptDownloaded,
} = useGetAvailableModels();
const onDeleteClick = async (product: Product) => {
await deleteModel(product);
await getAvailableModelExceptDownloaded();
};
const onDownloadClick = async (model: Product) => {
await downloadModel(model);
};
return (
<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
?.filter(
(e) =>
searchText.toLowerCase().trim() === "" ||
e.name.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => (
<DownloadedModelCard
key={item.id}
product={item}
onDeleteClick={onDeleteClick}
isRecommend={false}
/>
))}
</div>
<div className="pb-5 flex flex-col gap-2">
<Title title="Browse available models" />
{availableModels
?.filter(
(e) =>
searchText.toLowerCase().trim() === "" ||
e.name.toLowerCase().includes(searchText.toLowerCase())
)
.map((item) => (
<AvailableModelCard
key={item.id}
product={item}
onDownloadClick={onDownloadClick}
isRecommend={false}
/>
))}
</div>
</div>
);
};
type Props = {
title: string;
};
const Title: React.FC<Props> = ({ title }) => {
return (
<div className="flex gap-[10px]">
<span className="font-semibold text-xl leading-[25px] tracking-[-0.4px]">
{title}
</span>
</div>
);
};
export default ModelListContainer;

View File

@ -1,15 +0,0 @@
import HeaderBackButton from "../HeaderBackButton";
import HeaderTitle from "../HeaderTitle";
import ModelListContainer from "../ModelListContainer";
import ModelSearchBar from "../ModelSearchBar";
export default function ModelManagement() {
return (
<main className="pt-[30px] pr-[89px] pl-[60px] pb-[70px] flex-1">
{/* <HeaderBackButton /> */}
<HeaderTitle title="Explore Models" />
<ModelSearchBar />
<ModelListContainer />
</main>
);
}

View File

@ -1,17 +1,17 @@
import { Fragment, useEffect } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { Product } from "@/_models/Product";
import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
const SelectModels: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
const downloadedModels = useAtomValue(downloadedModelAtom);
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
useEffect(() => {

View File

@ -38,7 +38,7 @@ 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}`}
className={`rounded-[10px] py-0.5 px-2.5 w-fit text-xs font-medium ${statusType.backgroundColor}`}
>
{statusType.title}
</div>

View File

@ -2,63 +2,43 @@ import ProgressBar from "../ProgressBar";
import SystemItem from "../SystemItem";
import { useAtomValue } from "jotai";
import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
import { useEffect, useState } from "react";
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
import { SystemMonitoringService } from "../../../shared/coreService";
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useGetAppVersion from "@/_hooks/useGetAppVersion";
import useGetSystemResources from "@/_hooks/useGetSystemResources";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { DownloadState } from "@/_models/DownloadState";
import { formatDownloadPercentage } from "@/_utils/converter";
const MonitorBar: React.FC = () => {
const show = useAtomValue(getSystemBarVisibilityAtom);
const progress = useAtomValue(appDownloadProgress);
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 { version } = useGetAppVersion();
const { ram, cpu } = useGetSystemResources();
const modelDownloadStates = useAtomValue(modelDownloadStateAtom);
useEffect(() => {
const getSystemResources = async () => {
const resourceInfor = await executeSerial(
SystemMonitoringService.GET_RESOURCES_INFORMATION
);
const currentLoadInfor = await executeSerial(
SystemMonitoringService.GET_CURRENT_LOAD_INFORMATION
);
const ram =
(resourceInfor?.mem?.used ?? 0) / (resourceInfor?.mem?.total ?? 1);
setRam(Math.round(ram * 100));
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0));
};
const getAppVersion = () => {
window.electronAPI.appVersion().then((version: string | undefined) => {
setVersion(version ?? "");
});
};
getAppVersion();
getSystemResources();
// Fetch interval - every 3s
const intervalId = setInterval(() => {
getSystemResources();
}, 3000);
return () => clearInterval(intervalId);
}, []);
if (!show) return null;
const downloadStates: DownloadState[] = [];
for (const [, value] of Object.entries(modelDownloadStates)) {
downloadStates.push(value);
}
return (
<div className="flex flex-row items-center justify-between border-t border-gray-200">
{progress && progress >= 0 ? (
<ProgressBar total={100} used={progress} />
) : (
<div className="w-full" />
)}
<div className="flex-1 flex items-center gap-8 px-2">
) : null}
<div className="flex-1 justify-end flex items-center gap-8 px-2">
{downloadStates.length > 0 && (
<SystemItem
name="Downloading"
value={`${downloadStates[0].fileName}: ${formatDownloadPercentage(
downloadStates[0].percent
)}`}
/>
)}
<SystemItem name="CPU" value={`${cpu}%`} />
<SystemItem name="Mem" value={`${ram}%`} />
{activeModel && (
<SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
<SystemItem name={`Active model: ${activeModel.name}`} value={""} />
)}
<span className="text-gray-900 text-sm">v{version}</span>
</div>

View File

@ -4,7 +4,7 @@ import ActiveModelTable from "../ActiveModelTable";
import DownloadingModelTable from "../DownloadingModelTable";
const MyModelContainer: React.FC = () => (
<div className="flex flex-col w-full h-full pt-[60px]">
<div className="flex flex-col flex-1 pt-[60px]">
<HeaderTitle title="My Models" className="pl-[63px] pr-[89px]" />
<div className="pb-6 overflow-y-auto scroll">
<ActiveModelTable />

View File

@ -4,17 +4,19 @@ type Props = {
title: string;
onClick: () => void;
fullWidth?: boolean;
className?: string;
};
const PrimaryButton: React.FC<Props> = ({
title,
onClick,
fullWidth = false,
className,
}) => (
<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 line-clamp-1 flex-shrink-0 ${
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 ${className} ${
fullWidth ? "flex-1 " : ""
}}`}
>

View File

@ -5,24 +5,22 @@ type Props = {
used: number;
};
const ProgressBar: React.FC<Props> = ({ used, total }) => {
return (
<div className="flex gap-[10px] items-center p-[10px]">
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
Updating
</div>
<div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
<div
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
></div>
</div>
<div className="text-xs leading-[18px]">
{((used / total) * 100).toFixed(0)}%
</div>
const ProgressBar: React.FC<Props> = ({ used, total }) => (
<div className="flex gap-2.5 items-center p-[10px]">
<div className="text-xs leading-[18px] gap-0.5 flex items-center">
<Image src={"icons/app_icon.svg"} width={18} height={18} alt="" />
Updating
</div>
);
};
<div className="w-[150px] relative bg-blue-200 h-1 rounded-md flex">
<div
className="absolute top-0 left-0 h-full rounded-md bg-blue-600"
style={{ width: `${((used / total) * 100).toFixed(2)}%` }}
></div>
</div>
<div className="text-xs leading-[18px]">
{((used / total) * 100).toFixed(0)}%
</div>
</div>
);
export default ProgressBar;

View File

@ -1,9 +1,9 @@
import ChatContainer from "../ChatContainer";
import MainView from "../MainView";
import MonitorBar from "../MonitorBar";
const RightContainer = () => (
<div className="flex flex-col flex-1 h-screen">
<ChatContainer />
<MainView />
<MonitorBar />
</div>
);

View File

@ -1,7 +1,7 @@
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
export enum SearchType {
Model = "model",
@ -13,21 +13,12 @@ type Props = {
};
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);
};
const debounced = useDebouncedCallback((value) => {
setModelSearch(value);
}, 300);
return (
<div className="relative mt-3 flex items-center">
@ -43,10 +34,8 @@ const SearchBar: React.FC<Props> = ({ type, placeholder }) => {
type="text"
name="search"
id="search"
value={searchValue}
placeholder={placeholderText}
onChange={handleChange}
onKeyDown={handleKeyDown}
onChange={(e) => debounced(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"
/>
</div>

View File

@ -1,8 +1,8 @@
import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import { useAtom, useAtomValue } from "jotai";
import Image from "next/image";
const SendButton: React.FC = () => {
const [currentPrompt] = useAtom(currentPromptAtom);
@ -25,9 +25,9 @@ const SendButton: React.FC = () => {
onClick={sendChatMessage}
style={disabled ? disabledStyle : enabledStyle}
type="submit"
className="p-2 gap-[10px] inline-flex items-center rounded-[12px] text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
className="p-2 gap-2.5 inline-flex items-center rounded-xl text-sm font-semibold shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
<Image src={"icons/ic_arrowright.svg"} width={24} height={24} alt="" />
<ArrowRightIcon width={16} height={16} />
</button>
);
};

View File

@ -2,7 +2,6 @@ import Image from "next/image";
import useCreateConversation from "@/_hooks/useCreateConversation";
import PrimaryButton from "../PrimaryButton";
import { useAtomValue, useSetAtom } from "jotai";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { useEffect, useState } from "react";
import {
MainViewState,
@ -11,6 +10,7 @@ import {
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
enum ActionButton {
DownloadModel = "Download a Model",

View File

@ -2,7 +2,7 @@ import React from "react";
import Image from "next/image";
const SidebarHeader: React.FC = () => (
<div className="flex flex-col gap-[10px] px-3">
<div className="flex flex-col gap-2.5 px-3">
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
</div>
);

View File

@ -30,15 +30,15 @@ const SimpleControlNetMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
{displayDate(createdAt)}
</div>
</div>
<div className="flex gap-3 flex-col">
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{text}
</p>
<JanImage
@ -49,7 +49,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
<Link
href={imageUrls[0] || "#"}
target="_blank_"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
>
<Image src="icons/download.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">

View File

@ -30,10 +30,10 @@ const SimpleImageMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400 ml-2">
<div className="text-xs leading-[13.2px] font-medium text-gray-400 ml-2">
{displayDate(createdAt)}
</div>
</div>
@ -46,19 +46,19 @@ const SimpleImageMessage: React.FC<Props> = ({
<Link
href={imageUrls[0] || "#"}
target="_blank_"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
>
<Image src="icons/download.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">
<span className="leading-[20px] text-sm text-[#111928]">
Download
</span>
</Link>
<button
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-[12px]"
className="flex gap-1 items-center px-2 py-1 bg-[#F3F4F6] rounded-xl"
// onClick={() => sendChatMessage()}
>
<Image src="icons/refresh.svg" width={16} height={16} alt="" />
<span className="leading-[20px] text-[14px] text-[#111928]">
<span className="leading-[20px] text-sm text-[#111928]">
Re-generate
</span>
</button>

View File

@ -69,7 +69,7 @@ const SimpleTag: React.FC<Props> = ({
if (!clickable) {
return (
<div
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title}
</div>
@ -79,7 +79,7 @@ const SimpleTag: React.FC<Props> = ({
return (
<button
onClick={onClick}
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
className={`px-2.5 py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title} x
</button>

View File

@ -3,54 +3,61 @@ import { displayDate } from "@/_utils/datetime";
import { TextCode } from "../TextCode";
import { getMessageCode } from "@/_utils/message";
import Image from "next/image";
import { MessageSenderType } from "@/_models/ChatMessage";
type Props = {
avatarUrl: string;
senderName: string;
createdAt: number;
senderType: MessageSenderType;
text?: string;
};
const SimpleTextMessage: React.FC<Props> = ({
senderName,
createdAt,
senderType,
avatarUrl = "",
text = "",
}) => (
<div className="flex items-start gap-2 ml-3">
<Image
className="rounded-full"
src={avatarUrl}
width={32}
height={32}
alt=""
/>
<div className="flex flex-col gap-1 w-full">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{text.includes("```") ? (
getMessageCode(text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
}) => {
const backgroundColor =
senderType === MessageSenderType.User ? "" : "bg-gray-100";
return (
<div
className={`flex items-start gap-2 px-[148px] ${backgroundColor} py-5`}
>
<Image
className="rounded-full"
src={avatarUrl}
width={32}
height={32}
alt=""
/>
<div className="flex flex-col gap-1">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
))
) : (
<p
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
dangerouslySetInnerHTML={{ __html: text }}
/>
)}
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{text.includes("```") ? (
getMessageCode(text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
</div>
))
) : (
<span className="text-sm">{text}</span>
)}
</div>
</div>
</div>
);
);
};
export default React.memo(SimpleTextMessage);

View File

@ -34,17 +34,17 @@ const StreamTextMessage: React.FC<Props> = ({
/>
<div className="flex flex-col gap-1 w-full">
<div className="flex gap-1 justify-start items-baseline">
<div className="text-[#1B1B1B] text-[13px] font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
<div className="text-[#1B1B1B] text-sm font-extrabold leading-[15.2px] dark:text-[#d1d5db]">
{senderName}
</div>
<div className="text-[11px] leading-[13.2px] font-medium text-gray-400">
<div className="text-xs leading-[13.2px] font-medium text-gray-400">
{displayDate(createdAt)}
</div>
</div>
{message.text.includes("```") ? (
getMessageCode(message.text).map((item, i) => (
<div className="flex gap-1 flex-col" key={i}>
<p className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]">
<p className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]">
{item.text}
</p>
{item.code.trim().length > 0 && <TextCode text={item.code} />}
@ -52,7 +52,7 @@ const StreamTextMessage: React.FC<Props> = ({
))
) : (
<p
className="leading-[20px] whitespace-break-spaces text-[14px] font-normal dark:text-[#d1d5db]"
className="leading-[20px] whitespace-break-spaces text-sm font-normal dark:text-[#d1d5db]"
dangerouslySetInnerHTML={{ __html: message.text }}
/>
)}

View File

@ -3,15 +3,13 @@ type Props = {
value: string;
};
const SystemItem: React.FC<Props> = ({ name, value }) => {
return (
<div className="flex gap-2 pl-4 my-1">
<div className="flex gap-[10px] w-max font-bold text-gray-900 text-sm">
{name}
</div>
<span className="text-gray-900 text-sm">{value}</span>
const SystemItem: React.FC<Props> = ({ name, value }) => (
<div className="flex gap-2 pl-4 my-1">
<div className="flex gap-2.5 w-max font-bold text-gray-900 text-sm">
{name}
</div>
);
};
<span className="text-gray-900 text-sm">{value}</span>
</div>
);
export default SystemItem;

View File

@ -17,7 +17,7 @@ export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
];
return (
<div className="flex gap-[2px] rounded p-1 w-full bg-gray-200">
<div className="flex gap-0.5 rounded p-1 w-full bg-gray-200">
{btns.map((item, index) => (
<button
key={index}

View File

@ -19,7 +19,7 @@ const UserToolbar: React.FC = () => {
width={36}
height={36}
/>
<span className="flex gap-[2px] leading-6 text-base font-semibold">
<span className="flex gap-0.5 leading-6 text-base font-semibold">
{title}
</span>
</div>

View File

@ -9,7 +9,7 @@ const ViewModelDetailButton: React.FC<Props> = ({ callback }) => {
<div className="px-4 pb-4">
<button
onClick={callback}
className="bg-gray-100 py-1 px-[10px] w-full flex items-center justify-center gap-1 rounded-lg"
className="bg-gray-100 py-1 px-2.5 w-full flex items-center justify-center gap-1 rounded-lg"
>
<span className="text-xs leading-[18px]">View Details</span>
<ChevronDownIcon width={18} height={18} />

View File

@ -4,12 +4,14 @@ import { useSetAtom } from "jotai";
import { ReactNode, useEffect } from "react";
import { appDownloadProgress } from "./JotaiWrapper";
import { DownloadState } from "@/_models/DownloadState";
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "../../shared/coreService";
import {
setDownloadStateAtom,
setDownloadStateSuccessAtom,
} from "./atoms/DownloadState.atom";
import { getDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { downloadedModelAtom } from "./atoms/DownloadedModel.atom";
type Props = {
children: ReactNode;
@ -19,6 +21,7 @@ export default function EventListenerWrapper({ children }: Props) {
const setDownloadState = useSetAtom(setDownloadStateAtom);
const setDownloadStateSuccess = useSetAtom(setDownloadStateSuccessAtom);
const setProgress = useSetAtom(appDownloadProgress);
const setDownloadedModels = useSetAtom(downloadedModelAtom);
useEffect(() => {
if (window && window.electronAPI) {
@ -39,7 +42,15 @@ export default function EventListenerWrapper({ children }: Props) {
(_event: string, callback: any) => {
if (callback && callback.fileName) {
setDownloadStateSuccess(callback.fileName);
execute(DataService.UPDATE_FINISHED_DOWNLOAD, callback.fileName);
executeSerial(
DataService.UPDATE_FINISHED_DOWNLOAD,
callback.fileName
).then(() => {
getDownloadedModels().then((models) => {
setDownloadedModels(models);
});
});
}
}
);

View File

@ -0,0 +1,4 @@
import { Product } from "@/_models/Product";
import { atom } from "jotai";
export const downloadedModelAtom = atom<Product[]>([]);

View File

@ -1,11 +1,20 @@
import { execute, executeSerial } from "@/_services/pluginService";
import { DataService, ModelManagementService } from "../../shared/coreService";
import { Product } from "@/_models/Product";
import { useSetAtom } from "jotai";
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
import { getDownloadedModels } from "./useGetDownloadedModels";
export default function useDeleteModel() {
const setDownloadedModels = useSetAtom(downloadedModelAtom);
const deleteModel = async (model: Product) => {
execute(DataService.DELETE_DOWNLOAD_MODEL, model.id);
await executeSerial(ModelManagementService.DELETE_MODEL, model.fileName);
// reload models
const downloadedModels = await getDownloadedModels();
setDownloadedModels(downloadedModels);
};
return { deleteModel };

View File

@ -0,0 +1,17 @@
import { useEffect, useState } from "react";
export default function useGetAppVersion() {
const [version, setVersion] = useState<string>("");
useEffect(() => {
getAppVersion();
}, []);
const getAppVersion = () => {
window.electronAPI.appVersion().then((version: string | undefined) => {
setVersion(version ?? "");
});
};
return { version };
}

View File

@ -1,11 +1,13 @@
import { ModelVersion, Product, ProductType } from "@/_models/Product";
import { useEffect, useState } from "react";
import { useEffect } 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 { useAtom } from "jotai";
import { downloadedModelAtom } from "@/_helpers/atoms/DownloadedModel.atom";
export function useGetDownloadedModels() {
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
const [downloadedModels, setDownloadedModels] = useAtom(downloadedModelAtom);
useEffect(() => {
getDownloadedModels().then((downloadedModels) => {
@ -42,11 +44,12 @@ export async function searchHfModels(
const modelVersions: ModelVersion[] = [];
for (const [, file] of Object.entries(model.files)) {
const fileData: any = file as any;
const modelVersion: ModelVersion = {
path: file.path,
type: file.type,
downloadUrl: file.downloadLink,
size: file.size,
path: fileData.path,
type: fileData.type,
downloadUrl: fileData.downloadLink,
size: fileData.size,
};
modelVersions.push(modelVersion);
}

View File

@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { SystemMonitoringService } from "../../shared/coreService";
export default function useGetSystemResources() {
const [ram, setRam] = useState<number>(0);
const [cpu, setCPU] = useState<number>(0);
const getSystemResources = async () => {
const resourceInfor = await executeSerial(
SystemMonitoringService.GET_RESOURCES_INFORMATION
);
const currentLoadInfor = await executeSerial(
SystemMonitoringService.GET_CURRENT_LOAD_INFORMATION
);
const ram =
(resourceInfor?.mem?.used ?? 0) / (resourceInfor?.mem?.total ?? 1);
setRam(Math.round(ram * 100));
setCPU(Math.round(currentLoadInfor?.currentLoad ?? 0));
};
useEffect(() => {
getSystemResources();
// Fetch interval - every 3s
const intervalId = setInterval(() => {
getSystemResources();
}, 3000);
// clean up
return () => clearInterval(intervalId);
}, []);
return {
ram,
cpu,
};
}

View File

@ -1,24 +0,0 @@
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;
};

View File

@ -13,7 +13,7 @@ const Page: React.FC = () => {
<div className="relative flex flex-col text-black items-center h-screen overflow-y-scroll scroll pt-2">
<div className="absolute top-3 left-5">
<Link href="/" className="flex flex-row gap-2">
<div className="flex gap-[2px] items-center">
<div className="flex gap-0.5 items-center">
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
<Image src={"icons/Jan.svg"} width={27} height={12} alt="" />
</div>

View File

@ -13,7 +13,7 @@ const Page: React.FC = () => {
<div className="flex flex-col text-black items-center h-screen overflow-y-scroll scroll pt-2">
<div className="absolute top-3 left-5">
<Link href="/" className="flex flex-row gap-2">
<div className="flex gap-[2px] items-center">
<div className="flex gap-0.5 items-center">
<Image src={"icons/app_icon.svg"} width={28} height={28} alt="" />
<Image src={"icons/Jan.svg"} width={27} height={12} alt="" />
</div>

View File

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.92 11.6202C17.8724 11.4974 17.801 11.3853 17.71 11.2902L12.71 6.29019C12.6168 6.19695 12.5061 6.12299 12.3842 6.07253C12.2624 6.02207 12.1319 5.99609 12 5.99609C11.7337 5.99609 11.4783 6.10188 11.29 6.29019C11.1968 6.38342 11.1228 6.49411 11.0723 6.61594C11.0219 6.73776 10.9959 6.86833 10.9959 7.00019C10.9959 7.26649 11.1017 7.52188 11.29 7.71019L14.59 11.0002H7C6.73478 11.0002 6.48043 11.1055 6.29289 11.2931C6.10536 11.4806 6 11.735 6 12.0002C6 12.2654 6.10536 12.5198 6.29289 12.7073C6.48043 12.8948 6.73478 13.0002 7 13.0002H14.59L11.29 16.2902C11.1963 16.3831 11.1219 16.4937 11.0711 16.6156C11.0203 16.7375 10.9942 16.8682 10.9942 17.0002C10.9942 17.1322 11.0203 17.2629 11.0711 17.3848C11.1219 17.5066 11.1963 17.6172 11.29 17.7102C11.383 17.8039 11.4936 17.8783 11.6154 17.9291C11.7373 17.9798 11.868 18.006 12 18.006C12.132 18.006 12.2627 17.9798 12.3846 17.9291C12.5064 17.8783 12.617 17.8039 12.71 17.7102L17.71 12.7102C17.801 12.6151 17.8724 12.5029 17.92 12.3802C18.02 12.1367 18.02 11.8636 17.92 11.6202Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

1087
yarn.lock

File diff suppressed because it is too large Load Diff