feat: adding create bot functionality (#368)

* feat: adding create bot functionality

Signed-off-by: James <james@jan.ai>

* update the temperature progress bar

Signed-off-by: James <james@jan.ai>

* chore: remove tgz

Signed-off-by: James <james@jan.ai>

* update core dependency

Signed-off-by: James <james@jan.ai>

* fix e2e test

Signed-off-by: James <james@jan.ai>

---------

Signed-off-by: James <james@jan.ai>
Co-authored-by: James <james@jan.ai>
This commit is contained in:
NamH 2023-10-23 01:57:56 -07:00 committed by GitHub
parent ec9e41e765
commit 6e2210cb22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 1903 additions and 578 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ package-lock.json
*.log
plugin-core/lib
core/lib/**

View File

@ -35,7 +35,7 @@ test.afterAll(async () => {
});
test("explores models", async () => {
await page.getByRole("button", { name: "Explore Models" }).first().click();
await page.getByTestId("Explore Models").first().click();
const header = await page
.getByRole("heading")
.filter({ hasText: "Explore Models" })

View File

@ -35,7 +35,7 @@ test.afterAll(async () => {
});
test("shows my models", async () => {
await page.getByRole("button", { name: "My Models" }).first().click();
await page.getByTestId("My Models").first().click();
const header = await page
.getByRole("heading")
.filter({ hasText: "My Models" })

View File

@ -44,31 +44,26 @@ test("renders left navigation panel", async () => {
expect(chatSection).toBe(false);
// Home actions
const newChatBtn = await page
.getByRole("button", { name: "New Chat" })
const createBotBtn = await page
.getByRole("button", { name: "Create bot" })
.first()
.isEnabled();
const exploreBtn = await page
.getByRole("button", { name: "Explore Models" })
.first()
.isEnabled();
const discordBtn = await page
.getByRole("button", { name: "Discord" })
.first()
.isEnabled();
const myModelsBtn = await page
.getByRole("button", { name: "My Models" })
.getByTestId("My Models")
.first()
.isEnabled();
const settingsBtn = await page
.getByRole("button", { name: "Settings" })
.getByTestId("Settings")
.first()
.isEnabled();
expect(
[
newChatBtn,
createBotBtn,
exploreBtn,
discordBtn,
myModelsBtn,
settingsBtn,
].filter((e) => !e).length

View File

@ -35,7 +35,7 @@ test.afterAll(async () => {
});
test("shows settings", async () => {
await page.getByRole("button", { name: "Settings" }).first().click();
await page.getByTestId("Settings").first().click();
const pluginList = await page.getByTestId("plugin-item").count();
expect(pluginList).toBe(4);

View File

@ -19,6 +19,7 @@
"dev:electron": "yarn workspace jan dev",
"dev:web": "yarn workspace jan-web dev",
"dev": "concurrently --kill-others \"yarn dev:web\" \"wait-on http://localhost:3000 && yarn dev:electron\"",
"test-local": "yarn lint && yarn build && yarn test",
"build:core": "cd core && yarn install && yarn run build",
"build:web": "yarn workspace jan-web build && cpx \"web/out/**\" \"electron/renderer/\"",
"build:electron": "yarn workspace jan build",

View File

@ -1,4 +1,11 @@
import { core, store, RegisterExtensionPoint, StoreService, DataService, PluginService } from "@janhq/core";
import {
invokePluginFunc,
store,
RegisterExtensionPoint,
StoreService,
DataService,
PluginService,
} from "@janhq/core";
/**
* Create a collection on data store
@ -8,8 +15,14 @@ import { core, store, RegisterExtensionPoint, StoreService, DataService, PluginS
* @returns Promise<void>
*
*/
function createCollection({ name, schema }: { name: string; schema?: { [key: string]: any } }): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "createCollection", name, schema);
function createCollection({
name,
schema,
}: {
name: string;
schema?: { [key: string]: any };
}): Promise<void> {
return invokePluginFunc(MODULE_PATH, "createCollection", name, schema);
}
/**
@ -20,7 +33,7 @@ function createCollection({ name, schema }: { name: string; schema?: { [key: str
*
*/
function deleteCollection(name: string): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "deleteCollection", name);
return invokePluginFunc(MODULE_PATH, "deleteCollection", name);
}
/**
@ -31,8 +44,14 @@ function deleteCollection(name: string): Promise<void> {
* @returns Promise<any>
*
*/
function insertOne({ collectionName, value }: { collectionName: string; value: any }): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "insertOne", collectionName, value);
function insertOne({
collectionName,
value,
}: {
collectionName: string;
value: any;
}): Promise<any> {
return invokePluginFunc(MODULE_PATH, "insertOne", collectionName, value);
}
/**
@ -44,8 +63,16 @@ function insertOne({ collectionName, value }: { collectionName: string; value: a
* @returns Promise<void>
*
*/
function updateOne({ collectionName, key, value }: { collectionName: string; key: string; value: any }): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "updateOne", collectionName, key, value);
function updateOne({
collectionName,
key,
value,
}: {
collectionName: string;
key: string;
value: any;
}): Promise<void> {
return invokePluginFunc(MODULE_PATH, "updateOne", collectionName, key, value);
}
/**
@ -64,7 +91,13 @@ function updateMany({
value: any;
selector?: { [key: string]: any };
}): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "updateMany", collectionName, value, selector);
return invokePluginFunc(
MODULE_PATH,
"updateMany",
collectionName,
value,
selector
);
}
/**
@ -75,8 +108,14 @@ function updateMany({
* @returns Promise<void>
*
*/
function deleteOne({ collectionName, key }: { collectionName: string; key: string }): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "deleteOne", collectionName, key);
function deleteOne({
collectionName,
key,
}: {
collectionName: string;
key: string;
}): Promise<void> {
return invokePluginFunc(MODULE_PATH, "deleteOne", collectionName, key);
}
/**
@ -94,7 +133,7 @@ function deleteMany({
collectionName: string;
selector?: { [key: string]: any };
}): Promise<void> {
return core.invokePluginFunc(MODULE_PATH, "deleteMany", collectionName, selector);
return invokePluginFunc(MODULE_PATH, "deleteMany", collectionName, selector);
}
/**
@ -103,8 +142,14 @@ function deleteMany({
* @param {string} key - The key of the record to retrieve.
* @returns {Promise<any>} A promise that resolves when the record is retrieved.
*/
function findOne({ collectionName, key }: { collectionName: string; key: string }): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "findOne", collectionName, key);
function findOne({
collectionName,
key,
}: {
collectionName: string;
key: string;
}): Promise<any> {
return invokePluginFunc(MODULE_PATH, "findOne", collectionName, key);
}
/**
@ -123,19 +168,28 @@ function findMany({
selector: { [key: string]: any };
sort?: [{ [key: string]: any }];
}): Promise<any> {
return core.invokePluginFunc(MODULE_PATH, "findMany", collectionName, selector, sort);
return invokePluginFunc(
MODULE_PATH,
"findMany",
collectionName,
selector,
sort
);
}
function onStart() {
createCollection({ name: "conversations", schema: {} });
createCollection({ name: "messages", schema: {} });
createCollection({ name: "bots", schema: {} });
}
// Register all the above functions and objects with the relevant extension points
// prettier-ignore
export function init({ register }: { register: RegisterExtensionPoint }) {
register(PluginService.OnStart, PLUGIN_NAME, onStart);
register(StoreService.CreateCollection, createCollection.name, createCollection);
register(StoreService.DeleteCollection, deleteCollection.name, deleteCollection);
register(StoreService.InsertOne, insertOne.name, insertOne);
register(StoreService.UpdateOne, updateOne.name, updateOne);
register(StoreService.UpdateMany, updateMany.name, updateMany);
@ -144,19 +198,34 @@ export function init({ register }: { register: RegisterExtensionPoint }) {
register(StoreService.FindOne, findOne.name, findOne);
register(StoreService.FindMany, findMany.name, findMany);
// for conversations management
register(DataService.GetConversations, getConversations.name, getConversations);
register(DataService.GetConversationById,getConversationById.name,getConversationById);
register(DataService.CreateConversation, createConversation.name, createConversation);
register(DataService.UpdateConversation, updateConversation.name, updateConversation);
register(DataService.UpdateMessage, updateMessage.name, updateMessage);
register(DataService.DeleteConversation, deleteConversation.name, deleteConversation);
// for messages management
register(DataService.UpdateMessage, updateMessage.name, updateMessage);
register(DataService.CreateMessage, createMessage.name, createMessage);
register(DataService.GetConversationMessages, getConversationMessages.name, getConversationMessages);
// for bots management
register(DataService.CreateBot, createBot.name, createBot);
register(DataService.GetBots, getBots.name, getBots);
register(DataService.GetBotById, getBotById.name, getBotById);
register(DataService.DeleteBot, deleteBot.name, deleteBot);
register(DataService.UpdateBot, updateBot.name, updateBot);
}
function getConversations(): Promise<any> {
return store.findMany("conversations", {}, [{ updatedAt: "desc" }]);
}
function getConversationById(id: string): Promise<any> {
return store.findOne("conversations", id);
}
function createConversation(conversation: any): Promise<number | undefined> {
return store.insertOne("conversations", conversation);
}
@ -174,9 +243,83 @@ function updateMessage(message: any): Promise<void> {
}
function deleteConversation(id: any) {
return store.deleteOne("conversations", id).then(() => store.deleteMany("messages", { conversationId: id }));
return store
.deleteOne("conversations", id)
.then(() => store.deleteMany("messages", { conversationId: id }));
}
function getConversationMessages(conversationId: any) {
return store.findMany("messages", { conversationId }, [{ createdAt: "desc" }]);
return store.findMany("messages", { conversationId }, [
{ createdAt: "desc" },
]);
}
function createBot(bot: any): Promise<void> {
console.debug("Creating bot", JSON.stringify(bot, null, 2));
return store
.insertOne("bots", bot)
.then(() => {
console.debug("Bot created", JSON.stringify(bot, null, 2));
return Promise.resolve();
})
.catch((err) => {
console.error("Error creating bot", err);
return Promise.reject(err);
});
}
function getBots(): Promise<any> {
console.debug("Getting bots");
return store
.findMany("bots", { name: { $gt: null } })
.then((bots) => {
console.debug("Bots retrieved", JSON.stringify(bots, null, 2));
return Promise.resolve(bots);
})
.catch((err) => {
console.error("Error getting bots", err);
return Promise.reject(err);
});
}
function deleteBot(id: string): Promise<any> {
console.debug("Deleting bot", id);
return store
.deleteOne("bots", id)
.then(() => {
console.debug("Bot deleted", id);
return Promise.resolve();
})
.catch((err) => {
console.error("Error deleting bot", err);
return Promise.reject(err);
});
}
function updateBot(bot: any): Promise<void> {
console.debug("Updating bot", JSON.stringify(bot, null, 2));
return store
.updateOne("bots", bot._id, bot)
.then(() => {
console.debug("Bot updated");
return Promise.resolve();
})
.catch((err) => {
console.error("Error updating bot", err);
return Promise.reject(err);
});
}
function getBotById(botId: string): Promise<any> {
console.debug("Getting bot", botId);
return store
.findOne("bots", botId)
.then((bot) => {
console.debug("Bot retrieved", JSON.stringify(bot, null, 2));
return Promise.resolve(bot);
})
.catch((err) => {
console.error("Error getting bot", err);
return Promise.reject(err);
});
}

View File

@ -12,7 +12,7 @@
],
"scripts": {
"build": "tsc -b ./config/tsconfig.esm.json && tsc -b ./config/tsconfig.cjs.json && webpack --config webpack.config.js",
"postinstall": "rimraf ./data-plugin*.tgz && npm run build",
"postinstall": "rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"exports": {
@ -40,8 +40,12 @@
"node_modules"
],
"dependencies": {
"@janhq/core": "^0.1.3",
"@janhq/core": "^0.1.6",
"pouchdb-find": "^8.0.1",
"pouchdb-node": "^8.0.1"
}
},
"bundleDependencies": [
"pouchdb-node",
"pouchdb-find"
]
}

View File

@ -15,8 +15,18 @@ const stopModel = () => {
invokePluginFunc(MODULE_PATH, "killSubprocess");
};
function requestInference(recentMessages: any[]): Observable<string> {
function requestInference(recentMessages: any[], bot?: any): Observable<string> {
return new Observable((subscriber) => {
const requestBody = JSON.stringify({
messages: recentMessages,
stream: true,
model: "gpt-3.5-turbo",
max_tokens: bot?.maxTokens ?? 2048,
frequency_penalty: bot?.frequencyPenalty ?? 0,
presence_penalty: bot?.presencePenalty ?? 0,
temperature: bot?.customTemperature ?? 0,
});
console.debug(`Request body: ${requestBody}`);
fetch(INFERENCE_URL, {
method: "POST",
headers: {
@ -24,12 +34,7 @@ function requestInference(recentMessages: any[]): Observable<string> {
Accept: "text/event-stream",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
messages: recentMessages,
stream: true,
model: "gpt-3.5-turbo",
max_tokens: 500,
}),
body: requestBody,
})
.then(async (response) => {
const stream = response.body;
@ -62,22 +67,39 @@ function requestInference(recentMessages: any[]): Observable<string> {
});
}
async function retrieveLastTenMessages(conversationId: string) {
async function retrieveLastTenMessages(conversationId: string, bot?: any) {
// TODO: Common collections should be able to access via core functions instead of store
const messageHistory = (await store.findMany("messages", { conversationId }, [{ createdAt: "asc" }])) ?? [];
return messageHistory
let recentMessages = messageHistory
.filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant"))
.slice(-10)
.map((message) => {
return {
content: message.message.trim(),
role: message.user === "user" ? "user" : "assistant",
};
});
.slice(-9)
.map((message) => ({
content: message.message.trim(),
role: message.user === "user" ? "user" : "assistant",
}));
if (bot && bot.systemPrompt) {
// append bot's system prompt
recentMessages = [{
content: `[INST] ${bot.systemPrompt}`,
role: 'system'
},...recentMessages];
}
console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`);
return recentMessages;
}
async function handleMessageRequest(data: NewMessageRequest) {
const recentMessages = await retrieveLastTenMessages(data.conversationId);
const conversation = await store.findOne("conversations", data.conversationId);
let bot = undefined;
if (conversation.botId != null) {
bot = await store.findOne("bots", conversation.botId);
}
const recentMessages = await retrieveLastTenMessages(data.conversationId, bot);
const message = {
...data,
message: "",
@ -91,7 +113,7 @@ async function handleMessageRequest(data: NewMessageRequest) {
message._id = id;
events.emit(EventName.OnNewMessageResponse, message);
requestInference(recentMessages).subscribe({
requestInference(recentMessages, bot).subscribe({
next: (content) => {
message.message = content;
events.emit(EventName.OnMessageResponseUpdate, message);

View File

@ -12,7 +12,7 @@
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"postinstall": "rimraf ./*.tgz && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"postinstall": "rimraf *.tgz --glob && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"exports": {
@ -26,7 +26,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/core": "^0.1.3",
"@janhq/core": "^0.1.6",
"kill-port-process": "^3.2.0",
"rxjs": "^7.8.1",
"tcp-port-used": "^1.0.2",

View File

@ -12,7 +12,7 @@
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"postinstall": "rimraf ./*.tgz && npm run build",
"postinstall": "rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"devDependencies": {
@ -27,7 +27,7 @@
"README.md"
],
"dependencies": {
"@janhq/core": "^0.1.3",
"@janhq/core": "^0.1.6",
"ts-loader": "^9.5.0"
}
}

View File

@ -12,7 +12,7 @@
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"postinstall": "rimraf ./*.tgz && npm run build",
"postinstall": "rimraf *.tgz --glob && npm run build",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"devDependencies": {
@ -21,7 +21,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/core": "^0.1.3",
"@janhq/core": "^0.1.6",
"systeminformation": "^5.21.8",
"ts-loader": "^9.5.0"
},

View File

@ -12,7 +12,7 @@
],
"scripts": {
"build": "tsc -b . && webpack --config webpack.config.js",
"postinstall": "rimraf ./*.tgz && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"postinstall": "rimraf *.tgz --glob && npm run build && rimraf dist/nitro/* && cpx \"nitro/**\" \"dist/nitro\"",
"build:publish": "npm pack && cpx *.tgz ../../electron/core/pre-install"
},
"exports": {
@ -26,7 +26,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@janhq/core": "^0.1.3",
"@janhq/core": "^0.1.6",
"azure-openai": "^0.9.4",
"kill-port-process": "^3.2.0",
"tcp-port-used": "^1.0.2",

View File

@ -0,0 +1,23 @@
import React from 'react'
import SecondaryButton from '../SecondaryButton'
type Props = {
allowEdit?: boolean
}
const Avatar: React.FC<Props> = ({ allowEdit = false }) => (
<div className="mx-auto flex flex-col gap-5">
<span className="mx-auto inline-block h-14 w-14 overflow-hidden rounded-full bg-gray-100">
<svg
className="mx-auto h-full w-full text-gray-300"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
{allowEdit ?? <SecondaryButton title={'Edit picture'} />}
</div>
)
export default Avatar

View File

@ -0,0 +1,56 @@
import { activeBotAtom } from "@/_helpers/atoms/Bot.atom";
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useDeleteBot from "@/_hooks/useDeleteBot";
import { useAtomValue, useSetAtom } from "jotai";
import React from "react";
import PrimaryButton from "../PrimaryButton";
import ExpandableHeader from "../ExpandableHeader";
const BotInfo: React.FC = () => {
const { deleteBot } = useDeleteBot();
const { createConvoByBot } = useCreateConversation();
const setMainView = useSetAtom(setMainViewStateAtom);
const botInfo = useAtomValue(activeBotAtom);
if (!botInfo) return null;
const onNewChatClicked = () => {
if (!botInfo) {
alert("No bot selected");
return;
}
createConvoByBot(botInfo);
};
const onDeleteBotClick = async () => {
// TODO: display confirmation diaglog
const result = await deleteBot(botInfo._id);
if (result === "success") {
setMainView(MainViewState.Welcome);
}
};
return (
<div className="flex flex-col gap-2 mx-1 my-1">
<ExpandableHeader title="BOT INFO" expanded={true} onClick={() => {}} />
<div className="flex flex-col">
<label>{botInfo.name}</label>
<PrimaryButton onClick={onNewChatClicked} title="New chat" />
<span>{botInfo.description}</span>
</div>
<PrimaryButton
title="Delete bot"
onClick={onDeleteBotClick}
className="bg-red-500 hover:bg-red-400"
/>
</div>
);
};
export default BotInfo;

View File

@ -0,0 +1,71 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react'
import Avatar from '../Avatar'
import PrimaryButton from '../PrimaryButton'
import useCreateConversation from '@/_hooks/useCreateConversation'
import useDeleteBot from '@/_hooks/useDeleteBot'
import {
setMainViewStateAtom,
MainViewState,
} from '@/_helpers/atoms/MainView.atom'
const BotInfoContainer: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom)
const setMainView = useSetAtom(setMainViewStateAtom)
const { deleteBot } = useDeleteBot()
const { createConvoByBot } = useCreateConversation()
const onNewChatClicked = () => {
if (!activeBot) {
alert('No bot selected')
return
}
createConvoByBot(activeBot)
}
const onDeleteBotClick = async () => {
if (!activeBot) {
alert('No bot selected')
return
}
// TODO: display confirmation diaglog
const result = await deleteBot(activeBot._id)
if (result === 'success') {
setMainView(MainViewState.Welcome)
}
}
if (!activeBot) return null
return (
<div className="flex h-full w-full pt-4">
<div className="mx-auto flex w-[672px] min-w-max flex-col gap-4">
<Avatar />
<h1 className="text-center text-2xl font-bold">
{activeBot?.name}
</h1>
<div className="flex gap-4">
<PrimaryButton
fullWidth
title="New chat"
onClick={onNewChatClicked}
/>
<PrimaryButton
fullWidth
className='bg-red-500 hover:bg-red-400'
title="Delete bot"
onClick={onDeleteBotClick}
/>
</div>
<p>{activeBot?.description}</p>
<p>System prompt</p>
<p>{activeBot?.systemPrompt}</p>
</div>
</div>
)
}
export default BotInfoContainer

View File

@ -0,0 +1,59 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom'
import useGetBots from '@/_hooks/useGetBots'
import { Bot } from '@/_models/Bot'
import { useAtom, useSetAtom } from 'jotai'
import React, { useEffect, useState } from 'react'
import Avatar from '../Avatar'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
const BotListContainer: React.FC = () => {
const [open, setOpen] = useAtom(showingBotListModalAtom)
const setMainView = useSetAtom(setMainViewStateAtom)
const [activeBot, setActiveBot] = useAtom(activeBotAtom)
const [bots, setBots] = useState<Bot[]>([])
const { getAllBots } = useGetBots()
useEffect(() => {
if (open) {
getAllBots().then((res) => {
setBots(res)
})
}
}, [open])
const onBotSelected = (bot: Bot) => {
if (bot._id !== activeBot?._id) {
setMainView(MainViewState.BotInfo)
setActiveBot(bot)
}
setOpen(false)
}
return (
<div className="overflow-hidden bg-white shadow sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200">
{bots.map((bot) => (
<li
role="button"
key={bot._id}
className="flex gap-4 p-4 hover:bg-hover-light sm:px-6"
onClick={() => onBotSelected(bot)}
>
<Avatar />
<div className="flex flex-1 flex-col">
<p className="line-clamp-1">{bot.name}</p>
<p className="line-clamp-1 text-ellipsis">{bot._id}</p>
</div>
</li>
))}
</ul>
</div>
)
}
export default BotListContainer

View File

@ -0,0 +1,48 @@
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom'
import { Dialog, Transition } from '@headlessui/react'
import { useAtom } from 'jotai'
import React, { Fragment } from 'react'
import BotListContainer from '../BotListContainer'
const BotListModal: React.FC = () => {
const [open, setOpen] = useAtom(showingBotListModalAtom)
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={setOpen}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<h1 className="mb-4 text-lg text-black font-bold">Your bots</h1>
<BotListContainer />
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
export default BotListModal

View File

@ -0,0 +1,26 @@
import Image from "next/image";
const BotPreview: React.FC = () => {
return (
<div className="flex pb-2 flex-col border border-gray-400 min-h-[235px] gap-2 overflow-hidden rounded-lg">
<div className="flex items-center justify-center p-2 bg-gray-400">
<Image
className="rounded-md"
src={
"https://i.pinimg.com/564x/52/b1/6f/52b16f96f52221d48bea716795ccc89a.jpg"
}
width={32}
height={32}
alt=""
/>
</div>
<div className="flex items-center text-xs text-gray-400 gap-1 px-1">
<div className="flex-grow mx-1 border-b border-gray-400"></div>
Context cleared
<div className="flex-grow mx-1 border-b border-gray-400"></div>
</div>
</div>
);
};
export default BotPreview;

View File

@ -0,0 +1,133 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import { useAtomValue } from 'jotai'
import React, { useEffect, useState } from 'react'
import ExpandableHeader from '../ExpandableHeader'
import { useDebouncedCallback } from 'use-debounce'
import useUpdateBot from '@/_hooks/useUpdateBot'
import ProgressSetting from '../ProgressSetting'
import { set } from 'react-hook-form'
const delayBeforeUpdateInMs = 1000
const BotSetting: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom)
const [temperature, setTemperature] = useState(0)
const [maxTokens, setMaxTokens] = useState(0)
const [frequencyPenalty, setFrequencyPenalty] = useState(0)
const [presencePenalty, setPresencePenalty] = useState(0)
useEffect(() => {
if (!activeBot) return
setMaxTokens(activeBot.maxTokens ?? 0)
setTemperature(activeBot.customTemperature ?? 0)
setFrequencyPenalty(activeBot.frequencyPenalty ?? 0)
setPresencePenalty(activeBot.presencePenalty ?? 0)
}, [activeBot?._id])
const { updateBot } = useUpdateBot()
const debouncedTemperature = useDebouncedCallback((value) => {
if (!activeBot) return
if (activeBot.customTemperature === value) return
updateBot(activeBot, { customTemperature: value })
}, delayBeforeUpdateInMs)
const debouncedMaxToken = useDebouncedCallback((value) => {
if (!activeBot) return
if (activeBot.maxTokens === value) return
updateBot(activeBot, { maxTokens: value })
}, delayBeforeUpdateInMs)
const debouncedFreqPenalty = useDebouncedCallback((value) => {
if (!activeBot) return
if (activeBot.frequencyPenalty === value) return
updateBot(activeBot, { frequencyPenalty: value })
}, delayBeforeUpdateInMs)
const debouncedPresencePenalty = useDebouncedCallback((value) => {
if (!activeBot) return
if (activeBot.presencePenalty === value) return
updateBot(activeBot, { presencePenalty: value })
}, delayBeforeUpdateInMs)
const debouncedSystemPrompt = useDebouncedCallback((value) => {
if (!activeBot) return
if (activeBot.systemPrompt === value) return
updateBot(activeBot, { systemPrompt: value })
}, delayBeforeUpdateInMs)
if (!activeBot) return null
return (
<div className="my-3 flex flex-col">
<ExpandableHeader
title="BOT SETTINGS"
expanded={true}
onClick={() => {}}
/>
<div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4">
{/* System prompt */}
<div>
<label
htmlFor="comment"
className="block text-sm font-medium leading-6 text-gray-900"
>
System prompt
</label>
<div className="mt-2">
<textarea
rows={4}
name="comment"
id="comment"
className="block w-full rounded-md border-0 py-1.5 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"
defaultValue={activeBot.systemPrompt}
onChange={(e) => debouncedSystemPrompt(e.target.value)}
/>
</div>
</div>
<ProgressSetting
title="Max tokens"
min={0}
max={4096}
step={1}
value={maxTokens}
onValueChanged={(value) => debouncedMaxToken(value)}
/>
<ProgressSetting
min={0}
max={1}
step={0.01}
title="Temperature"
value={temperature}
onValueChanged={(value) => debouncedTemperature(value)}
/>
<ProgressSetting
title="Frequency penalty"
value={frequencyPenalty}
min={0}
max={1}
step={0.01}
onValueChanged={(value) => debouncedFreqPenalty(value)}
/>
<ProgressSetting
min={0}
max={1}
step={0.01}
title="Presence penalty"
value={presencePenalty}
onValueChanged={(value) => {
setPresencePenalty(value)
debouncedPresencePenalty(value)
}}
/>
</div>
</div>
)
}
export default BotSetting

