Merge branch 'main' into feat_adr_002

This commit is contained in:
namvuong 2023-10-06 17:12:56 +07:00 committed by GitHub
commit d413d4a1dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 8552 additions and 2756 deletions

View File

@ -19,9 +19,14 @@ const dispose = async () =>
});
const inferenceUrl = () => "http://localhost:3928/llama/chat_completion";
const stopModel = () => {
window.electronAPI.invokePluginFunc(MODULE_PATH, "killSubprocess");
};
// Register all the above functions and objects with the relevant extension points
export function init({ register }) {
register("initModel", "initModel", initModel);
register("inferenceUrl", "inferenceUrl", inferenceUrl);
register("dispose", "dispose", dispose);
register("stopModel", "stopModel", stopModel);
}

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

@ -11,6 +11,7 @@
"electron-is-dev",
"node-llama-cpp"
],
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"electron-is-dev": "^2.0.0"

View File

@ -38,10 +38,20 @@ const deleteModel = async (path) =>
}
});
const searchModels = async (params) =>
new Promise(async (resolve) => {
if (window.electronAPI) {
window.electronAPI
.invokePluginFunc(MODULE_PATH, "searchModels", 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("searchModels", "searchModels", searchModels);
}

View File

@ -1,6 +1,10 @@
const path = require("path");
const { readdirSync, lstatSync } = require("fs");
const { app } = require("electron");
const { listModels, listFiles, fileDownloadInfo } = require("@huggingface/hub");
let modelsIterator = undefined;
let currentSearchOwner = undefined;
const ALL_MODELS = [
{
@ -87,6 +91,76 @@ function getDownloadedModels() {
return downloadedModels;
}
const getNextModels = async (count) => {
const models = [];
let hasMore = true;
while (models.length < count) {
const next = await modelsIterator.next();
// end if we reached the end
if (next.done) {
hasMore = false;
break;
}
const model = next.value;
const files = await listFilesByName(model.name);
models.push({
...model,
files,
});
}
const result = {
data: models,
hasMore,
};
return result;
};
const searchModels = async (params) => {
if (currentSearchOwner === params.search.owner && modelsIterator != null) {
// paginated search
console.debug(`Paginated search owner: ${params.search.owner}`);
const models = await getNextModels(params.limit);
return models;
} else {
// new search
console.debug(`Init new search owner: ${params.search.owner}`);
currentSearchOwner = params.search.owner;
modelsIterator = listModels({
search: params.search,
credentials: params.credentials,
});
const models = await getNextModels(params.limit);
return models;
}
};
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 +173,5 @@ function getAvailableModels() {
module.exports = {
getDownloadedModels,
getAvailableModels,
searchModels,
};

View File

@ -7,7 +7,14 @@
"": {
"name": "model-management-plugin",
"version": "1.0.0",
"bundleDependencies": [
"@huggingface/hub"
],
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@huggingface/hub": "^0.8.5"
},
"devDependencies": {
"cpx": "^1.5.0",
"rimraf": "^3.0.2",
@ -24,6 +31,18 @@
"node": ">=10.0.0"
}
},
"node_modules/@huggingface/hub": {
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/@huggingface/hub/-/hub-0.8.6.tgz",
"integrity": "sha512-V2f1+BiBd3PIRYkEjvjrqJuCWqZQniaHMYcNvwB+PcubvEECkmgBt3tvXMpNUK4M27YK1RjflQbCVXSZMuQeow==",
"inBundle": true,
"dependencies": {
"hash-wasm": "^4.9.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@ -1366,6 +1385,12 @@
"node": ">=0.10.0"
}
},
"node_modules/hash-wasm": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.9.0.tgz",
"integrity": "sha512-7SW7ejyfnRxuOc7ptQHSf4LDoZaWOivfzqw+5rpcQku0nHfmicPKE51ra9BiRLAmT8+gGLestr1XroUkqdjL6w==",
"inBundle": true
},
"node_modules/import-local": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",

View File

@ -24,5 +24,11 @@
"dist/*",
"package.json",
"README.md"
],
"dependencies": {
"@huggingface/hub": "^0.8.5"
},
"bundledDependencies": [
"@huggingface/hub"
]
}

View File

@ -10,6 +10,7 @@
"bundleDependencies": [
"systeminformation"
],
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"systeminformation": "^5.21.8"

View File

@ -147,6 +147,18 @@ function handleIPCs() {
ipcMain.handle("relaunch", async (_event, url) => {
dispose(requiredModules);
app.relaunch();
app.exit();
});
ipcMain.handle("reloadPlugins", async (_event, url) => {
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.log(err);
app.relaunch();
app.exit();
});
});
/**

View File

@ -51,11 +51,14 @@
},
"dependencies": {
"@npmcli/arborist": "^7.1.0",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.4",
"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",

View File

@ -13,6 +13,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
pluginPath: () => ipcRenderer.invoke("pluginPath"),
reloadPlugins: () => ipcRenderer.invoke("reloadPlugins"),
appVersion: () => ipcRenderer.invoke("appVersion"),
openExternalUrl: (url: string) => ipcRenderer.invoke("openExternalUrl", url),

1614
node_modules/.yarn-integrity generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -9,10 +9,10 @@ const ActiveModelTable: React.FC = () => {
if (!activeModel) return null;
return (
<Fragment>
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
<ModelTable models={[activeModel]} />
</Fragment>
</div>
);
};

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

@ -1,13 +1,11 @@
"use client";
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useRef, useState, useEffect } from "react";
import ChatItem from "../ChatItem";
import { ChatMessage } from "@/_models/ChatMessage";
import useChatMessages from "@/_hooks/useChatMessages";
import { showingTyping } from "@/_helpers/JotaiWrapper";
import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import LoadingIndicator from "../LoadingIndicator";
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
@ -16,12 +14,11 @@ const ChatBody: React.FC = () => {
const messageList = useAtomValue(
selectAtom(
chatMessages,
useCallback((v) => v[activeConversationId], [activeConversationId])
)
useCallback((v) => v[activeConversationId], [activeConversationId]),
),
);
const [content, setContent] = useState<React.JSX.Element[]>([]);
const isTyping = useAtomValue(showingTyping);
const [offset, setOffset] = useState(0);
const { loading, hasMore } = useChatMessages(offset);
const intersectObs = useRef<any>(null);
@ -40,10 +37,10 @@ const ChatBody: React.FC = () => {
if (message) intersectObs.current.observe(message);
},
[loading, hasMore]
[loading, hasMore],
);
React.useEffect(() => {
useEffect(() => {
const list = messageList?.map((message, index) => {
if (messageList?.length === index + 1) {
return (
@ -58,11 +55,6 @@ const ChatBody: React.FC = () => {
return (
<div className="flex flex-col-reverse flex-1 py-4 overflow-y-auto scroll">
{isTyping && (
<div className="ml-4 mb-2" key="indicator">
<LoadingIndicator />
</div>
)}
{content}
</div>
);

View File

@ -2,22 +2,17 @@ import SimpleControlNetMessage from "../SimpleControlNetMessage";
import SimpleImageMessage from "../SimpleImageMessage";
import SimpleTextMessage from "../SimpleTextMessage";
import { ChatMessage, MessageType } from "@/_models/ChatMessage";
import StreamTextMessage from "../StreamTextMessage";
import { useAtomValue } from "jotai";
import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
export default function renderChatMessage({
id,
messageType,
messageSenderType,
senderAvatarUrl,
senderName,
createdAt,
imageUrls,
htmlText,
text,
}: ChatMessage): React.ReactNode {
// eslint-disable-next-line react-hooks/rules-of-hooks
const message = useAtomValue(currentStreamingMessageAtom);
switch (messageType) {
case MessageType.ImageWithText:
return (
@ -42,22 +37,14 @@ export default function renderChatMessage({
/>
);
case MessageType.Text:
return id !== message?.id ? (
return (
<SimpleTextMessage
key={id}
avatarUrl={senderAvatarUrl}
senderName={senderName}
createdAt={createdAt}
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
/>
) : (
<StreamTextMessage
key={id}
id={id}
avatarUrl={senderAvatarUrl}
senderName={senderName}
createdAt={createdAt}
text={htmlText && htmlText.trim().length > 0 ? htmlText : text}
senderType={messageSenderType}
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,14 +6,16 @@ import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
if (!downloadedModels || downloadedModels.length === 0) return null;
return (
<Fragment>
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
<div className="py-5 w-[568px]">
<SearchBar />
</div>
<ModelTable models={downloadedModels} />
</Fragment>
</div>
);
};

View File

@ -0,0 +1,29 @@
import React, { Fragment } from "react";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
import { useAtomValue } from "jotai";
import ModelDownloadingTable from "../ModelDownloadingTable";
import { DownloadState } from "@/_models/DownloadState";
const DownloadingModelTable: React.FC = () => {
const modelDownloadState = useAtomValue(modelDownloadStateAtom);
const isAnyModelDownloading = Object.values(modelDownloadState).length > 0;
if (!isAnyModelDownloading) return null;
const downloadStates: DownloadState[] = [];
for (const [, value] of Object.entries(modelDownloadState)) {
downloadStates.push(value);
}
return (
<div className="pl-[63px] pr-[89px]">
<h3 className="text-xl leading-[25px] mt-[50px] mb-4">
Downloading Models
</h3>
<ModelDownloadingTable downloadStates={downloadStates} />
</div>
);
};
export default DownloadingModelTable;

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,55 +1,20 @@
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";
import SearchBar, { SearchType } from "../SearchBar";
import ExploreModelList from "../ExploreModelList";
import ExploreModelFilter from "../ExploreModelFilter";
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>
const ExploreModelContainer: React.FC = () => (
<div className="flex flex-col flex-1 px-16 pt-14 overflow-hidden">
<HeaderTitle title="Explore Models" />
<SearchBar
type={SearchType.Model}
placeholder="Owner name like TheBloke, bhlim etc.."
/>
<div className="flex flex-1 gap-x-10 mt-9 overflow-hidden">
<ExploreModelFilter />
<ExploreModelList />
</div>
);
};
</div>
);
export default ExploreModelContainer;

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,36 +1,30 @@
/* eslint-disable react/display-name */
"use client";
import ExploreModelItemHeader from "../ExploreModelItemHeader";
import ModelVersionList from "../ModelVersionList";
import { useMemo, useState } from "react";
import { Product } from "@/_models/Product";
import { Fragment, forwardRef, 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;
};
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 ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
const [show, setShow] = useState(false);
return (
<div className="flex flex-col border border-gray-200 rounded-[5px]">
<div
ref={ref}
className="flex flex-col border border-gray-200 rounded-md mb-4"
>
<ExploreModelItemHeader
name={model.name}
status={TagType.Recommended}
total={model.totalSize}
downloadState={downloadState}
onDownloadClick={() => downloadModel(model)}
versions={model.availableVersions}
/>
<div className="flex flex-col px-[26px] py-[22px]">
<div className="flex justify-between">
@ -39,7 +33,7 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<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>
@ -87,15 +81,24 @@ const ExploreModelItem: React.FC<Props> = ({ model }) => {
<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>
{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>
);
};
});
export default ExploreModelItem;