View File

@ -0,0 +1,12 @@
import React from "react";
import MainHeader from "../MainHeader";
import MainView from "../MainView";
const CenterContainer: React.FC = () => (
<div className="flex-1 flex flex-col">
<MainHeader />
<MainView />
</div>
);
export default React.memo(CenterContainer);

View File

@ -2,6 +2,7 @@ import React from 'react'
import Image from 'next/image'
import useCreateConversation from '@/_hooks/useCreateConversation'
import { AssistantModel } from '@/_models/AssistantModel'
import { PlayIcon } from "@heroicons/react/24/outline"
type Props = {
model: AssistantModel
@ -33,7 +34,7 @@ const ConversationalCard: React.FC<Props> = ({ model }) => {
</span>
</div>
<span className="flex items-center gap-0.5 text-xs leading-5 text-gray-500">
<Image src={'icons/play.svg'} width={16} height={16} alt="" />
<PlayIcon width={16} height={16} />
32.2k runs
</span>
</button>

View File

@ -0,0 +1,181 @@
import React from 'react'
import TextInputWithTitle from '../TextInputWithTitle'
import TextAreaWithTitle from '../TextAreaWithTitle'
import DropdownBox from '../DropdownBox'
import PrimaryButton from '../PrimaryButton'
import ToggleSwitch from '../ToggleSwitch'
import CreateBotPromptInput from '../CreateBotPromptInput'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import { Bot } from '@/_models/Bot'
import { SubmitHandler, useForm } from 'react-hook-form'
import Avatar from '../Avatar'
import { v4 as uuidv4 } from 'uuid'
import DraggableProgressBar from '../DraggableProgressBar'
import { useSetAtom } from 'jotai'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import { DataService } from '@janhq/core'
const CreateBotContainer: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels()
const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const createBot = async (bot: Bot) => {
try {
await executeSerial(DataService.CreateBot, bot).then(async () => {
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
})
} catch (err) {
alert(err)
console.error(err)
}
}
const { handleSubmit, control } = useForm<Bot>({
defaultValues: {
_id: uuidv4(),
name: '',
description: '',
visibleFromBotProfile: true,
systemPrompt: '',
welcomeMessage: '',
publiclyAccessible: true,
suggestReplies: false,
renderMarkdownContent: true,
customTemperature: 0.7,
enableCustomTemperature: false,
maxTokens: 2048,
frequencyPenalty: 0,
presencePenalty: 0,
},
mode: 'onChange',
})
const onSubmit: SubmitHandler<Bot> = (data) => {
console.log('bot', JSON.stringify(data, null, 2))
if (!data.modelId) {
alert('Please select a model')
return
}
const bot: Bot = {
...data,
customTemperature: Number(data.customTemperature),
maxTokens: Number(data.maxTokens),
frequencyPenalty: Number(data.frequencyPenalty),
presencePenalty: Number(data.presencePenalty),
}
createBot(bot)
}
let models = downloadedModels.map((model) => {
return model._id
})
models = ['Select a model', ...models]
return (
<form
className="flex h-full w-full flex-col"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mx-6 mt-3 flex items-center justify-between gap-3">
<span className="text-3xl font-bold text-gray-900">Create Bot</span>
<div className="flex gap-3">
<PrimaryButton isSubmit title="Create" />
</div>
</div>
<div className="scroll flex flex-1 flex-col overflow-y-auto pt-4">
<div className="mx-auto flex max-w-2xl flex-col gap-4">
<Avatar allowEdit />
<TextInputWithTitle
description="Bot name should be unique, 4-20 characters long, and may include alphanumeric characters, dashes or underscores."
title="Bot name"
id="name"
control={control}
required={true}
/>
<TextAreaWithTitle
id="description"
title="Bot description"
placeholder="Optional"
control={control}
/>
<div className="flex flex-col gap-4 pb-2">
<DropdownBox
id="modelId"
title="Model"
data={models}
control={control}
required={true}
/>
<CreateBotPromptInput
id="systemPrompt"
control={control}
required
/>
<div className="flex flex-col gap-0.5">
<label className="block text-base font-bold text-gray-900">
Bot access
</label>
<span className="pb-2 text-sm text-[#737d7d]">
If this setting is enabled, the bot will be added to your
profile and will be publicly accessible. Turning this off will
make the bot private.
</span>
<ToggleSwitch
id="publiclyAccessible"
title="Bot publicly accessible"
control={control}
/>
</div>
<p>Max tokens</p>
<DraggableProgressBar
id="maxTokens"
control={control}
min={0}
max={4096}
step={1}
/>
<p>Custom temperature</p>
<DraggableProgressBar
id="customTemperature"
control={control}
min={0}
max={1}
step={0.01}
/>
<p>Frequency penalty</p>
<DraggableProgressBar
id="frequencyPenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
<p>Presence penalty</p>
<DraggableProgressBar
id="presencePenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
</div>
</div>
</form>
)
}
export default CreateBotContainer

View File

@ -0,0 +1,49 @@
import React, { Fragment, use } from "react";
import ToggleSwitch from "../ToggleSwitch";
import { useController } from "react-hook-form";
type Props = {
id: string;
control?: any;
required?: boolean;
};
const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
const { field } = useController({
name: id,
control: control,
rules: { required: required },
});
return (
<Fragment>
<div className="flex flex-col gap-2">
<label
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
Prompt
</label>
<p className="text-sm text-gray-400 font-normal">
All conversations with this bot will start with your prompt but it
will not be visible to the user in the chat. If you would like the
prompt message to be visible to the user, consider using an intro
message instead.
</p>
<ToggleSwitch
id="visibleFromBotProfile"
title={"Prompt visible from bot profile"}
control={control}
/>
<textarea
rows={4}
className="block w-full resize-none rounded-md border-0 py-1.5 bg-transparent shadow-sm ring-1 ring-inset text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="Talk to me like a pirate"
{...field}
/>
</div>
</Fragment>
);
};
export default CreateBotPromptInput;

View File

@ -0,0 +1,35 @@
import ToggleSwitch from "../ToggleSwitch";
import DraggableProgressBar from "../DraggableProgressBar";
import { Controller } from "react-hook-form";
type Props = {
control?: any;
};
const CutomBotTemperature: React.FC<Props> = ({ control }) => (
<div className="flex flex-col gap-2">
<ToggleSwitch
id="enableCustomTemperature"
title="Custom temperature"
control={control}
/>
<div className="text-gray-500 mt-1 text-[0.8em]">
Controls the creativity of the bot&apos;s responses. Higher values produce more
varied but unpredictable replies, lower values generate more consistent
responses.
</div>
<span className="text-gray-900">default: 0.7</span>
<Controller
name="enableCustomTemperature"
control={control}
render={({ field: { value } }) => {
if (!value) return <div />;
return (
<DraggableProgressBar id="customTemperature" control={control} min={0} max={1} step={0.01} />
);
}}
/>
</div>
);
export default CutomBotTemperature;

View File

@ -1,18 +0,0 @@
import React from 'react'
import Link from 'next/link'
import Image from 'next/image'
const DiscordContainer = () => (
<div className="flex items-center justify-between gap-3 border-t border-gray-200 p-3">
<Link
className="flex items-center gap-2 rounded-lg text-xs font-semibold leading-[18px] text-purple-700"
href={process.env.NEXT_PUBLIC_DISCORD_INVITATION_URL ?? '#'}
target="_blank_"
>
<Image src={'icons/ico_Discord.svg'} width={20} height={20} alt="" />
Discord
</Link>
</div>
)
export default React.memo(DiscordContainer)

View File

@ -0,0 +1,42 @@
import { formatTwoDigits } from "@/_utils/converter";
import React from "react";
import { Controller, useController } from "react-hook-form";
type Props = {
id: string;
control: any;
min: number;
max: number;
step: number;
};
const DraggableProgressBar: React.FC<Props> = ({ id, control, min, max, step }) => {
const { field } = useController({
name: id,
control: control,
});
return (
<div className="flex items-center gap-2 mt-2">
<input
{...field}
className="flex-1"
type="range"
min={min}
max={max}
step={step}
/>
<Controller
name={id}
control={control}
render={({ field: { value } }) => (
<span className="border border-[#737d7d] rounded-md py-1 px-2 text-gray-900">
{formatTwoDigits(value)}
</span>
)}
/>
</div>
);
};
export default DraggableProgressBar;

View File

@ -0,0 +1,40 @@
import React, { Fragment } from 'react'
import { useController } from 'react-hook-form'
type Props = {
id: string
title: string
data: string[]
control?: any
required?: boolean
}
const DropdownBox: React.FC<Props> = ({
id,
title,
data,
control,
required = false,
}) => {
const { field } = useController({
name: id,
control: control,
rules: { required: required },
})
return (
<Fragment>
<label className="block text-base font-bold text-gray-900">{title}</label>
<select
className="mt-2 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
{...field}
>
{data.map((option) => (
<option key={option}>{option}</option>
))}
</select>
</Fragment>
)
}
export default DropdownBox

View File