View File

@ -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?.()}
/>
)}
</div>
);
);
} 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;

View File

@ -0,0 +1,52 @@
import React, { useEffect } from "react";
import ExploreModelItem from "../ExploreModelItem";
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import useGetHuggingFaceModel from "@/_hooks/useGetHuggingFaceModel";
import { useAtom, useAtomValue } from "jotai";
import { useInView } from "react-intersection-observer";
import { modelLoadMoreAtom } from "@/_helpers/atoms/ExploreModelLoading.atom";
import { Waveform } from "@uiball/loaders";
const ExploreModelList: React.FC = () => {
const [loadMoreInProgress, setLoadMoreInProress] = useAtom(modelLoadMoreAtom);
const modelSearch = useAtomValue(modelSearchAtom);
const { modelList, getHuggingFaceModel } = useGetHuggingFaceModel();
const { ref, inView } = useInView({
threshold: 0,
triggerOnce: true,
});
useEffect(() => {
if (modelList.length === 0 && modelSearch.length > 0) {
setLoadMoreInProress(true);
}
getHuggingFaceModel(modelSearch);
}, [modelSearch]);
useEffect(() => {
if (inView) {
console.debug("Load more models..");
setLoadMoreInProress(true);
getHuggingFaceModel(modelSearch);
}
}, [inView]);
return (
<div className="flex flex-col flex-1 overflow-y-auto scroll">
{modelList.map((item, index) => (
<ExploreModelItem
ref={index === modelList.length - 1 ? ref : null}
key={item.id}
model={item}
/>
))}
{loadMoreInProgress && (
<div className="mx-auto mt-2 mb-4">
<Waveform size={24} color="#9CA3AF" />
</div>
)}
</div>
);
};
export default ExploreModelList;

View File

@ -1,11 +1,14 @@
import React from 'react';
import React from "react";
type Props = {
title: string;
className?: string;
};
const HeaderTitle: React.FC<Props> = ({ title }) => (
<h2 className="my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px]">
const HeaderTitle: React.FC<Props> = ({ title, className }) => (
<h2
className={`my-5 font-semibold text-[34px] tracking-[-0.4px] leading-[41px] ${className}`}
>
{title}
</h2>
);

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

@ -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

View File

@ -4,19 +4,56 @@ import BasicPromptInput from "../BasicPromptInput";
import BasicPromptAccessories from "../BasicPromptAccessories";
import { useAtomValue } from "jotai";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
import SecondaryButton from "../SecondaryButton";
import { Fragment } from "react";
import { PlusIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import { showingTyping } from "@/_helpers/JotaiWrapper";
import LoadingIndicator from "../LoadingIndicator";
const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const isTyping = useAtomValue(showingTyping);
if (showingAdvancedPrompt) {
return <div />;
}
// TODO: implement regenerate
// const onRegenerateClick = () => {};
const onNewConversationClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return (
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
<BasicPromptInput />
<BasicPromptAccessories />
</div>
<Fragment>
<div className="flex justify-between gap-2 mr-3 my-2">
<div className="h-6">
{isTyping && (
<div className="my-2" key="indicator">
<LoadingIndicator />
</div>
)}{" "}
</div>
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
<SecondaryButton
onClick={onNewConversationClick}
title="New Conversation"
icon={<PlusIcon width={16} height={16} />}
/>
</div>
<div className="mx-3 mb-3 flex-none overflow-hidden shadow-sm ring-1 ring-inset ring-gray-300 rounded-lg dark:bg-gray-800">
<BasicPromptInput />
<BasicPromptAccessories />
</div>
</Fragment>
);
};

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

@ -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 />

View File

@ -1,12 +1,6 @@
const LoadingIndicator = () => {
let circleCommonClasses = "h-1.5 w-1.5 bg-current rounded-full";
return (
// <div className="flex">
// <div className={`${circleCommonClasses} mr-1 animate-bounce`}></div>
// <div className={`${circleCommonClasses} mr-1 animate-bounce200`}></div>
// <div className={`${circleCommonClasses} animate-bounce400`}></div>
// </div>
<div className="typingIndicatorContainer">
<div className="typingIndicatorBubble">
<div className="typingIndicatorBubbleDot"></div>

View File

@ -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) {
const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom);
switch (viewState) {
case MainViewState.ConversationEmptyModel:
return <EmptyChatContainer />
return <EmptyChatContainer />;
case MainViewState.ExploreModel:
return <ExploreModelContainer />;
case MainViewState.Setting:
@ -32,6 +28,8 @@ export default function ChatContainer({ children }: Props) {
case MainViewState.Welcome:
return <Welcome />;
default:
return <div className="flex flex-1 overflow-hidden">{children}</div>;
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 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

@ -0,0 +1,36 @@
import React from "react";
import { DownloadState } from "@/_models/DownloadState";
import {
formatDownloadPercentage,
formatDownloadSpeed,
toGigabytes,
} from "@/_utils/converter";
type Props = {
downloadState: DownloadState;
};
const ModelDownloadingRow: React.FC<Props> = ({ downloadState }) => (
<tr
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
key={downloadState.fileName}
>
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
{downloadState.fileName}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{toGigabytes(downloadState.size.transferred)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{toGigabytes(downloadState.size.total)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{formatDownloadPercentage(downloadState.percent)}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{formatDownloadSpeed(downloadState.speed)}
</td>
</tr>
);
export default ModelDownloadingRow;

View File

@ -0,0 +1,34 @@
import React from "react";
import ModelTableHeader from "../ModelTableHeader";
import { DownloadState } from "@/_models/DownloadState";
import ModelDownloadingRow from "../ModelDownloadingRow";
type Props = {
downloadStates: DownloadState[];
};
const tableHeaders = ["MODEL", "TRANSFERRED", "SIZE", "PERCENTAGE", "SPEED"];
const ModelDownloadingTable: React.FC<Props> = ({ downloadStates }) => (
<div className="flow-root 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>
{downloadStates.map((state) => (
<ModelDownloadingRow key={state.fileName} downloadState={state} />
))}
</tbody>
</table>
</div>
);
export default React.memo(ModelDownloadingTable);

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,29 +1,16 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation";
import { useSetAtom } from "jotai";
import { TrashIcon } from "@heroicons/react/24/outline";
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ModelMenu: React.FC = () => {
const currentProduct = useAtomValue(currentProductAtom);
const { requestCreateConvo } = useCreateConversation();
const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom
);
const onCreateConvoClick = () => {
if (currentProduct) {
requestCreateConvo(currentProduct);
}
};
return (
<div className="flex items-center gap-3">
<button onClick={() => onCreateConvoClick()}>
<PlusIcon width={24} height={24} color="#9CA3AF" />
</button>
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
<TrashIcon width={24} height={24} color="#9CA3AF" />
</button>

View File

@ -14,7 +14,7 @@ type Props = {
};
const ModelRow: React.FC<Props> = ({ model }) => {
const { startModel } = useStartStopModel();
const { startModel, stopModel } = useStartStopModel();
const activeModel = useAtomValue(currentProductAtom);
const { deleteModel } = useDeleteModel();
@ -31,6 +31,8 @@ const ModelRow: React.FC<Props> = ({ model }) => {
const onModelActionClick = (action: ModelActionType) => {
if (action === ModelActionType.Start) {
startModel(model.id);
} else {
stopModel(model.id);
}
};

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

@ -10,7 +10,7 @@ type Props = {
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">
<div className="flow-root 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">

View File

@ -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">
<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)}
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">{modelVersion.path}</span>
</div>
<div className="flex items-center gap-4">
<div className="px-2.5 py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(modelVersion.size)}
</div>
{downloadButton}
</div>
<button className="text-indigo-600 text-sm font-medium">Download</button>
</div>
</div>
);
);
};
export default ModelVersionItem;

View File

@ -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,
},
];
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>
);
type Props = {
model: Product;
versions: ModelVersion[];
};
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="border border-gray-200 rounded-lg overflow-hidden">
{versions.map((item) => (
<ModelVersionItem key={item.path} model={model} modelVersion={item} />
))}
</div>
</div>
);
export default ModelVersionList;

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

@ -1,12 +1,16 @@
import HeaderTitle from "../HeaderTitle";
import DownloadedModelTable from "../DownloadedModelTable";
import ActiveModelTable from "../ActiveModelTable";
import DownloadingModelTable from "../DownloadingModelTable";
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 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 />
<DownloadingModelTable />
<DownloadedModelTable />
</div>
</div>
);

View File

@ -11,6 +11,7 @@ import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
import { PlusIcon } from "@heroicons/react/24/outline";
const NewChatButton: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom);
@ -32,8 +33,13 @@ 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"
icon={<PlusIcon width={16} height={16} />}
/>
);
};
export default NewChatButton;
export default React.memo(NewChatButton);

View File

@ -172,15 +172,30 @@ export const Preferences = () => {
/>
</label>
</div>
<button
type="submit"
className={classNames(
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
fileName ? "bg-indigo-600 hover:bg-indigo-500" : "bg-gray-500"
)}
>
Install Plugin
</button>
<div className="flex flex-col space-y-2">
<button
type="submit"
className={classNames(
"rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
fileName
? "bg-blue-500 hover:bg-blue-300"
: "bg-gray-500"
)}
>
Install Plugin
</button>
<button
className={classNames(
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
)}
onClick={() => {
window.electronAPI.reloadPlugins();
}}
>
Reload Plugins
</button>
</div>
</div>
</form>

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 ${
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,12 +1,9 @@
import ChatContainer from "../ChatContainer";
import MainChat from "../MainChat";
import MainView from "../MainView";
import MonitorBar from "../MonitorBar";
const RightContainer = () => (
<div className="flex flex-col flex-1 h-screen">
<ChatContainer>
<MainChat />
</ChatContainer>
<MainView />
<MonitorBar />
</div>
);

View File

@ -1,14 +1,25 @@
import { searchAtom } from "@/_helpers/JotaiWrapper";
import { modelSearchAtom } from "@/_helpers/JotaiWrapper";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { useSetAtom } from "jotai";
import { useDebouncedCallback } from "use-debounce";
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 setModelSearch = useSetAtom(modelSearchAtom);
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
const debounced = useDebouncedCallback((value) => {
setModelSearch(value);
}, 300);
return (
<div className="relative mt-3 flex items-center">
<div className="absolute top-0 left-2 h-full flex items-center">
@ -24,7 +35,7 @@ const SearchBar: React.FC<Props> = ({ placeholder }) => {
name="search"
id="search"
placeholder={placeholderText}
onChange={(e) => setText(e.target.value)}
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,11 @@
import React from "react";
type Props = {
title: string;
onClick: () => void;
onClick?: () => void;
disabled?: boolean;
className?: string;
icon?: React.ReactNode;
};
const SecondaryButton: React.FC<Props> = ({
@ -10,15 +13,17 @@ const SecondaryButton: React.FC<Props> = ({
onClick,
disabled,
className,
icon,
}) => (
<button
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={`flex items-center justify-center gap-1 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`}
>
{icon}
{title}
</button>
);
export default SecondaryButton;
export default React.memo(SecondaryButton);

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 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={() =>
@ -11,7 +11,7 @@ const SidebarFooter: React.FC = () => (
className="flex-1"
/>
<SecondaryButton
title={"Discord"}
title={"Twitter"}
onClick={() =>
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
}

View File

@ -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]">
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
</div>
);
};
const SidebarHeader: React.FC = () => (
<div className="flex flex-col gap-2.5 px-3">
<Image src={"icons/Jan_AppIcon.svg"} width={68} height={28} alt="" />
</div>
);
export default SidebarHeader;
export default React.memo(SidebarHeader);

View File

@ -21,21 +21,16 @@ const menu = [
];
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>
<ul role="list" className="mx-1 mt-2 space-y-1 mb-2">
{menu.map((item) => (
<SidebarMenuItem
title={item.name}
viewState={item.state}
iconName={item.icon}
key={item.name}
/>
))}
</ul>
);
export default React.memo(SidebarMenu);

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

@ -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>("");

View File

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

View File

@ -0,0 +1,3 @@
import { atom } from "jotai";
export const modelLoadMoreAtom = atom<boolean>(false);

View File

@ -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(),
};

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

@ -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,
};
}

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,10 +1,13 @@
import { Product } from "@/_models/Product";
import { useEffect, useState } from "react";
import { ModelVersion, Product, ProductType } from "@/_models/Product";
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) => {
@ -28,3 +31,63 @@ export async function getModelFiles(): Promise<Product[]> {
);
return downloadedModels ?? [];
}
export async function searchModels(
params: SearchModelParamHf
): Promise<QueryProductResult> {
const result = await executeSerial(
ModelManagementService.SEARCH_MODELS,
params
);
const products: Product[] = result.data.map((model: any) => {
const modelVersions: ModelVersion[] = [];
for (const [, file] of Object.entries(model.files)) {
const fileData: any = file as any;
const modelVersion: ModelVersion = {
path: fileData.path,
type: fileData.type,
downloadUrl: fileData.downloadLink,
size: fileData.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 {
data: products,
hasMore: result.hasMore,
};
}
// TODO define somewhere else
export type QueryProductResult = {
data: Product[];
hasMore: boolean;
};

View File

@ -0,0 +1,37 @@
import { useState } from "react";
import { searchModels } from "./useGetDownloadedModels";
import { SearchModelParamHf } from "@/_models/hf/SearchModelParam.hf";
import { Product } from "@/_models/Product";
import { useSetAtom } from "jotai";
import { modelLoadMoreAtom } from "@/_helpers/atoms/ExploreModelLoading.atom";
export default function useGetHuggingFaceModel() {
const setLoadMoreInProgress = useSetAtom(modelLoadMoreAtom);
const [modelList, setModelList] = useState<Product[]>([]);
const [currentOwner, setCurrentOwner] = useState<string | undefined>(
undefined
);
const getHuggingFaceModel = async (owner?: string) => {
if (!owner) {
setModelList([]);
return;
}
const searchParams: SearchModelParamHf = {
search: { owner },
limit: 5,
};
const result = await searchModels(searchParams);
console.debug("result", JSON.stringify(result));
if (owner !== currentOwner) {
setModelList(result.data);
setCurrentOwner(owner);
} else {
setModelList([...modelList, ...result.data]);
}
setLoadMoreInProgress(false);
};
return { modelList, getHuggingFaceModel };
}

View File

@ -1,9 +0,0 @@
import React, { useState } from "react";
export default function useGetModels() {
const [models, setModels] = useState<any[]>()
return {
models
};
}

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,9 +1,12 @@
import { executeSerial } from "@/_services/pluginService";
import { DataService } from "../../shared/coreService";
import { DataService, InfereceService } from "../../shared/coreService";
import useInitModel from "./useInitModel";
import { useSetAtom } from "jotai";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
export default function useStartStopModel() {
const { initModel } = useInitModel();
const setActiveModel = useSetAtom(currentProductAtom);
const startModel = async (modelId: string) => {
const model = await executeSerial(DataService.GET_MODEL_BY_ID, modelId);
@ -14,7 +17,10 @@ export default function useStartStopModel() {
}
};
const stopModel = async (modelId: string) => {};
const stopModel = async (modelId: string) => {
await executeSerial(InfereceService.STOP_MODEL, modelId);
setActiveModel(undefined);
};
return { startModel, stopModel };
}

View File

@ -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;
}

View 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";

View File

@ -13,3 +13,8 @@ export const toGigabytes = (input: number) => {
export const formatDownloadPercentage = (input: number) => {
return (input * 100).toFixed(2) + "%";
};
export const formatDownloadSpeed = (input: number | undefined) => {
if (!input) return "0B/s";
return toGigabytes(input) + "/s";
};

View File

@ -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, {

View File

@ -386,7 +386,7 @@ body {
align-items: center;
justify-content: center;
width: 52px;
height: 34px;
height: 28px;
margin: 0px 8px;
background-color: #f0f1f1;
border-radius: 12px;

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="M9 5H7C5.89543 5 5 5.89543 5 7V19C5 20.1046 5.89543 21 7 21H17C18.1046 21 19 20.1046 19 19V7C19 5.89543 18.1046 5 17 5H15M9 5C9 6.10457 9.89543 7 11 7H13C14.1046 7 15 6.10457 15 5M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5M12 12H15M12 16H15M9 12H9.01M9 16H9.01" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 448 B

View File

@ -1,3 +0,0 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 0C8.05228 0 8.5 0.447715 8.5 1V6H13.5C14.0523 6 14.5 6.44772 14.5 7C14.5 7.55228 14.0523 8 13.5 8H8.5V13C8.5 13.5523 8.05228 14 7.5 14C6.94772 14 6.5 13.5523 6.5 13V8H1.5C0.947715 8 0.5 7.55228 0.5 7C0.5 6.44771 0.947715 6 1.5 6L6.5 6V1C6.5 0.447715 6.94772 0 7.5 0Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 440 B

View File

@ -1,10 +0,0 @@
<svg width="26" height="27" viewBox="0 0 26 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_680_4355)">
<path d="M23.6078 20.7621C23.2146 21.6705 22.7492 22.5067 22.2099 23.2754C21.4749 24.3234 20.8731 25.0488 20.4093 25.4516C19.6903 26.1128 18.9199 26.4515 18.095 26.4707C17.5028 26.4707 16.7886 26.3022 15.9573 25.9604C15.1232 25.6201 14.3567 25.4516 13.6559 25.4516C12.9209 25.4516 12.1325 25.6201 11.2893 25.9604C10.4448 26.3022 9.76452 26.4804 9.24438 26.498C8.45333 26.5317 7.66485 26.1835 6.87781 25.4516C6.37548 25.0135 5.74717 24.2624 4.99448 23.1984C4.1869 22.0621 3.52295 20.7445 3.00281 19.2423C2.44575 17.6198 2.1665 16.0486 2.1665 14.5275C2.1665 12.785 2.54301 11.2822 3.29715 10.0229C3.88983 9.0113 4.67831 8.21335 5.66516 7.62756C6.652 7.04178 7.71829 6.74327 8.86659 6.72417C9.4949 6.72417 10.3188 6.91852 11.3428 7.30049C12.3638 7.68374 13.0194 7.87809 13.3068 7.87809C13.5217 7.87809 14.25 7.65083 15.4847 7.19777C16.6522 6.77761 17.6376 6.60364 18.4449 6.67217C20.6323 6.84871 22.2757 7.71102 23.3687 9.26455C21.4123 10.4499 20.4446 12.1102 20.4638 14.24C20.4815 15.899 21.0833 17.2795 22.2661 18.3757C22.8021 18.8844 23.4008 19.2776 24.0668 19.5569C23.9224 19.9757 23.7699 20.377 23.6078 20.7621ZM18.5909 1.02039C18.5909 2.32069 18.1159 3.53477 17.169 4.65852C16.0263 5.99443 14.6442 6.76638 13.1454 6.64457C13.1263 6.48857 13.1152 6.32439 13.1152 6.15187C13.1152 4.90358 13.6586 3.56767 14.6236 2.47539C15.1054 1.92234 15.7182 1.46249 16.4612 1.09566C17.2027 0.734304 17.904 0.534466 18.5636 0.500244C18.5829 0.674074 18.5909 0.847914 18.5909 1.02038V1.02039Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_680_4355">
<rect width="26" height="26" fill="white" transform="translate(0 0.5)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.99998 14.4001C9.69736 14.4001 11.3252 13.7258 12.5255 12.5256C13.7257 11.3253 14.4 9.69748 14.4 8.0001C14.4 6.30271 13.7257 4.67485 12.5255 3.47461C11.3252 2.27438 9.69736 1.6001 7.99998 1.6001C6.30259 1.6001 4.67472 2.27438 3.47449 3.47461C2.27426 4.67485 1.59998 6.30271 1.59998 8.0001C1.59998 9.69748 2.27426 11.3253 3.47449 12.5256C4.67472 13.7258 6.30259 14.4001 7.99998 14.4001ZM8.79998 5.6001C8.79998 5.38792 8.71569 5.18444 8.56566 5.03441C8.41563 4.88438 8.21215 4.8001 7.99998 4.8001C7.7878 4.8001 7.58432 4.88438 7.43429 5.03441C7.28426 5.18444 7.19998 5.38792 7.19998 5.6001V8.4689L6.16558 7.4345C6.01469 7.28877 5.81261 7.20814 5.60285 7.20996C5.3931 7.21178 5.19245 7.29592 5.04412 7.44424C4.89579 7.59257 4.81166 7.79322 4.80984 8.00298C4.80801 8.21273 4.88865 8.41482 5.03438 8.5657L7.43438 10.9657C7.5844 11.1157 7.78784 11.1999 7.99998 11.1999C8.21211 11.1999 8.41555 11.1157 8.56558 10.9657L10.9656 8.5657C11.1113 8.41482 11.1919 8.21273 11.1901 8.00298C11.1883 7.79322 11.1042 7.59257 10.9558 7.44424C10.8075 7.29592 10.6069 7.21178 10.3971 7.20996C10.1873 7.20814 9.98526 7.28877 9.83438 7.4345L8.79998 8.4689V5.6001Z" fill="#1C64F2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0002 5.41007L6.71019 1.17007C6.61723 1.07634 6.50663 1.00194 6.38477 0.951175C6.26291 0.900406 6.1322 0.874268 6.00019 0.874268C5.86818 0.874268 5.73747 0.900406 5.61562 0.951175C5.49376 1.00194 5.38316 1.07634 5.29019 1.17007L1.05019 5.41007C0.956464 5.50303 0.88207 5.61363 0.831301 5.73549C0.780533 5.85735 0.754395 5.98805 0.754395 6.12007C0.754395 6.25208 0.780533 6.38278 0.831301 6.50464C0.88207 6.6265 0.956464 6.7371 1.05019 6.83007C1.23756 7.01632 1.49101 7.12086 1.75519 7.12086C2.01938 7.12086 2.27283 7.01632 2.46019 6.83007L6.00019 3.29007L9.54019 6.83007C9.72645 7.01481 9.97785 7.11896 10.2402 7.12007C10.3718 7.12083 10.5023 7.0956 10.6241 7.04584C10.7459 6.99607 10.8568 6.92275 10.9502 6.83007C11.0473 6.74045 11.1256 6.63248 11.1807 6.51241C11.2358 6.39233 11.2666 6.26253 11.2713 6.13049C11.2759 5.99846 11.2543 5.86681 11.2078 5.74315C11.1613 5.6195 11.0907 5.50629 11.0002 5.41007Z" fill="#6B7280"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +0,0 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M9.78088 18C9.57895 17.9992 9.38536 17.9194 9.24156 17.7776C9.09776 17.6359 9.01517 17.4434 9.01149 17.2415V14.5044C9.01098 14.3409 8.94574 14.1842 8.83004 14.0686C8.71434 13.953 8.55759 13.8879 8.39405 13.8876H6.94566C6.28372 13.8893 5.63579 13.6969 5.08199 13.3343C4.52819 12.9717 4.09278 12.4548 3.82961 11.8474C1.87535 11.4185 -0.244978 9.81366 0.0230285 6.62836C0.044828 6.39562 0.393621 3.53796 3.85782 2.97758C4.16494 2.15369 4.79713 1.40673 5.65308 0.861743C6.51583 0.308884 7.51687 0.0102382 8.54152 2.01591e-05C8.83818 -0.000876215 9.13419 0.0281226 9.42504 0.0865771C10.7374 0.306536 11.9155 1.02134 12.7168 2.0838C13.8531 2.15782 14.9183 2.66303 15.6945 3.49616C16.4708 4.32929 16.8996 5.42744 16.8933 6.56617C17.7711 7.39968 18.5199 8.8782 18.2699 10.3958C18.1263 11.2691 17.587 12.4668 15.8335 13.2125C15.2979 13.4546 14.717 13.5798 14.1292 13.5798C13.2927 13.5853 12.4776 13.3149 11.8102 12.8105C11.1665 12.3133 10.7199 11.604 10.5496 10.8087C10.0629 10.7304 9.60663 10.5214 9.22946 10.204C8.8523 9.88653 8.56842 9.47262 8.40815 9.00643L8.40495 8.99553V8.98463C8.36063 8.79081 8.39307 8.58737 8.49545 8.41693C8.59784 8.2465 8.76222 8.12232 8.95415 8.07042C9.14608 8.01852 9.35064 8.04294 9.52497 8.13854C9.69929 8.23415 9.82984 8.39353 9.88924 8.58327C10.0566 9.03208 10.5304 9.32701 11.1036 9.32701C11.4313 9.33238 11.7507 9.22306 12.0064 9.01797L12.0121 9.01284C12.1956 8.86778 12.339 8.67829 12.4288 8.4623C12.5185 8.24631 12.5517 8.01098 12.5251 7.77861V7.76386V7.75809C12.5136 7.55605 12.5822 7.35763 12.716 7.20582C12.8498 7.05401 13.0381 6.96103 13.24 6.94702C13.2579 6.94702 13.2759 6.94702 13.2938 6.94702C13.489 6.94655 13.677 7.02027 13.8199 7.15324C13.9627 7.28621 14.0497 7.4685 14.0632 7.6632C14.1209 8.31185 13.9608 8.96124 13.6083 9.5088C13.2558 10.0564 12.731 10.4709 12.1166 10.6869C12.4321 11.6762 13.3509 12.0334 14.144 12.0334C14.5276 12.0352 14.9069 11.9519 15.2545 11.7897L15.2647 11.7852L15.2756 11.7814C16.0072 11.5102 16.5125 10.9895 16.6984 10.315C16.8541 9.70591 16.7677 9.06015 16.4573 8.51338C16.2464 8.96219 15.9341 9.49885 15.384 9.56232H15.3673H15.3513C15.2055 9.56043 15.0632 9.51731 14.9408 9.43794C14.8185 9.35857 14.7211 9.2462 14.66 9.11382C14.5988 8.98144 14.5764 8.83445 14.5952 8.68984C14.6141 8.54524 14.6735 8.40893 14.7666 8.29667C15.9534 6.90342 14.9942 5.11073 14.9531 5.03507C14.1837 3.61874 12.3321 3.60207 12.3135 3.60207C12.1815 3.60439 12.0511 3.57269 11.9349 3.51004C11.8186 3.44738 11.7205 3.35587 11.6499 3.2443C11.3213 2.72338 10.8654 2.29474 10.3253 1.99882C9.78517 1.70289 9.17855 1.54943 8.56267 1.55292C7.4727 1.55292 6.42632 2.01391 5.76336 2.78651C5.15682 3.49179 4.91638 4.38942 5.06834 5.38066C5.09985 5.57735 5.05384 5.77859 4.94 5.94206C4.82616 6.10552 4.65336 6.21847 4.45795 6.25713C4.40853 6.26683 4.35829 6.27176 4.30792 6.27187C4.12992 6.2717 3.95748 6.20982 3.82 6.09676C3.68251 5.98371 3.58847 5.82648 3.55391 5.65187V5.63969C3.50407 5.30619 3.48774 4.96853 3.50518 4.63178C1.68685 5.19344 1.56182 6.67709 1.55733 6.74377V6.75403V6.76429C1.44726 7.47423 1.58646 8.20039 1.95126 8.81931C2.31607 9.43823 2.88396 9.9117 3.5584 10.1592C3.64071 9.31984 4.03069 8.54057 4.65324 7.97153C5.27579 7.40248 6.08687 7.08391 6.93027 7.07717C6.98573 7.06959 7.04164 7.06573 7.09762 7.06563C7.66248 7.06563 7.95742 7.45867 7.95742 7.84721C7.95802 7.94845 7.93811 8.04877 7.89888 8.14211C7.85965 8.23544 7.80192 8.31986 7.72916 8.39028C7.64482 8.46978 7.54546 8.53165 7.43691 8.57227C7.32836 8.6129 7.21279 8.63146 7.09698 8.62686C7.04166 8.62666 6.98642 8.62302 6.93156 8.61597C6.68645 8.61706 6.44397 8.66642 6.21794 8.76123C5.99192 8.85604 5.78678 8.99443 5.61424 9.16852C5.26578 9.52011 5.07126 9.99572 5.07347 10.4907C5.07568 10.9857 5.27444 11.4596 5.62602 11.808C5.97761 12.1565 6.45322 12.351 6.94823 12.3488H8.39661C8.96795 12.3502 9.51552 12.5777 9.91958 12.9816C10.3236 13.3856 10.5513 13.9331 10.5528 14.5044V17.2377C10.551 17.4385 10.4707 17.6307 10.3291 17.7731C10.1875 17.9156 9.99582 17.997 9.79499 18H9.78088Z" fill="#9CA3AF" />
<path d="M10.2005 6.89635C10.0392 6.89633 9.88203 6.84514 9.75165 6.75016C9.66975 6.69094 9.60032 6.61617 9.54732 6.53011C9.49433 6.44405 9.45881 6.34839 9.44279 6.2486C9.42677 6.14881 9.43058 6.04684 9.45398 5.94852C9.47738 5.85019 9.51992 5.75745 9.57918 5.67557L9.5888 5.66275L9.5997 5.65057C9.64162 5.60611 9.6784 5.55707 9.70934 5.50438C9.82471 5.31044 9.85832 5.07861 9.80277 4.8599C9.74722 4.64118 9.60706 4.45348 9.41312 4.33811C9.21918 4.22273 8.98735 4.18912 8.76863 4.24467C8.54992 4.30022 8.36222 4.44039 8.24685 4.63432L8.23787 4.64907L8.22697 4.66318C8.19178 4.70681 8.15225 4.74677 8.109 4.78243C7.98973 4.87936 7.8444 4.93875 7.69138 4.95309C7.53836 4.96743 7.38453 4.93607 7.24933 4.86298C7.11413 4.78989 7.00365 4.67836 6.93184 4.54247C6.86003 4.40659 6.83012 4.25247 6.84591 4.09959C6.86496 3.90606 6.95659 3.72692 7.10237 3.5982C7.12759 3.56615 7.15324 3.53473 7.17931 3.50395C7.59527 3.04048 8.176 2.75808 8.7974 2.71709C9.41881 2.6761 10.0316 2.87978 10.5048 3.28461C10.9781 3.68943 11.2742 4.26328 11.3299 4.88354C11.3857 5.5038 11.1966 6.12126 10.8032 6.60398C10.7314 6.6952 10.6399 6.76893 10.5355 6.81958C10.4311 6.87023 10.3165 6.89648 10.2005 6.89635Z" fill="#9CA3AF" />
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -1,4 +0,0 @@
<svg width="16" height="14" viewBox="0 0 16 14" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M1.45455 1.4V12.4696L3.69816 10.6753L4.10711 11.149L3.69816 10.6753C3.93864 10.483 4.24138 10.3785 4.55294 10.3785H14.5455V1.4H1.45455ZM0.393527 0.371029C0.644209 0.132541 0.982426 0 1.33333 0H14.6667C15.0176 0 15.3558 0.13254 15.6065 0.371029C15.8574 0.609791 16 0.935364 16 1.27658V10.5019C16 10.8431 15.8574 11.1687 15.6065 11.4074C15.3558 11.6459 15.0176 11.7785 14.6667 11.7785H4.59476L2.18811 13.7032C1.99353 13.8588 1.75722 13.9576 1.50718 13.9891C1.25716 14.0206 1.00265 13.9836 0.773317 13.8819C0.543911 13.7803 0.348518 13.6179 0.211049 13.4127C0.0735237 13.2074 -2.60092e-07 12.9683 0 12.7234V1.27658C0 0.935363 0.142559 0.60979 0.393527 0.371029Z" fill="#9CA3AF" />
</svg>

Before

Width:  |  Height:  |  Size: 831 B

Some files were not shown because too many files have changed in this diff Show More