@ -1,6 +1,6 @@
import { Fragment, useState } from 'react'
import { Menu, Transition } from '@headlessui/react'
import Image from 'next/image'
import { ChevronDownIcon } from '@heroicons/react/24/outline'
function classNames(...classes: any) {
return classes.filter(Boolean).join(' ')
@ -20,12 +20,7 @@ export const DropdownsList: React.FC<Props> = ({ data, title }) => {
<h2 className="text-sm text-[#111928]">{title}</h2>
<Menu.Button className="inline-flex w-full items-center justify-between gap-x-1.5 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">
{checked}
<Image
src={'icons/unicorn_angle-down.svg'}
width={12}
height={12}
alt=""
/>
<ChevronDownIcon width={12} height={12} />
</Menu.Button>
</div>

View File

@ -3,7 +3,7 @@ import SelectModels from '../ModelSelector'
import InputToolbar from '../InputToolbar'
const EmptyChatContainer: React.FC = () => (
<div className="flex flex-1 flex-col">
<div className="flex flex-1 flex-col h-full w-full">
<div className="flex flex-1 items-center justify-center">
<SelectModels />
</div>

View File

@ -1,5 +1,4 @@
import HeaderTitle from '../HeaderTitle'
import SearchBar, { SearchType } from '../SearchBar'
import ExploreModelList from '../ExploreModelList'
import ExploreModelFilter from '../ExploreModelFilter'

View File

@ -5,7 +5,6 @@ import { Conversation } from '@/_models/Conversation'
import { ModelManagementService } from '@janhq/core'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import {
conversationStatesAtom,
getActiveConvoIdAtom,
setActiveConvoIdAtom,
updateConversationErrorAtom,
@ -34,10 +33,11 @@ const HistoryItem: React.FC<Props> = ({
updatedAt,
}) => {
const setMainViewState = useSetAtom(setMainViewStateAtom)
const conversationStates = useAtomValue(conversationStatesAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const updateConvWaiting = useSetAtom(
updateConversationWaitingForResponseAtom
)
const updateConvError = useSetAtom(updateConversationErrorAtom)
const isSelected = activeConvoId === conversation._id
@ -62,18 +62,13 @@ const HistoryItem: React.FC<Props> = ({
setMainViewState(MainViewState.Conversation)
setActiveConvoId(conversation._id)
}
}
};
const backgroundColor = isSelected
? 'bg-gray-100 dark:bg-gray-700'
: 'bg-white dark:bg-gray-500'
? "bg-gray-100 dark:bg-gray-700"
: "bg-white dark:bg-gray-500"
let rightImageUrl: string | undefined
if (conversationStates[conversation._id ?? '']?.waitingForResponse === true) {
rightImageUrl = 'icons/loading.svg'
}
const description = conversation?.lastMessage ?? 'No new message'
const description = conversation?.lastMessage ?? "No new message"
return (
<li

View File

@ -2,27 +2,67 @@
import BasicPromptInput from '../BasicPromptInput'
import BasicPromptAccessories from '../BasicPromptAccessories'
import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom'
import SecondaryButton from '../SecondaryButton'
import { Fragment } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { PlusIcon } from '@heroicons/react/24/outline'
import useCreateConversation from '@/_hooks/useCreateConversation'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom'
import { currentConvoStateAtom } from '@/_helpers/atoms/Conversation.atom'
import {
currentConversationAtom,
currentConvoStateAtom,
} from '@/_helpers/atoms/Conversation.atom'
import useGetBots from '@/_hooks/useGetBots'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom)
const activeModel = useAtomValue(activeAssistantModelAtom)
const { requestCreateConvo } = useCreateConversation()
const currentConvoState = useAtomValue(currentConvoStateAtom)
const currentConvo = useAtomValue(currentConversationAtom)
if (showingAdvancedPrompt) {
return <div />
}
const setActiveBot = useSetAtom(activeBotAtom)
const { getBotById } = useGetBots()
const [inputState, setInputState] = useState<
'available' | 'disabled' | 'loading'
>()
const [error, setError] = useState<string | undefined>()
const { downloadedModels } = useGetDownloadedModels()
// TODO: implement regenerate
// const onRegenerateClick = () => {};
useEffect(() => {
const getReplyState = async () => {
setInputState('loading')
if (currentConvo && currentConvo.botId && currentConvo.botId.length > 0) {
// if botId is set, check if bot is available
const bot = await getBotById(currentConvo.botId)
console.debug('Found bot', JSON.stringify(bot, null, 2))
if (bot) {
setActiveBot(bot)
}
setInputState(bot ? 'available' : 'disabled')
setError(
bot
? undefined
: `Bot ${currentConvo.botId} has been deleted by its creator. Your chat history is saved but you won't be able to send new messages.`
)
} else {
const model = downloadedModels.find(
(model) => model._id === activeModel?._id
)
setInputState(model ? 'available' : 'disabled')
setError(
model
? undefined
: `Model ${activeModel?._id} cannot be found. Your chat history is saved but you won't be able to send new messages.`
)
}
}
getReplyState()
}, [currentConvo])
const onNewConversationClick = () => {
if (activeModel) {
@ -30,6 +70,24 @@ const InputToolbar: React.FC = () => {
}
}
if (showingAdvancedPrompt) {
return <div />
}
if (inputState === 'loading') {
return <div>Loading..</div>
}
if (inputState === 'disabled') {
// text italic
return (
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center text-sm italic text-gray-600">
{error}
</p>
)
}
return (
<Fragment>
{currentConvoState?.error && (
@ -40,7 +98,6 @@ const InputToolbar: React.FC = () => {
</div>
)}
<div className="my-3 flex justify-center gap-2">
{/* <SecondaryButton title="Regenerate" onClick={onRegenerateClick} /> */}
<SecondaryButton
onClick={onNewConversationClick}
title="New Conversation"

View File

@ -1,19 +0,0 @@
import { setActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom'
import { useSetAtom } from 'jotai'
import Image from 'next/image'
import React from 'react'
const JanLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
return (
<button
className="flex items-center gap-0.5 p-3"
onClick={() => setActiveConvoId(undefined)}
>
<Image src={'icons/app_icon.svg'} width={28} height={28} alt="" />
<Image src={'icons/Jan.svg'} width={27} height={12} alt="" />
</button>
)
}
export default React.memo(JanLogo)

View File

@ -1,18 +1,43 @@
import React, { Fragment } from 'react'
import SidebarFooter from '../SidebarFooter'
import SidebarHeader from '../SidebarHeader'
import SidebarMenu from '../SidebarMenu'
import HistoryList from '../HistoryList'
import NewChatButton from '../NewChatButton'
import React, { Fragment } from "react"
import HistoryList from "../HistoryList"
import LeftHeaderAction from "../LeftHeaderAction"
import { leftSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom"
import { useAtomValue } from "jotai"
import { Variants, motion } from "framer-motion"
const LeftContainer: React.FC = () => (
<Fragment>
<SidebarHeader />
<NewChatButton />
<HistoryList />
<SidebarMenu />
<SidebarFooter />
</Fragment>
)
const leftSideBarVariants: Variants = {
show: {
x: 0,
width: 320,
opacity: 1,
transition: { duration: 0.3 },
},
hide: {
x: "-100%",
width: 0,
opacity: 0,
transition: { duration: 0.3 },
},
}
const LeftContainer: React.FC = () => {
const isVisible = useAtomValue(leftSideBarExpandStateAtom)
return (
<motion.div
initial={false}
animate={isVisible ? "show" : "hide"}
variants={leftSideBarVariants}
className="flex flex-col w-80 flex-shrink-0 border-r border-gray-200"
>
{isVisible && (
<Fragment>
<LeftHeaderAction />
<HistoryList />
</Fragment>
)}
</motion.div>
)
}
export default React.memo(LeftContainer)

View File

@ -0,0 +1,47 @@
"use client";
import React from "react";
import SecondaryButton from "../SecondaryButton";
import { useSetAtom } from "jotai";
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import { MagnifyingGlassIcon, PlusIcon } from "@heroicons/react/24/outline";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const LeftHeaderAction: React.FC = () => {
const setMainView = useSetAtom(setMainViewStateAtom);
const { downloadedModels } = useGetDownloadedModels();
const onExploreClick = () => {
setMainView(MainViewState.ExploreModel);
};
const onCreateBotClicked = () => {
if (downloadedModels.length === 0) {
alert("You need to download at least one model to create a bot.");
return;
}
setMainView(MainViewState.CreateBot);
};
return (
<div className="flex flex-row gap-2 mx-3 my-3">
<SecondaryButton
title={"Explore"}
onClick={onExploreClick}
className="flex-1"
icon={<MagnifyingGlassIcon width={16} height={16} />}
/>
<SecondaryButton
title={"Create bot"}
onClick={onCreateBotClicked}
className="flex-1"
icon={<PlusIcon width={16} height={16} />}
/>
</div>
);
};
export default React.memo(LeftHeaderAction);

View File

@ -0,0 +1,122 @@
import {
MainViewState,
getMainViewStateAtom,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
import Image from 'next/image'
import CompactLogo from '../CompactLogo'
import {
ChatBubbleOvalLeftEllipsisIcon,
Cog8ToothIcon,
CpuChipIcon,
CubeTransparentIcon,
Squares2X2Icon,
} from '@heroicons/react/24/outline'
import { useAtomValue, useSetAtom } from 'jotai'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import useGetBots from '@/_hooks/useGetBots'
const menu = [
{
name: 'Explore Models',
iconComponent: <CpuChipIcon width={24} height={24} color="#C7D2FE" />,
state: MainViewState.ExploreModel,
},
{
name: 'My Models',
iconComponent: <Squares2X2Icon width={24} height={24} color="#C7D2FE" />,
state: MainViewState.MyModel,
},
{
name: 'Settings',
iconComponent: <Cog8ToothIcon width={24} height={24} color="#C7D2FE" />,
state: MainViewState.Setting,
},
]
const LeftRibbonNav: React.FC = () => {
const currentState = useAtomValue(getMainViewStateAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const setBotListModal = useSetAtom(showingBotListModalAtom)
const { downloadedModels } = useGetDownloadedModels()
const { getAllBots } = useGetBots()
const onMenuClick = (mainViewState: MainViewState) => {
if (currentState === mainViewState) return
setMainViewState(mainViewState)
}
const isConversationView = currentState === MainViewState.Conversation
const bgColor = isConversationView ? 'bg-gray-500' : ''
const onConversationClick = () => {
if (currentState === MainViewState.Conversation) return
setMainViewState(MainViewState.Conversation)
}
const onBotListClick = async () => {
const bots = await getAllBots()
if (bots.length === 0) {
alert('You have no bot')
return
}
if (downloadedModels.length === 0) {
alert('You have no model downloaded')
return
}
setBotListModal(true)
}
return (
<nav className="flex h-screen w-20 flex-col bg-gray-900">
<CompactLogo />
<div className="flex w-full flex-1 flex-col items-center justify-between px-3 py-6">
<div>
<button
onClick={onConversationClick}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`}
>
<ChatBubbleOvalLeftEllipsisIcon
width={24}
height={24}
color="#C7D2FE"
/>
</button>
<button
onClick={onBotListClick}
className={`rounded-lg p-4 hover:bg-gray-400`}
>
<CubeTransparentIcon width={24} height={24} color="#C7D2FE" />
</button>
</div>
<ul className="flex flex-col gap-3">
{menu.map((item) => {
const bgColor = currentState === item.state ? 'bg-gray-500' : ''
return (
<li
role="button"
data-testid={item.name}
key={item.name}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`}
onClick={() => onMenuClick(item.state)}
>
{item.iconComponent}
</li>
)
})}
</ul>
</div>
{/* User avatar */}
<div className="flex items-center justify-center pb-5">
<Image src={'/icons/avatar.svg'} width={40} height={40} alt="" />
</div>
</nav>
)
}
export default LeftRibbonNav

View File

@ -1,20 +0,0 @@
import React from 'react'
import SearchBar from '../SearchBar'
// import ShortcutList from "../ShortcutList";
import HistoryList from '../HistoryList'
import DiscordContainer from '../DiscordContainer'
import JanLogo from '../JanLogo'
const LeftSidebar: React.FC = () => (
<div className="hidden h-screen flex-shrink-0 flex-col overflow-hidden border-r border-gray-200 dark:bg-gray-800 lg:inset-y-0 lg:flex lg:w-72 lg:flex-col">
<JanLogo />
<div className="flex flex-1 flex-col gap-3 overflow-x-hidden">
<SearchBar />
{/* <ShortcutList /> */}
<HistoryList />
</div>
<DiscordContainer />
</div>
)
export default LeftSidebar

View File

@ -1,10 +1,8 @@
import ChatBody from '../ChatBody'
import InputToolbar from '../InputToolbar'
import MainChatHeader from '../MainChatHeader'
import ChatBody from "../ChatBody";
import InputToolbar from "../InputToolbar";
const MainChat: React.FC = () => (
<div className="flex h-full w-full flex-col">
<MainChatHeader />
<ChatBody />
<InputToolbar />
</div>

View File

@ -1,11 +0,0 @@
import ModelMenu from '../ModelMenu'
import UserToolbar from '../UserToolbar'
const MainChatHeader: React.FC = () => (
<div className="flex w-full justify-between border-b border-gray-200 px-3 py-1 shadow-sm dark:bg-gray-950">
<UserToolbar />
<ModelMenu />
</div>
)
export default MainChatHeader

View File

@ -1,45 +1,24 @@
'use client'
import React from "react"
import LeftContainer from "../LeftContainer"
import LeftRibbonNav from "../LeftRibbonNav"
import MonitorBar from "../MonitorBar"
import RightContainer from "../RightContainer"
import CenterContainer from "../CenterContainer"
import React from 'react'
import LeftContainer from '../LeftContainer'
import RightContainer from '../RightContainer'
import { Variants, motion } from 'framer-motion'
import { useAtomValue } from 'jotai'
import { leftSideBarExpandStateAtom } from '@/_helpers/atoms/LeftSideBarExpand.atom'
const MainContainer: React.FC = () => (
<div className="flex h-screen">
<LeftRibbonNav />
const leftSideBarVariants: Variants = {
show: {
x: 0,
width: 320,
opacity: 1,
transition: { duration: 0.1 },
},
hide: {
x: '-100%',
width: 0,
opacity: 0,
transition: { duration: 0.1 },
},
}
const MainContainer: React.FC = () => {
const leftSideBarExpand = useAtomValue(leftSideBarExpandStateAtom)
return (
<div className="flex">
<motion.div
initial={false}
animate={leftSideBarExpand ? 'show' : 'hide'}
variants={leftSideBarVariants}
className="flex h-screen w-80 flex-shrink-0 flex-col border-r border-gray-200 py-3"
>
<div className="flex flex-1 flex-col h-full">
<div className="flex flex-1 overflow-hidden">
<LeftContainer />
</motion.div>
<div className="flex h-screen flex-1 flex-col">
<CenterContainer />
<RightContainer />
</div>
<MonitorBar />
</div>
)
}
</div>
)
export default MainContainer

View File

@ -0,0 +1,67 @@
import { currentConversationAtom } from '@/_helpers/atoms/Conversation.atom'
import {
leftSideBarExpandStateAtom,
rightSideBarExpandStateAtom,
showRightSideBarToggleAtom,
} from '@/_helpers/atoms/LeftSideBarExpand.atom'
import { TrashIcon } from '@heroicons/react/24/outline'
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom'
import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react'
import Image from 'next/image'
const MainHeader: React.FC = () => {
const setLeftSideBarVisibility = useSetAtom(leftSideBarExpandStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const showRightSideBarToggle = useAtomValue(showRightSideBarToggleAtom)
const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom
)
const activeConversation = useAtomValue(currentConversationAtom)
const currentConvo = useAtomValue(currentConversationAtom)
let title = currentConvo?.name ?? ''
return (
<div className="flex justify-between bg-gray-200 px-2 py-3">
<Image
role="button"
alt=""
src="icons/ic_sidebar_off.svg"
width={24}
onClick={() => setLeftSideBarVisibility((prev) => !prev)}
height={24}
/>
<span className="flex gap-0.5 text-base font-semibold leading-6">
{title}
</span>
{/* right most */}
<div className="flex gap-4">
{activeConversation != null && (
<TrashIcon
role="button"
width={24}
height={24}
color="#9CA3AF"
onClick={() => setShowConfirmDeleteConversationModal(true)}
/>
)}
{showRightSideBarToggle && (
<Image
role="button"
alt=""
src="icons/ic_sidebar_off.svg"
width={24}
onClick={() => setRightSideBarVisibility((prev) => !prev)}
height={24}
/>
)}
</div>
</div>
)
}
export default MainHeader

View File

@ -11,25 +11,42 @@ import {
} from '@/_helpers/atoms/MainView.atom'
import EmptyChatContainer from '../EmptyChatContainer'
import MainChat from '../MainChat'
import CreateBotContainer from '../CreateBotContainer'
import BotInfoContainer from '../BotInfoContainer'
const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom)
let children = null
switch (viewState) {
case MainViewState.ConversationEmptyModel:
return <EmptyChatContainer />
children = <EmptyChatContainer />
break
case MainViewState.ExploreModel:
return <ExploreModelContainer />
children = <ExploreModelContainer />
break
case MainViewState.Setting:
return <Preferences />
children = <Preferences />
break
case MainViewState.ResourceMonitor:
case MainViewState.MyModel:
return <MyModelContainer />
children = <MyModelContainer />
break
case MainViewState.CreateBot:
children = <CreateBotContainer />
break
case MainViewState.Welcome:
return <Welcome />
children = <Welcome />
break
case MainViewState.BotInfo:
children = <BotInfoContainer />
break
default:
return <MainChat />
children = <MainChat />
break
}
return <div className="flex-1 overflow-hidden">{children}</div>
}
export default MainView

View File

@ -1,42 +0,0 @@
import Image from 'next/image'
import ModelInfoItem from '../ModelInfoItem'
import React from 'react'
type Props = {
modelName: string
inferenceTime: string
hardware: string
pricing: string
}
const ModelInfo: React.FC<Props> = ({
modelName,
inferenceTime,
hardware,
pricing,
}) => (
<div className="flex flex-col gap-3 rounded-lg border border-gray-200 p-3">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">
{modelName} is available via Jan API
</h2>
<div className="flex items-start gap-4">
<ModelInfoItem description={inferenceTime} name="Inference Time" />
<ModelInfoItem description={hardware} name="Hardware" />
</div>
<hr />
<div className="flex items-center justify-between ">
<div className="flex flex-col">
<h2 className="text-xl font-semibold tracking-[-0.4px]">{pricing}</h2>
<span className="text-xs leading-[18px] text-[#6B7280]">
Average Cost / Call
</span>
</div>
<button className="flex items-center gap-2 rounded-lg bg-[#1F2A37] px-3 py-2">
<Image src={'icons/code.svg'} width={16} height={17} alt="" />
<span className="text-sm font-medium text-white">Get API Key</span>
</button>
</div>
</div>
)
export default React.memo(ModelInfo)

View File

@ -1,21 +0,0 @@
'use client'
import { useSetAtom } from 'jotai'
import { TrashIcon } from '@heroicons/react/24/outline'
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom'
const ModelMenu: React.FC = () => {
const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom
)
return (
<div className="flex items-center gap-3">
<button onClick={() => setShowConfirmDeleteConversationModal(true)}>
<TrashIcon width={24} height={24} color="#9CA3AF" />
</button>
</div>
)
}
export default ModelMenu

View File

@ -1,45 +0,0 @@
'use client'
import React from 'react'
import SecondaryButton from '../SecondaryButton'
import { useAtomValue, useSetAtom } from 'jotai'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
import useCreateConversation from '@/_hooks/useCreateConversation'
import useInitModel from '@/_hooks/useInitModel'
import { PlusIcon } from '@heroicons/react/24/outline'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom'
import { AssistantModel } from '@/_models/AssistantModel'
const NewChatButton: React.FC = () => {
const activeModel = useAtomValue(activeAssistantModelAtom)
const setMainView = useSetAtom(setMainViewStateAtom)
const { requestCreateConvo } = useCreateConversation()
const { initModel } = useInitModel()
const onClick = () => {
if (!activeModel) {
setMainView(MainViewState.ConversationEmptyModel)
} else {
createConversationAndInitModel(activeModel)
}
}
const createConversationAndInitModel = async (model: AssistantModel) => {
await requestCreateConvo(model)
await initModel(model)
}
return (
<SecondaryButton
title={'New Chat'}
onClick={onClick}
className="mx-3 my-5"
icon={<PlusIcon width={16} height={16} />}
/>
)
}
export default React.memo(NewChatButton)

View File

@ -2,7 +2,8 @@ import React from 'react'
type Props = {
title: string
onClick: () => void
onClick?: () => void
isSubmit?: boolean
fullWidth?: boolean
className?: string
}
@ -10,12 +11,13 @@ type Props = {
const PrimaryButton: React.FC<Props> = ({
title,
onClick,
isSubmit = false,
fullWidth = false,
className,
}) => (
<button
onClick={onClick}
type="button"
onClick={() => onClick?.()}
type={isSubmit ? "submit" : "button"}
className={`line-clamp-1 flex-shrink-0 rounded-md bg-blue-500 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-50 ${className} ${
fullWidth ? 'flex-1 ' : ''
}}`}

View File

@ -0,0 +1,42 @@
import { formatTwoDigits } from '@/_utils/converter'
import React from 'react'
type Props = {
title: string
value: number
min: number
max: number
step: number
onValueChanged: (value: number) => void
}
const ProgressSetting: React.FC<Props> = ({
title,
value,
min,
max,
step,
onValueChanged,
}) => (
<div className="flex w-full flex-col">
<p>{title}</p>
<div className="mt-2 flex items-center gap-2">
<input
className="flex-1"
type="range"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => {
onValueChanged(Number(e.target.value))
}}
/>
<span className="rounded-md border border-[#737d7d] px-2 py-1 text-gray-900">
{formatTwoDigits(value)}
</span>
</div>
</div>
)
export default ProgressSetting

View File

@ -1,12 +1,54 @@
import { Fragment } from 'react'
import MainView from '../MainView'
import MonitorBar from '../MonitorBar'
import { rightSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom"
import { Variants, motion } from "framer-motion"
import { useAtomValue } from "jotai"
import { Fragment } from "react"
import BotSetting from "../BotSetting"
import BotInfo from "../BotInfo"
const RightContainer = () => (
<Fragment>
<MainView />
<MonitorBar />
</Fragment>
)
const variants: Variants = {
show: {
x: 0,
width: 320,
opacity: 1,
transition: { duration: 0.3 },
},
hide: {
x: "100%",
width: 0,
opacity: 0,
transition: { duration: 0.3 },
},
}
const RightContainer = () => {
const isVisible = useAtomValue(rightSideBarExpandStateAtom)
return (
<motion.div
initial={false}
animate={isVisible ? "show" : "hide"}
variants={variants}
className="flex flex-col w-80 flex-shrink-0 py-3 border-l border-gray-200 overflow-y-auto scroll"
>
{isVisible && (
<Fragment>
<BotInfo />
{/* Divider */}
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
>
<div className="w-full border-t border-gray-300" />
</div>
</div>
<BotSetting />
</Fragment>
)}
</motion.div>
)
}
export default RightContainer

View File

@ -1,4 +1,3 @@
import Image from 'next/image'
import useCreateConversation from '@/_hooks/useCreateConversation'
import PrimaryButton from '../PrimaryButton'
import { useAtomValue, useSetAtom } from 'jotai'
@ -11,6 +10,7 @@ import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom'
import useInitModel from '@/_hooks/useInitModel'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import { AssistantModel } from '@/_models/AssistantModel'
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline"
enum ActionButton {
DownloadModel = 'Download a Model',
@ -53,12 +53,7 @@ const SidebarEmptyHistory: React.FC = () => {
return (
<div className="flex flex-col items-center gap-3 py-10">
<Image
src={'icons/chat-bubble-oval-left.svg'}
width={32}
height={32}
alt=""
/>
<ChatBubbleOvalLeftEllipsisIcon width={32} height={32} />
<div className="flex flex-col items-center gap-6">
<div className="text-center text-sm text-gray-900">No Chat History</div>
<div className="text-center text-sm text-gray-500">

View File

@ -1,36 +0,0 @@
import React from 'react'
import SidebarMenuItem from '../SidebarMenuItem'
import { MainViewState } from '@/_helpers/atoms/MainView.atom'
const menu = [
{
name: 'Explore Models',
icon: 'Search_gray',
state: MainViewState.ExploreModel,
},
{
name: 'My Models',
icon: 'ViewGrid',
state: MainViewState.MyModel,
},
{
name: 'Settings',
icon: 'Cog',
state: MainViewState.Setting,
},
]
const SidebarMenu: React.FC = () => (
<ul role="list" className="mx-1 mb-2 mt-2 space-y-1">
{menu.map((item) => (
<SidebarMenuItem
title={item.name}
viewState={item.state}
iconName={item.icon}
key={item.name}
/>
))}
</ul>
)
export default React.memo(SidebarMenu)

View File

@ -3,6 +3,7 @@ import Link from 'next/link'
import React from 'react'
import JanImage from './JanImage'
import Image from 'next/image'
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline"
type Props = {
avatarUrl?: string
@ -51,7 +52,7 @@ const SimpleControlNetMessage: React.FC<Props> = ({
target="_blank_"
className="flex items-center gap-1 rounded-xl bg-[#F3F4F6] px-2 py-1"
>
<Image src="icons/download.svg" width={16} height={16} alt="" />
<ArrowDownTrayIcon width={16} height={16} />
<span className="text-[14px] leading-[20px] text-[#111928]">
Download
</span>

View File

@ -1,35 +0,0 @@
import Image from 'next/image'
type Props = {
onTabClick: (clickedTab: 'description' | 'api') => void
tab: string
}
export const TabModelDetail: React.FC<Props> = ({ onTabClick, tab }) => {
const btns = [
{
name: 'api',
icon: 'icons/unicorn_arrow.svg',
},
{
name: 'description',
icon: 'icons/unicorn_exclamation-circle.svg',
},
]
return (
<div className="flex w-full gap-0.5 rounded bg-gray-200 p-1">
{btns.map((item, index) => (
<button
key={index}
onClick={() => onTabClick(item.name as 'description' | 'api')}
className={`relative flex w-1/2 items-center justify-center gap-2 px-3 py-[6px] text-sm capitalize leading-5 ${
tab !== item.name ? '' : 'rounded bg-white shadow-sm'
}`}
>
<Image src={item.icon} width={20} height={20} alt="" />
{item.name}
</button>
))}
</div>
)
}

View File

@ -0,0 +1,48 @@
import React from "react";
import { useController } from "react-hook-form";
type Props = {
id: string;
title: string;
placeholder: string;
description?: string;
control?: any;
required?: boolean;
};
const TextAreaWithTitle: React.FC<Props> = ({
id,
title,
placeholder,
description,
control,
required = false,
}) => {
const { field } = useController({
name: id,
control: control,
rules: { required: required },
});
return (
<div className="flex flex-col gap-2">
<label
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
{title}
</label>
{description && (
<p className="text-sm text-gray-400 font-normal">{description}</p>
)}
<textarea
rows={4}
className="block w-full resize-none rounded-md border-0 py-1.5 bg-transparent shadow-sm ring-1 ring-inset text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder={placeholder}
{...field}
/>
</div>
);
};
export default TextAreaWithTitle;

View File

@ -0,0 +1,40 @@
import React from "react";
import { useController } from "react-hook-form";
type Props = {
id: string;
title: string;
description: string;
placeholder?: string;
control?: any;
required?: boolean;
};
const TextInputWithTitle: React.FC<Props> = ({
id,
title,
description,
placeholder,
control,
required = false,
}) => {
const { field } = useController({
name: id,
control: control,
rules: { required: required },
});
return (
<div className="flex flex-col gap-2">
<div className="text-gray-900 font-bold">{title}</div>
<div className="text-sm pb-2 text-[#737d7d]">{description}</div>
<input
className="block w-full rounded-md border-0 py-1.5 bg-transparent shadow-sm ring-1 ring-inset text-gray-900 ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder={placeholder}
{...field}
/>
</div>
);
};
export default TextInputWithTitle;

View File

@ -0,0 +1,51 @@
import React from "react";
import { Switch } from "@headlessui/react";
import { Controller } from "react-hook-form";
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
type Props = {
id: string;
title: string;
control: any;
required?: boolean;
};
const ToggleSwitch: React.FC<Props> = ({
id,
title,
control,
required = false,
}) => (
<div className="flex items-center justify-between">
<div className="text-gray-900 text-base">{title}</div>
<Controller
name={id}
control={control}
rules={{ required }}
render={({ field: { value, onChange } }) => (
<Switch
checked={value}
onChange={onChange}
className={classNames(
value ? "bg-indigo-600" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
)}
>
<span className="sr-only">Use setting</span>
<span
aria-hidden="true"
className={classNames(
value ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}
/>
</Switch>
)}
/>
</div>
);
export default ToggleSwitch;

View File

@ -1,29 +0,0 @@
'use client'
import { currentConversationAtom } from '@/_helpers/atoms/Conversation.atom'
import { useAtomValue } from 'jotai'
import Image from 'next/image'
const UserToolbar: React.FC = () => {
const currentConvo = useAtomValue(currentConversationAtom)
const avatarUrl = currentConvo?.image
const title = currentConvo?.summary ?? currentConvo?.name ?? ''
return (
<div className="flex items-center gap-3 p-1">
<Image
className="aspect-square h-8 w-8 rounded-full"
src={avatarUrl ?? 'icons/app_icon.svg'}
alt=""
width={36}
height={36}
/>
<span className="flex gap-0.5 text-base font-semibold leading-6">
{title}
</span>
</div>
)
}
export default UserToolbar

View File

@ -1,10 +1,11 @@
'use client'
import ConfirmDeleteConversationModal from '@/_components/ConfirmDeleteConversationModal'
import ConfirmDeleteModelModal from '@/_components/ConfirmDeleteModelModal'
import ConfirmSignOutModal from '@/_components/ConfirmSignOutModal'
import MobileMenuPane from '@/_components/MobileMenuPane'
import { ReactNode } from 'react'
import BotListModal from "@/_components/BotListModal"
import ConfirmDeleteConversationModal from "@/_components/ConfirmDeleteConversationModal"
import ConfirmDeleteModelModal from "@/_components/ConfirmDeleteModelModal"
import ConfirmSignOutModal from "@/_components/ConfirmSignOutModal"
import MobileMenuPane from "@/_components/MobileMenuPane"
import { ReactNode } from "react"
type Props = {
children: ReactNode
@ -16,6 +17,7 @@ export const ModalWrapper: React.FC<Props> = ({ children }) => (
<ConfirmDeleteConversationModal />
<ConfirmSignOutModal />
<ConfirmDeleteModelModal />
<BotListModal />
{children}
</>
)

View File

@ -0,0 +1,4 @@
import { Bot } from "@/_models/Bot";
import { atom } from "jotai";
export const activeBotAtom = atom<Bot | undefined>(undefined);

View File

@ -1,6 +1,6 @@
import { atom } from 'jotai'
import { MainViewState, setMainViewStateAtom } from './MainView.atom'
import { Conversation, ConversationState } from '@/_models/Conversation'
import { atom } from "jotai"
import { MainViewState, setMainViewStateAtom } from "./MainView.atom"
import { Conversation, ConversationState } from "@/_models/Conversation"
/**
* Stores the current active conversation id.
@ -16,7 +16,6 @@ export const setActiveConvoIdAtom = atom(
console.debug(`Set active conversation id: ${convoId}`)
set(setMainViewStateAtom, MainViewState.Conversation)
}
set(activeConversationIdAtom, convoId)
}
)

View File

@ -4,3 +4,7 @@ import { atom } from 'jotai'
* Stores expand state of conversation container. Default is true.
*/
export const leftSideBarExpandStateAtom = atom<boolean>(true)
export const rightSideBarExpandStateAtom = atom<boolean>(false)
export const showRightSideBarToggleAtom = atom<boolean>(true)

View File

@ -1,9 +1,14 @@
import { atom } from 'jotai'
import { setActiveConvoIdAtom } from './Conversation.atom'
import { systemBarVisibilityAtom } from './SystemBar.atom'
import {
rightSideBarExpandStateAtom,
showRightSideBarToggleAtom,
} from './LeftSideBarExpand.atom'
export enum MainViewState {
Welcome,
CreateBot,
ExploreModel,
MyModel,
ResourceMonitor,
@ -14,6 +19,8 @@ export enum MainViewState {
* When user wants to create new conversation but haven't selected a model yet.
*/
ConversationEmptyModel,
BotInfo,
}
/**
@ -40,6 +47,13 @@ export const setMainViewStateAtom = atom(
set(setActiveConvoIdAtom, undefined)
}
if (state in [MainViewState.Welcome, MainViewState.CreateBot]) {
set(showRightSideBarToggleAtom, false)
set(rightSideBarExpandStateAtom, false)
} else {
set(showRightSideBarToggleAtom, true)
}
const showSystemBar =
state !== MainViewState.Conversation &&
state !== MainViewState.ConversationEmptyModel

View File

@ -6,3 +6,4 @@ export const showConfirmDeleteModalAtom = atom(false)
export const showingAdvancedPromptAtom = atom<boolean>(false)
export const showingProductDetailAtom = atom<boolean>(false)
export const showingMobilePaneAtom = atom<boolean>(false)
export const showingBotListModalAtom = atom<boolean>(false)

View File

@ -0,0 +1,27 @@
import { Bot } from '@/_models/Bot'
import { executeSerial } from '../../../electron/core/plugin-manager/execution/extension-manager'
import { DataService } from '@janhq/core'
import {
MainViewState,
setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom'
import { useSetAtom } from 'jotai'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom'
export default function useCreateBot() {
const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const createBot = async (bot: Bot) => {
try {
await executeSerial(DataService.CreateBot, bot)
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
} catch (err) {
alert(err)
console.error(err)
}
}
return { createBot }
}

View File

@ -1,7 +1,7 @@
import { useAtom, useSetAtom } from 'jotai'
import { Conversation } from '@/_models/Conversation'
import { executeSerial } from '@/_services/pluginService'
import { DataService } from '@janhq/core'
import { useAtom, useSetAtom } from "jotai"
import { Conversation } from "@/_models/Conversation"
import { executeSerial } from "@/_services/pluginService"
import { DataService, ModelManagementService } from "@janhq/core"
import {
userConversationsAtom,
setActiveConvoIdAtom,
@ -11,6 +11,7 @@ import {
} from '@/_helpers/atoms/Conversation.atom'
import useInitModel from './useInitModel'
import { AssistantModel } from '@/_models/AssistantModel'
import { Bot } from "@/_models/Bot"
const useCreateConversation = () => {
const { initModel } = useInitModel()
@ -22,13 +23,30 @@ const useCreateConversation = () => {
const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
const updateConvError = useSetAtom(updateConversationErrorAtom)
const requestCreateConvo = async (model: AssistantModel) => {
const createConvoByBot = async (bot: Bot) => {
const model = await executeSerial(
ModelManagementService.GetModelById,
bot.modelId
)
if (!model) {
alert(
`Model ${bot.modelId} not found! Please re-download the model first.`
)
return
}
return requestCreateConvo(model, bot)
}
const requestCreateConvo = async (model: AssistantModel, bot?: Bot) => {
const conversationName = model.name
const conv: Conversation = {
modelId: model._id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
name: conversationName,
botId: bot?._id ?? undefined,
}
const id = await executeSerial(DataService.CreateConversation, conv)
@ -46,6 +64,7 @@ const useCreateConversation = () => {
name: conversationName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
botId: bot?._id ?? undefined,
}
addNewConvoState(id ?? '', {
@ -57,6 +76,7 @@ const useCreateConversation = () => {
}
return {
createConvoByBot,
requestCreateConvo,
}
}

View File

@ -0,0 +1,25 @@
import { useSetAtom } from "jotai";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { activeBotAtom } from "@/_helpers/atoms/Bot.atom";
import { rightSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom";
import { DataService } from "@janhq/core";
export default function useDeleteBot() {
const setActiveBot = useSetAtom(activeBotAtom);
const setRightPanelVisibility = useSetAtom(rightSideBarExpandStateAtom);
const deleteBot = async (botId: string): Promise<"success" | "failed"> => {
try {
await executeSerial(DataService.DeleteBot, botId);
setRightPanelVisibility(false);
setActiveBot(undefined);
return "success";
} catch (err) {
alert(`Failed to delete bot ${botId}: ${err}`);
console.error(err);
return "failed";
}
};
return { deleteBot };
}

View File

@ -0,0 +1,29 @@
import { Bot } from "@/_models/Bot";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "@janhq/core";
export default function useGetBots() {
const getAllBots = async (): Promise<Bot[]> => {
try {
const bots = await executeSerial(DataService.GetBots);
return bots;
} catch (err) {
alert(`Failed to get bots: ${err}`);
console.error(err);
return [];
}
};
const getBotById = async (botId: string): Promise<Bot | undefined> => {
try {
const bot: Bot = await executeSerial(DataService.GetBotById, botId);
return bot;
} catch (err) {
alert(`Failed to get bot ${botId}: ${err}`);
console.error(err);
return undefined;
}
};
return { getBotById, getAllBots };
}

View File

@ -18,7 +18,7 @@ export default function useInitModel() {
const res = await executeSerial(InferenceService.InitModel, model._id)
if (res?.error) {
console.log('error occured: ', res)
console.error('Failed to init model: ', res.error)
return res
} else {
console.debug(

View File

@ -0,0 +1,38 @@
import { Bot } from "@/_models/Bot";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "@janhq/core";
export default function useUpdateBot() {
const updateBot = async (
bot: Bot,
updatableField: UpdatableField
): Promise<void> => {
try {
// TODO: if bot does not changed, no need to update
for (const [key, value] of Object.entries(updatableField)) {
if (value !== undefined) {
//@ts-ignore
bot[key] = value;
}
}
await executeSerial(DataService.UpdateBot, bot);
console.debug("Bot updated", JSON.stringify(bot, null, 2));
} catch (err) {
alert(`Update bot error: ${err}`);
console.error(err);
return;
}
};
return { updateBot };
}
export type UpdatableField = {
presencePenalty?: number;
frequencyPenalty?: number;
maxTokens?: number;
customTemperature?: number;
systemPrompt?: number;
};

32
web/app/_models/Bot.ts Normal file
View File

@ -0,0 +1,32 @@
export type Bot = {
_id: string;
name: string;
description: string;
visibleFromBotProfile: boolean;
systemPrompt: string;
welcomeMessage: string;
publiclyAccessible: boolean;
suggestReplies: boolean;
renderMarkdownContent: boolean;
/**
* If true, the bot will use the custom temperature value instead of the
* default temperature value.
*/
enableCustomTemperature: boolean;
/**
* Default is 0.7.
*/
customTemperature: number;
maxTokens: number;
frequencyPenalty: number;
presencePenalty: number;
modelId: string;
createdAt?: number;
updatedAt?: number;
};

View File

@ -8,6 +8,7 @@ export interface Conversation {
summary?: string
createdAt?: string
updatedAt?: string
botId?: string
}
/**

View File

@ -18,3 +18,15 @@ export const formatDownloadSpeed = (input: number | undefined) => {
if (!input) return '0B/s'
return toGigabytes(input) + '/s'
}
export const formatAsFixed = (input: number) => {
input = Number(input)
return input.toFixed(0)
}
export const formatTwoDigits = (input: number) => {
// convert input from string to number
input = Number(input)
return input.toFixed(2)
}

View File

@ -1,10 +1,10 @@
'use client'
import { PluginService } from '@janhq/core'
import { ThemeWrapper } from './_helpers/ThemeWrapper'
import JotaiWrapper from './_helpers/JotaiWrapper'
import { ModalWrapper } from './_helpers/ModalWrapper'
import { useEffect, useState } from 'react'
import Image from 'next/image'
"use client"
import { PluginService } from "@janhq/core"
import { ThemeWrapper } from "./_helpers/ThemeWrapper"
import JotaiWrapper from "./_helpers/JotaiWrapper"
import { ModalWrapper } from "./_helpers/ModalWrapper"
import { useEffect, useState } from "react"
import Image from "next/image"
import {
setup,
plugins,
@ -72,20 +72,20 @@ const Page: React.FC = () => {
{setupCore && (
<EventListenerWrapper>
<ThemeWrapper>
<ModalWrapper>
{activated ? (
{activated ? (
<ModalWrapper>
<MainContainer />
) : (
<div className="flex h-screen w-screen items-center justify-center bg-white">
<Image
width={50}
height={50}
src="icons/app_icon.svg"
alt=""
/>
</div>
)}
</ModalWrapper>
</ModalWrapper>
) : (
<div className="flex h-screen w-screen items-center justify-center bg-white">
<Image
width={50}
height={50}
src="icons/app_icon.svg"
alt=""
/>
</div>
)}
</ThemeWrapper>
</EventListenerWrapper>
)}

View File

@ -14,7 +14,7 @@
"dependencies": {
"@headlessui/react": "^1.7.15",
"@heroicons/react": "^2.0.18",
"@janhq/core": "^0.1.2",
"@janhq/core": "^0.1.6",
"@tailwindcss/typography": "^0.5.9",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
@ -40,13 +40,15 @@
"react-dom": "18.2.0",
"react-hook-form": "^7.45.4",
"tailwindcss": "3.3.3",
"typescript": "5.1.6"
"typescript": "5.1.6",
"uuid": "^9.0.1"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.4",
"@types/node": "20.6.5",
"encoding": "^0.1.13",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6"
"prettier-plugin-tailwindcss": "^0.5.6",
"@types/uuid": "^9.0.6"
}
}

View File

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.3246 4.31731C10.751 2.5609 13.249 2.5609 13.6754 4.31731C13.9508 5.45193 15.2507 5.99038 16.2478 5.38285C17.7913 4.44239 19.5576 6.2087 18.6172 7.75218C18.0096 8.74925 18.5481 10.0492 19.6827 10.3246C21.4391 10.751 21.4391 13.249 19.6827 13.6754C18.5481 13.9508 18.0096 15.2507 18.6172 16.2478C19.5576 17.7913 17.7913 19.5576 16.2478 18.6172C15.2507 18.0096 13.9508 18.5481 13.6754 19.6827C13.249 21.4391 10.751 21.4391 10.3246 19.6827C10.0492 18.5481 8.74926 18.0096 7.75219 18.6172C6.2087 19.5576 4.44239 17.7913 5.38285 16.2478C5.99038 15.2507 5.45193 13.9508 4.31731 13.6754C2.5609 13.249 2.5609 10.751 4.31731 10.3246C5.45193 10.0492 5.99037 8.74926 5.38285 7.75218C4.44239 6.2087 6.2087 4.44239 7.75219 5.38285C8.74926 5.99037 10.0492 5.45193 10.3246 4.31731Z" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

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="M21 21L15 15M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10Z" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 318 B

View File

@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 27C22.6274 27 28 22.0751 28 16C28 9.92487 22.6274 5 16 5C9.37258 5 4 9.92487 4 16C4 18.9181 5.2396 21.5709 7.26245 23.5399C7.70761 23.9732 8.00605 24.565 7.88518 25.1744C7.68369 26.1902 7.22576 27.1137 6.58105 27.8746C6.78906 27.9119 7 27.941 7.21289 27.9618C7.47168 27.9871 7.73438 28 8 28C9.70934 28 11.2935 27.4638 12.5936 26.5505C13.6734 26.843 14.8167 27 16 27Z" stroke="#6B7280" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 569 B

View File

@ -1,5 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.59333 10.9997C4.40667 10.9997 4.23333 10.8997 4.14667 10.7264C4.02 10.4797 4.12 10.1797 4.37333 10.053C4.95333 9.76636 5.44667 9.32636 5.8 8.79302C5.92 8.61302 5.92 8.38636 5.8 8.20636C5.44 7.67302 4.94667 7.23302 4.37333 6.94636C4.12 6.82636 4.02 6.52636 4.14667 6.27302C4.26667 6.02636 4.56667 5.92636 4.81333 6.05302C5.54667 6.41969 6.17333 6.97302 6.62667 7.65302C6.96667 8.16636 6.96667 8.83302 6.62667 9.34636C6.17333 10.0264 5.54667 10.5797 4.81333 10.9464C4.74667 10.9797 4.66667 10.9997 4.59333 10.9997Z" fill="white"/>
<path d="M11.3333 11H8.66666C8.39332 11 8.16666 10.7733 8.16666 10.5C8.16666 10.2267 8.39332 10 8.66666 10H11.3333C11.6067 10 11.8333 10.2267 11.8333 10.5C11.8333 10.7733 11.6067 11 11.3333 11Z" fill="white"/>
<path d="M10 15.6663H6.00001C2.38001 15.6663 0.833344 14.1197 0.833344 10.4997V6.49967C0.833344 2.87967 2.38001 1.33301 6.00001 1.33301H10C13.62 1.33301 15.1667 2.87967 15.1667 6.49967V10.4997C15.1667 14.1197 13.62 15.6663 10 15.6663ZM6.00001 2.33301C2.92668 2.33301 1.83334 3.42634 1.83334 6.49967V10.4997C1.83334 13.573 2.92668 14.6663 6.00001 14.6663H10C13.0733 14.6663 14.1667 13.573 14.1667 10.4997V6.49967C14.1667 3.42634 13.0733 2.33301 10 2.33301H6.00001Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.52667 9.35992C5.46418 9.42189 5.41459 9.49563 5.38074 9.57687C5.34689 9.65811 5.32947 9.74524 5.32947 9.83325C5.32947 9.92126 5.34689 10.0084 5.38074 10.0896C5.41459 10.1709 5.46418 10.2446 5.52667 10.3066L7.52667 12.3066C7.58864 12.3691 7.66238 12.4187 7.74362 12.4525C7.82486 12.4864 7.91199 12.5038 8 12.5038C8.08801 12.5038 8.17515 12.4864 8.25638 12.4525C8.33762 12.4187 8.41136 12.3691 8.47333 12.3066L10.4733 10.3066C10.5989 10.181 10.6694 10.0108 10.6694 9.83325C10.6694 9.65572 10.5989 9.48545 10.4733 9.35992C10.3478 9.23438 10.1775 9.16386 10 9.16386C9.82247 9.16386 9.6522 9.23438 9.52667 9.35992L8.66667 10.2266V2.49992C8.66667 2.32311 8.59643 2.15354 8.4714 2.02851C8.34638 1.90349 8.17681 1.83325 8 1.83325C7.82319 1.83325 7.65362 1.90349 7.5286 2.02851C7.40357 2.15354 7.33333 2.32311 7.33333 2.49992V10.2266L6.47333 9.35992C6.41136 9.29743 6.33762 9.24784 6.25638 9.21399C6.17515 9.18015 6.08801 9.16272 6 9.16272C5.91199 9.16272 5.82486 9.18015 5.74362 9.21399C5.66238 9.24784 5.58864 9.29743 5.52667 9.35992ZM12 6.49992H10.6667C10.4899 6.49992 10.3203 6.57016 10.1953 6.69518C10.0702 6.82021 10 6.98977 10 7.16659C10 7.3434 10.0702 7.51297 10.1953 7.63799C10.3203 7.76301 10.4899 7.83325 10.6667 7.83325H12C12.1768 7.83325 12.3464 7.90349 12.4714 8.02851C12.5964 8.15354 12.6667 8.32311 12.6667 8.49992V13.1666C12.6667 13.3434 12.5964 13.513 12.4714 13.638C12.3464 13.763 12.1768 13.8333 12 13.8333H4C3.82319 13.8333 3.65362 13.763 3.5286 13.638C3.40357 13.513 3.33333 13.3434 3.33333 13.1666V8.49992C3.33333 8.32311 3.40357 8.15354 3.5286 8.02851C3.65362 7.90349 3.82319 7.83325 4 7.83325H5.33333C5.51014 7.83325 5.67971 7.76301 5.80474 7.63799C5.92976 7.51297 6 7.3434 6 7.16659C6 6.98977 5.92976 6.82021 5.80474 6.69518C5.67971 6.57016 5.51014 6.49992 5.33333 6.49992H4C3.46957 6.49992 2.96086 6.71063 2.58579 7.0857C2.21071 7.46078 2 7.96949 2 8.49992V13.1666C2 13.697 2.21071 14.2057 2.58579 14.5808C2.96086 14.9559 3.46957 15.1666 4 15.1666H12C12.5304 15.1666 13.0391 14.9559 13.4142 14.5808C13.7893 14.2057 14 13.697 14 13.1666V8.49992C14 7.96949 13.7893 7.46078 13.4142 7.0857C13.0391 6.71063 12.5304 6.49992 12 6.49992Z" fill="#111928"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,9 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.8 2.63965H2.2C0.99 2.63965 0 3.62965 0 4.83965V17.1596C0 18.3696 0.99 19.3596 2.2 19.3596H19.8C21.01 19.3596 22 18.3696 22 17.1596V4.83965C22 3.62965 21.01 2.63965 19.8 2.63965ZM14.08 18.4796H2.2C1.474 18.4796 0.88 17.8856 0.88 17.1596V4.83965C0.88 4.11365 1.474 3.51965 2.2 3.51965H14.08V18.4796ZM21.12 17.1596C21.12 17.8856 20.526 18.4796 19.8 18.4796H14.96V3.51965H19.8C20.526 3.51965 21.12 4.11365 21.12 4.83965V17.1596Z" fill="#6B7280"/>
<path d="M2.6402 5.93957H3.0802C3.3222 5.93957 3.5202 5.74157 3.5202 5.49957C3.5202 5.25757 3.3222 5.05957 3.0802 5.05957H2.6402C2.3982 5.05957 2.2002 5.25757 2.2002 5.49957C2.2002 5.74157 2.3982 5.93957 2.6402 5.93957Z" fill="#6B7280"/>
<path d="M4.61969 5.93957H5.05969C5.30169 5.93957 5.49969 5.74157 5.49969 5.49957C5.49969 5.25757 5.30169 5.05957 5.05969 5.05957H4.61969C4.37769 5.05957 4.17969 5.25757 4.17969 5.49957C4.17969 5.74157 4.37769 5.93957 4.61969 5.93957Z" fill="#6B7280"/>
<path d="M6.60016 5.93957H7.04016C7.28216 5.93957 7.48016 5.74157 7.48016 5.49957C7.48016 5.25757 7.28216 5.05957 7.04016 5.05957H6.60016C6.35816 5.05957 6.16016 5.25757 6.16016 5.49957C6.16016 5.74157 6.35816 5.93957 6.60016 5.93957Z" fill="#6B7280"/>
<path d="M18.6997 6.37988H17.1597C16.9177 6.37988 16.7197 6.57788 16.7197 6.81988C16.7197 7.06188 16.9177 7.25988 17.1597 7.25988H18.6997C18.9417 7.25988 19.1397 7.06188 19.1397 6.81988C19.1397 6.57788 18.9417 6.37988 18.6997 6.37988Z" fill="#6B7280"/>
<path d="M18.6997 8.58008H17.1597C16.9177 8.58008 16.7197 8.77808 16.7197 9.02008C16.7197 9.26208 16.9177 9.46008 17.1597 9.46008H18.6997C18.9417 9.46008 19.1397 9.26208 19.1397 9.02008C19.1397 8.77808 18.9417 8.58008 18.6997 8.58008Z" fill="#6B7280"/>
<path d="M18.6997 10.7793H17.1597C16.9177 10.7793 16.7197 10.9773 16.7197 11.2193C16.7197 11.4613 16.9177 11.6593 17.1597 11.6593H18.6997C18.9417 11.6593 19.1397 11.4613 19.1397 11.2193C19.1397 10.9773 18.9417 10.7793 18.6997 10.7793Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,9 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.8 2.63965H2.2C0.99 2.63965 0 3.62965 0 4.83965V17.1596C0 18.3696 0.99 19.3596 2.2 19.3596H19.8C21.01 19.3596 22 18.3696 22 17.1596V4.83965C22 3.62965 21.01 2.63965 19.8 2.63965ZM14.08 18.4796H2.2C1.474 18.4796 0.88 17.8856 0.88 17.1596V4.83965C0.88 4.11365 1.474 3.51965 2.2 3.51965H14.08V18.4796ZM21.12 17.1596C21.12 17.8856 20.526 18.4796 19.8 18.4796H14.96V3.51965H19.8C20.526 3.51965 21.12 4.11365 21.12 4.83965V17.1596Z" fill="#6B7280"/>
<path d="M2.63922 5.93957H3.07922C3.32122 5.93957 3.51922 5.74157 3.51922 5.49957C3.51922 5.25757 3.32122 5.05957 3.07922 5.05957H2.63922C2.39722 5.05957 2.19922 5.25757 2.19922 5.49957C2.19922 5.74157 2.39722 5.93957 2.63922 5.93957Z" fill="#6B7280"/>
<path d="M4.61969 5.93957H5.05969C5.30169 5.93957 5.49969 5.74157 5.49969 5.49957C5.49969 5.25757 5.30169 5.05957 5.05969 5.05957H4.61969C4.37769 5.05957 4.17969 5.25757 4.17969 5.49957C4.17969 5.74157 4.37769 5.93957 4.61969 5.93957Z" fill="#6B7280"/>
<path d="M6.60016 5.93957H7.04016C7.28216 5.93957 7.48016 5.74157 7.48016 5.49957C7.48016 5.25757 7.28216 5.05957 7.04016 5.05957H6.60016C6.35816 5.05957 6.16016 5.25757 6.16016 5.49957C6.16016 5.74157 6.35816 5.93957 6.60016 5.93957Z" fill="#6B7280"/>
<path d="M18.7007 6.37988H17.1607C16.9187 6.37988 16.7207 6.57788 16.7207 6.81988C16.7207 7.06188 16.9187 7.25988 17.1607 7.25988H18.7007C18.9427 7.25988 19.1407 7.06188 19.1407 6.81988C19.1407 6.57788 18.9427 6.37988 18.7007 6.37988Z" fill="#6B7280"/>
<path d="M18.7007 8.58008H17.1607C16.9187 8.58008 16.7207 8.77808 16.7207 9.02008C16.7207 9.26208 16.9187 9.46008 17.1607 9.46008H18.7007C18.9427 9.46008 19.1407 9.26208 19.1407 9.02008C19.1407 8.77808 18.9427 8.58008 18.7007 8.58008Z" fill="#6B7280"/>
<path d="M18.7007 10.7793H17.1607C16.9187 10.7793 16.7207 10.9773 16.7207 11.2193C16.7207 11.4613 16.9187 11.6593 17.1607 11.6593H18.7007C18.9427 11.6593 19.1407 11.4613 19.1407 11.2193C19.1407 10.9773 18.9427 10.7793 18.7007 10.7793Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,4 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="20" height="20" rx="10" fill="#6C2BD9"/>
<path d="M14.8516 5.41184C13.9593 4.98979 13.0024 4.67884 12.0019 4.50075C11.9837 4.49731 11.9655 4.5059 11.9561 4.52308C11.8331 4.7487 11.6968 5.04304 11.6013 5.27439C10.5252 5.10833 9.45469 5.10833 8.40069 5.27439C8.30521 5.0379 8.16395 4.7487 8.04034 4.52308C8.03095 4.50647 8.01275 4.49788 7.99453 4.50075C6.99461 4.67827 6.03774 4.98922 5.14488 5.41184C5.13715 5.41528 5.13052 5.42101 5.12613 5.42845C3.31115 8.22357 2.81395 10.95 3.05786 13.6426C3.05896 13.6558 3.06613 13.6684 3.07607 13.6764C4.27354 14.5829 5.4335 15.1332 6.57191 15.498C6.59013 15.5037 6.60944 15.4969 6.62103 15.4814C6.89032 15.1023 7.13037 14.7026 7.33619 14.2823C7.34834 14.2576 7.33675 14.2284 7.31192 14.2187C6.93116 14.0698 6.5686 13.8883 6.21984 13.6821C6.19226 13.6655 6.19005 13.6248 6.21543 13.6054C6.28882 13.5487 6.36223 13.4897 6.43231 13.4301C6.44499 13.4193 6.46265 13.417 6.47756 13.4238C8.76875 14.5022 11.2492 14.5022 13.5134 13.4238C13.5283 13.4164 13.546 13.4187 13.5592 13.4296C13.6293 13.4891 13.7027 13.5487 13.7766 13.6054C13.802 13.6248 13.8003 13.6655 13.7728 13.6821C13.424 13.8923 13.0614 14.0698 12.6801 14.2181C12.6553 14.2279 12.6443 14.2576 12.6564 14.2823C12.8666 14.702 13.1067 15.1017 13.371 15.4808C13.3821 15.4969 13.4019 15.5037 13.4201 15.498C14.5641 15.1332 15.724 14.5829 16.9215 13.6764C16.932 13.6684 16.9386 13.6563 16.9397 13.6432C17.2316 10.5302 16.4508 7.82615 14.8698 5.42902C14.8659 5.42101 14.8593 5.41528 14.8516 5.41184ZM7.67835 12.0031C6.98854 12.0031 6.42016 11.3503 6.42016 10.5485C6.42016 9.74683 6.97752 9.09401 7.67835 9.09401C8.38468 9.09401 8.94756 9.75256 8.93651 10.5485C8.93651 11.3503 8.37916 12.0031 7.67835 12.0031ZM12.3303 12.0031C11.6405 12.0031 11.0721 11.3503 11.0721 10.5485C11.0721 9.74683 11.6294 9.09401 12.3303 9.09401C13.0366 9.09401 13.5995 9.75256 13.5885 10.5485C13.5885 11.3503 13.0366 12.0031 12.3303 12.0031Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -1,35 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin:auto;background:transparent;display:block;" width="24px" height="24px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g transform="rotate(0 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.875s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(45 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(90 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.625s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(135 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(180 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.375s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(225 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(270 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.125s" repeatCount="indefinite"></animate>
</rect>
</g><g transform="rotate(315 50 50)">
<rect x="47" y="24" rx="3" ry="4.08" width="6" height="12" fill="#9ca3af">
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
</rect>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 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 d="M5.2465 14.187C4.71984 14.187 4.21984 14.0603 3.77984 13.807C2.73984 13.207 2.1665 11.987 2.1665 10.3803V5.62698C2.1665 4.01365 2.73984 2.80032 3.77984 2.20032C4.81984 1.60032 6.15984 1.71365 7.55984 2.52032L11.6732 4.89365C13.0665 5.70032 13.8398 6.80699 13.8398 8.00699C13.8398 9.20699 13.0732 10.3137 11.6732 11.1203L7.55984 13.4937C6.75317 13.9537 5.9665 14.187 5.2465 14.187ZM5.2465 2.81365C4.8865 2.81365 4.5665 2.89365 4.27984 3.06032C3.55984 3.47365 3.1665 4.38698 3.1665 5.62698V10.3737C3.1665 11.6137 3.55984 12.5203 4.27984 12.9403C4.99984 13.3603 5.9865 13.2403 7.05984 12.6203L11.1732 10.247C12.2465 9.62699 12.8398 8.83365 12.8398 8.00032C12.8398 7.16699 12.2465 6.37365 11.1732 5.75365L7.05984 3.38032C6.4065 3.00699 5.79317 2.81365 5.2465 2.81365Z" fill="#6B7280"/>
</svg>

Before

Width:  |  Height:  |  Size: 894 B

View File

@ -1,3 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6667 8.49992C14.6667 12.1799 11.68 15.1666 8.00001 15.1666C4.32001 15.1666 2.07334 11.4599 2.07334 11.4599M2.07334 11.4599H5.08668M2.07334 11.4599V14.7933M1.33334 8.49992C1.33334 4.81992 4.29334 1.83325 8.00001 1.83325C12.4467 1.83325 14.6667 5.53992 14.6667 5.53992M14.6667 5.53992V2.20659M14.6667 5.53992H11.7067" stroke="#111928" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 517 B

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.0002 9.16994C16.8128 8.98369 16.5594 8.87915 16.2952 8.87915C16.031 8.87915 15.7776 8.98369 15.5902 9.16994L12.0002 12.7099L8.46019 9.16994C8.27283 8.98369 8.01938 8.87915 7.75519 8.87915C7.49101 8.87915 7.23756 8.98369 7.05019 9.16994C6.95646 9.26291 6.88207 9.37351 6.8313 9.49537C6.78053 9.61723 6.75439 9.74793 6.75439 9.87994C6.75439 10.012 6.78053 10.1427 6.8313 10.2645C6.88207 10.3864 6.95646 10.497 7.05019 10.5899L11.2902 14.8299C11.3832 14.9237 11.4938 14.9981 11.6156 15.0488C11.7375 15.0996 11.8682 15.1257 12.0002 15.1257C12.1322 15.1257 12.2629 15.0996 12.3848 15.0488C12.5066 14.9981 12.6172 14.9237 12.7102 14.8299L17.0002 10.5899C17.0939 10.497 17.1683 10.3864 17.2191 10.2645C17.2699 10.1427 17.296 10.012 17.296 9.87994C17.296 9.74793 17.2699 9.61723 17.2191 9.49537C17.1683 9.37351 17.0939 9.26291 17.0002 9.16994Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 968 B

View File

@ -1,3 +0,0 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59162 5.24162C8.51415 5.16351 8.42198 5.10151 8.32043 5.05921C8.21888 5.0169 8.10996 4.99512 7.99995 4.99512C7.88994 4.99512 7.78102 5.0169 7.67947 5.05921C7.57792 5.10151 7.48575 5.16351 7.40828 5.24162L3.24162 9.40828C3.16351 9.48575 3.10151 9.57792 3.05921 9.67947C3.0169 9.78102 2.99512 9.88994 2.99512 9.99995C2.99512 10.11 3.0169 10.2189 3.05921 10.3204C3.10151 10.422 3.16351 10.5141 3.24162 10.5916L7.40828 14.7583C7.48575 14.8364 7.57792 14.8984 7.67947 14.9407C7.78102 14.983 7.88994 15.0048 7.99995 15.0048C8.10996 15.0048 8.21888 14.983 8.32043 14.9407C8.42198 14.8984 8.51415 14.8364 8.59162 14.7583C8.66972 14.6808 8.73172 14.5886 8.77402 14.4871C8.81633 14.3855 8.83811 14.2766 8.83811 14.1666C8.83811 14.0566 8.81633 13.9477 8.77402 13.8461C8.73172 13.7446 8.66972 13.6524 8.59162 13.575L5.00828 9.99995L8.59162 6.42495C8.66972 6.34748 8.73172 6.25531 8.77402 6.15376C8.81633 6.05221 8.83811 5.94329 8.83811 5.83328C8.83811 5.72327 8.81633 5.61435 8.77402 5.5128C8.73172 5.41125 8.66972 5.31908 8.59162 5.24162ZM17.7583 9.40828L13.5916 5.24162C13.5139 5.16392 13.4217 5.10228 13.3202 5.06023C13.2186 5.01818 13.1098 4.99654 12.9999 4.99654C12.778 4.99654 12.5652 5.0847 12.4083 5.24162C12.3306 5.31931 12.2689 5.41156 12.2269 5.51307C12.1848 5.61459 12.1632 5.7234 12.1632 5.83328C12.1632 6.0552 12.2514 6.26803 12.4083 6.42495L15.9916 9.99995L12.4083 13.575C12.3302 13.6524 12.2682 13.7446 12.2259 13.8461C12.1836 13.9477 12.1618 14.0566 12.1618 14.1666C12.1618 14.2766 12.1836 14.3855 12.2259 14.4871C12.2682 14.5886 12.3302 14.6808 12.4083 14.7583C12.4858 14.8364 12.5779 14.8984 12.6795 14.9407C12.781 14.983 12.8899 15.0048 12.9999 15.0048C13.11 15.0048 13.2189 14.983 13.3204 14.9407C13.422 14.8984 13.5141 14.8364 13.5916 14.7583L17.7583 10.5916C17.8364 10.5141 17.8984 10.422 17.9407 10.3204C17.983 10.2189 18.0048 10.11 18.0048 9.99995C18.0048 9.88994 17.983 9.78102 17.9407 9.67947C17.8984 9.57792 17.8364 9.48575 17.7583 9.40828Z" fill="#374151"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

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="M15 14H9C8.73478 14 8.48043 14.1054 8.29289 14.2929C8.10536 14.4804 8 14.7348 8 15C8 15.2652 8.10536 15.5196 8.29289 15.7071C8.48043 15.8946 8.73478 16 9 16H15C15.2652 16 15.5196 15.8946 15.7071 15.7071C15.8946 15.5196 16 15.2652 16 15C16 14.7348 15.8946 14.4804 15.7071 14.2929C15.5196 14.1054 15.2652 14 15 14ZM15 10H11C10.7348 10 10.4804 10.1054 10.2929 10.2929C10.1054 10.4804 10 10.7348 10 11C10 11.2652 10.1054 11.5196 10.2929 11.7071C10.4804 11.8946 10.7348 12 11 12H15C15.2652 12 15.5196 11.8946 15.7071 11.7071C15.8946 11.5196 16 11.2652 16 11C16 10.7348 15.8946 10.4804 15.7071 10.2929C15.5196 10.1054 15.2652 10 15 10ZM17 4H15.82C15.6137 3.41645 15.2319 2.911 14.7271 2.55294C14.2222 2.19488 13.6189 2.00174 13 2H11C10.3811 2.00174 9.7778 2.19488 9.27293 2.55294C8.76807 2.911 8.38631 3.41645 8.18 4H7C6.20435 4 5.44129 4.31607 4.87868 4.87868C4.31607 5.44129 4 6.20435 4 7V19C4 19.7956 4.31607 20.5587 4.87868 21.1213C5.44129 21.6839 6.20435 22 7 22H17C17.7956 22 18.5587 21.6839 19.1213 21.1213C19.6839 20.5587 20 19.7956 20 19V7C20 6.20435 19.6839 5.44129 19.1213 4.87868C18.5587 4.31607 17.7956 4 17 4ZM10 5C10 4.73478 10.1054 4.48043 10.2929 4.29289C10.4804 4.10536 10.7348 4 11 4H13C13.2652 4 13.5196 4.10536 13.7071 4.29289C13.8946 4.48043 14 4.73478 14 5V6H10V5ZM18 19C18 19.2652 17.8946 19.5196 17.7071 19.7071C17.5196 19.8946 17.2652 20 17 20H7C6.73478 20 6.48043 19.8946 6.29289 19.7071C6.10536 19.5196 6 19.2652 6 19V7C6 6.73478 6.10536 6.48043 6.29289 6.29289C6.48043 6.10536 6.73478 6 7 6H8V7C8 7.26522 8.10536 7.51957 8.29289 7.70711C8.48043 7.89464 8.73478 8 9 8H15C15.2652 8 15.5196 7.89464 15.7071 7.70711C15.8946 7.51957 16 7.26522 16 7V6H17C17.2652 6 17.5196 6.10536 17.7071 6.29289C17.8946 6.48043 18 6.73478 18 7V19Z" fill="#9CA3AF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,3 +0,0 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4998 5.83342C10.2788 5.83342 10.0669 5.92121 9.91059 6.07749C9.75431 6.23377 9.66651 6.44573 9.66651 6.66675V10.0001C9.66651 10.2211 9.75431 10.4331 9.91059 10.5893C10.0669 10.7456 10.2788 10.8334 10.4998 10.8334C10.7209 10.8334 10.9328 10.7456 11.0891 10.5893C11.2454 10.4331 11.3332 10.2211 11.3332 10.0001V6.66675C11.3332 6.44573 11.2454 6.23377 11.0891 6.07749C10.9328 5.92121 10.7209 5.83342 10.4998 5.83342ZM11.2665 13.0167C11.2483 12.9636 11.223 12.9132 11.1915 12.8667L11.0915 12.7417C10.9743 12.6261 10.8255 12.5478 10.6639 12.5166C10.5022 12.4855 10.3349 12.5029 10.1832 12.5667C10.0822 12.6089 9.98918 12.6681 9.90817 12.7417C9.83094 12.8196 9.76984 12.912 9.72837 13.0135C9.68689 13.115 9.66587 13.2237 9.66651 13.3334C9.66782 13.4423 9.69047 13.5499 9.73317 13.6501C9.7706 13.7535 9.83031 13.8474 9.90808 13.9252C9.98584 14.0029 10.0798 14.0627 10.1832 14.1001C10.2829 14.1442 10.3908 14.1669 10.4998 14.1669C10.6089 14.1669 10.7168 14.1442 10.8165 14.1001C10.9199 14.0627 11.0138 14.0029 11.0916 13.9252C11.1694 13.8474 11.2291 13.7535 11.2665 13.6501C11.3092 13.5499 11.3319 13.4423 11.3332 13.3334C11.3373 13.2779 11.3373 13.2222 11.3332 13.1667C11.3188 13.1136 11.2963 13.063 11.2665 13.0167ZM10.4998 1.66675C8.85166 1.66675 7.2405 2.15549 5.87009 3.07117C4.49968 3.98685 3.43158 5.28834 2.80084 6.81105C2.17011 8.33377 2.00509 10.0093 2.32663 11.6258C2.64817 13.2423 3.44185 14.7272 4.60728 15.8926C5.77272 17.0581 7.25758 17.8517 8.87409 18.1733C10.4906 18.4948 12.1662 18.3298 13.6889 17.6991C15.2116 17.0683 16.5131 16.0002 17.4288 14.6298C18.3444 13.2594 18.8332 11.6483 18.8332 10.0001C18.8332 8.90573 18.6176 7.8221 18.1988 6.81105C17.78 5.80001 17.1662 4.88135 16.3924 4.10752C15.6186 3.3337 14.6999 2.71987 13.6889 2.30109C12.6778 1.8823 11.5942 1.66675 10.4998 1.66675ZM10.4998 16.6667C9.1813 16.6667 7.89237 16.2758 6.79604 15.5432C5.69971 14.8107 4.84523 13.7695 4.34064 12.5513C3.83606 11.3331 3.70404 9.99269 3.96127 8.69948C4.21851 7.40627 4.85345 6.21839 5.7858 5.28604C6.71815 4.35369 7.90603 3.71875 9.19924 3.46151C10.4924 3.20428 11.8329 3.3363 13.0511 3.84088C14.2692 4.34547 15.3104 5.19995 16.043 6.29628C16.7755 7.39261 17.1665 8.68154 17.1665 10.0001C17.1665 11.7682 16.4641 13.4639 15.2139 14.7141C13.9636 15.9644 12.268 16.6667 10.4998 16.6667Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

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="M20 6H16V5C16 4.20435 15.6839 3.44129 15.1213 2.87868C14.5587 2.31607 13.7956 2 13 2H11C10.2044 2 9.44129 2.31607 8.87868 2.87868C8.31607 3.44129 8 4.20435 8 5V6H4C3.73478 6 3.48043 6.10536 3.29289 6.29289C3.10536 6.48043 3 6.73478 3 7C3 7.26522 3.10536 7.51957 3.29289 7.70711C3.48043 7.89464 3.73478 8 4 8H5V19C5 19.7956 5.31607 20.5587 5.87868 21.1213C6.44129 21.6839 7.20435 22 8 22H16C16.7956 22 17.5587 21.6839 18.1213 21.1213C18.6839 20.5587 19 19.7956 19 19V8H20C20.2652 8 20.5196 7.89464 20.7071 7.70711C20.8946 7.51957 21 7.26522 21 7C21 6.73478 20.8946 6.48043 20.7071 6.29289C20.5196 6.10536 20.2652 6 20 6ZM10 5C10 4.73478 10.1054 4.48043 10.2929 4.29289C10.4804 4.10536 10.7348 4 11 4H13C13.2652 4 13.5196 4.10536 13.7071 4.29289C13.8946 4.48043 14 4.73478 14 5V6H10V5ZM17 19C17 19.2652 16.8946 19.5196 16.7071 19.7071C16.5196 19.8946 16.2652 20 16 20H8C7.73478 20 7.48043 19.8946 7.29289 19.7071C7.10536 19.5196 7 19.2652 7 19V8H17V19Z" fill="#9CA3AF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,3 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.25 5.79997C21.4983 6.12606 20.7034 6.34163 19.89 6.43997C20.7482 5.92729 21.3913 5.12075 21.7 4.16997C20.8936 4.65003 20.0108 4.98826 19.09 5.16997C18.4745 4.50254 17.655 4.05826 16.7598 3.90682C15.8647 3.75537 14.9445 3.90532 14.1438 4.33315C13.343 4.76099 12.7069 5.4425 12.3352 6.2708C11.9635 7.09911 11.8773 8.02736 12.09 8.90997C10.4594 8.82749 8.86444 8.40292 7.40865 7.66383C5.95287 6.92474 4.66885 5.88766 3.64 4.61997C3.27914 5.25013 3.08952 5.96379 3.09 6.68997C3.08872 7.36435 3.25422 8.02858 3.57176 8.62353C3.88929 9.21848 4.34902 9.72568 4.91 10.1C4.25798 10.0822 3.61989 9.90726 3.05 9.58997V9.63997C3.05489 10.5849 3.38599 11.4991 3.98731 12.2279C4.58864 12.9568 5.42326 13.4556 6.35 13.64C5.99326 13.7485 5.62287 13.8058 5.25 13.81C4.99189 13.807 4.73442 13.7835 4.48 13.74C4.74391 14.5528 5.25462 15.2631 5.94107 15.7721C6.62753 16.2811 7.45558 16.5635 8.31 16.58C6.8672 17.7152 5.08588 18.3348 3.25 18.34C2.91574 18.3411 2.58174 18.321 2.25 18.28C4.12443 19.4902 6.30881 20.1327 8.54 20.13C10.0797 20.146 11.6071 19.855 13.0331 19.274C14.4591 18.6931 15.755 17.8338 16.8452 16.7465C17.9354 15.6591 18.798 14.3654 19.3826 12.9409C19.9672 11.5164 20.262 9.98969 20.25 8.44997C20.25 8.27996 20.25 8.09997 20.25 7.91997C21.0347 7.33478 21.7115 6.61739 22.25 5.79997Z" fill="#6B7280"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,9 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="71px" height="71px" viewBox="-6 -6 36.00 36.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.00024000000000000003">
<g id="SVGRepo_bgCarrier" stroke-width="0">
<rect x="-6" y="-6" width="36.00" height="36.00" rx="18" fill="#ffffff" strokewidth="0"/>
</g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M12 18C14.7614 18 17 15.7614 17 13C17 10.2386 14.7614 8 12 8C9.23858 8 7 10.2386 7 13C7 15.7614 9.23858 18 12 18ZM12 16.0071C10.3392 16.0071 8.9929 14.6608 8.9929 13C8.9929 11.3392 10.3392 9.9929 12 9.9929C13.6608 9.9929 15.0071 11.3392 15.0071 13C15.0071 14.6608 13.6608 16.0071 12 16.0071Z" fill="#000000"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.56155 2C8.18495 2 6.985 2.93689 6.65112 4.27239L6.21922 6H4C2.34315 6 1 7.34315 1 9V19C1 20.6569 2.34315 22 4 22H20C21.6569 22 23 20.6569 23 19V9C23 7.34315 21.6569 6 20 6H17.7808L17.3489 4.27239C17.015 2.93689 15.8151 2 14.4384 2H9.56155ZM8.59141 4.75746C8.7027 4.3123 9.10268 4 9.56155 4H14.4384C14.8973 4 15.2973 4.3123 15.4086 4.75746L15.8405 6.48507C16.0631 7.37541 16.863 8 17.7808 8H20C20.5523 8 21 8.44772 21 9V19C21 19.5523 20.5523 20 20 20H4C3.44772 20 3 19.5523 3 19V9C3 8.44772 3.44772 8 4 8H6.21922C7.13696 8 7.93692 7.37541 8.15951 6.48507L8.59141 4.75746Z" fill="#000000"/> </g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB