ui: interface revamp (#429)

* feat: adding create bot functionality

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

* update the temperature progress bar

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

* WIP baselayout

* Mapping plugins with available preferences

* Added loader component

* WIP working another screen

* Cleanup types and avoid import one by one

* Prepare bottom bar

* Add css variables colors to enable user select the accent

* Enable change accent color

* Seperate css variable

* Fix conflict

* Add blank state of my model empty

* Restyle explore models page

* Enable user config left sidebar

* Restyle my models page

* WIP styling chat page

* Restyling chat message

* Fix conflict

* Adde form preferences setting plugins

* Fixed form bot info

* Sidebar bot chat

* Showing rightbar for both setting when user created bot

* Fix style bot info

* Using overflow auto intead of scroll

* Remove script built UI from root package

* Fix missig import

* Resolve error linter

* fix e2e tests

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:
Faisal Amir 2023-10-24 10:59:12 +07:00 committed by GitHub
parent e332a7c0bd
commit 539b467141
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
209 changed files with 3286 additions and 1913 deletions

View File

@ -1,4 +1,11 @@
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron"; import {
app,
BrowserWindow,
ipcMain,
dialog,
shell,
nativeTheme,
} from "electron";
import { readdirSync, writeFileSync } from "fs"; import { readdirSync, writeFileSync } from "fs";
import { resolve, join, extname } from "path"; import { resolve, join, extname } from "path";
import { rmdir, unlink, createWriteStream } from "fs"; import { rmdir, unlink, createWriteStream } from "fs";
@ -36,12 +43,30 @@ app.on("window-all-closed", () => {
app.quit(); app.quit();
}); });
ipcMain.handle("setNativeThemeLight", () => {
nativeTheme.themeSource = "light";
});
ipcMain.handle("setNativeThemeDark", () => {
nativeTheme.themeSource = "dark";
});
ipcMain.handle("setNativeThemeSystem", () => {
nativeTheme.themeSource = "system";
});
function createMainWindow() { function createMainWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
frame: false,
show: false, show: false,
backgroundColor: "white", trafficLightPosition: {
x: 16,
y: 10,
},
titleBarStyle: "hidden",
vibrancy: "sidebar",
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
preload: join(__dirname, "preload.js"), preload: join(__dirname, "preload.js"),
@ -118,11 +143,13 @@ function handleIPCs() {
ipcMain.handle( ipcMain.handle(
"invokePluginFunc", "invokePluginFunc",
async (_event, modulePath, method, ...args) => { async (_event, modulePath, method, ...args) => {
const module = require(/* webpackIgnore: true */ join( const module = require(
app.getPath("userData"), /* webpackIgnore: true */ join(
"plugins", app.getPath("userData"),
modulePath "plugins",
)); modulePath
)
);
requiredModules[modulePath] = module; requiredModules[modulePath] = module;
if (typeof module[method] === "function") { if (typeof module[method] === "function") {

View File

@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld("electronAPI", {
invokePluginFunc: (plugin: any, method: any, ...args: any[]) => invokePluginFunc: (plugin: any, method: any, ...args: any[]) =>
ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args), ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args),
setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"),
setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"),
setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"),
basePlugins: () => ipcRenderer.invoke("basePlugins"), basePlugins: () => ipcRenderer.invoke("basePlugins"),
pluginPath: () => ipcRenderer.invoke("pluginPath"), pluginPath: () => ipcRenderer.invoke("pluginPath"),
@ -23,19 +29,27 @@ contextBridge.exposeInMainWorld("electronAPI", {
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
installRemotePlugin: (pluginName: string) => ipcRenderer.invoke("installRemotePlugin", pluginName), installRemotePlugin: (pluginName: string) =>
ipcRenderer.invoke("installRemotePlugin", pluginName),
downloadFile: (url: string, path: string) => ipcRenderer.invoke("downloadFile", url, path), downloadFile: (url: string, path: string) =>
ipcRenderer.invoke("downloadFile", url, path),
onFileDownloadUpdate: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), onFileDownloadUpdate: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
onFileDownloadError: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), onFileDownloadError: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
onFileDownloadSuccess: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), onFileDownloadSuccess: (callback: any) =>
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
onAppUpdateDownloadUpdate: (callback: any) => ipcRenderer.on("APP_UPDATE_PROGRESS", callback), onAppUpdateDownloadUpdate: (callback: any) =>
ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
onAppUpdateDownloadError: (callback: any) => ipcRenderer.on("APP_UPDATE_ERROR", callback), onAppUpdateDownloadError: (callback: any) =>
ipcRenderer.on("APP_UPDATE_ERROR", callback),
onAppUpdateDownloadSuccess: (callback: any) => ipcRenderer.on("APP_UPDATE_COMPLETE", callback), onAppUpdateDownloadSuccess: (callback: any) =>
ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
}); });

View File

@ -48,10 +48,8 @@ test("renders the home page", async () => {
// Welcome text is available // Welcome text is available
const welcomeText = await page const welcomeText = await page
.locator(".text-5xl", { .getByTestId("testid-welcome-title")
hasText: "Welcome,lets download your first model",
})
.first() .first()
.isDisabled(); .isVisible();
expect(welcomeText).toBe(false); expect(welcomeText).toBe(false);
}); });

View File

@ -40,7 +40,7 @@ test("shows my models", async () => {
.getByRole("heading") .getByRole("heading")
.filter({ hasText: "My Models" }) .filter({ hasText: "My Models" })
.first() .first()
.isDisabled(); .isVisible();
expect(header).toBe(false); expect(header).toBe(false);
// More test cases here... // More test cases here...
}); });

View File

@ -35,37 +35,13 @@ test.afterAll(async () => {
}); });
test("renders left navigation panel", async () => { test("renders left navigation panel", async () => {
// Chat History section is available // Chat section should be there
const chatSection = await page const chatSection = await page.getByTestId("Chat").first().isVisible();
.getByRole("heading")
.filter({ hasText: "CHAT HISTORY" })
.first()
.isDisabled();
expect(chatSection).toBe(false); expect(chatSection).toBe(false);
// Home actions // Home actions
const createBotBtn = await page const botBtn = await page.getByTestId("Bot").first().isEnabled();
.getByRole("button", { name: "Create bot" }) const myModelsBtn = await page.getByTestId("My Models").first().isEnabled();
.first() const settingsBtn = await page.getByTestId("Settings").first().isEnabled();
.isEnabled(); expect([botBtn, myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0);
const exploreBtn = await page
.getByRole("button", { name: "Explore Models" })
.first()
.isEnabled();
const myModelsBtn = await page
.getByTestId("My Models")
.first()
.isEnabled();
const settingsBtn = await page
.getByTestId("Settings")
.first()
.isEnabled();
expect(
[
createBotBtn,
exploreBtn,
myModelsBtn,
settingsBtn,
].filter((e) => !e).length
).toBe(0);
}); });

View File

@ -36,7 +36,5 @@ test.afterAll(async () => {
test("shows settings", async () => { test("shows settings", async () => {
await page.getByTestId("Settings").first().click(); await page.getByTestId("Settings").first().click();
await page.getByTestId("testid-setting-description").isVisible();
const pluginList = await page.getByTestId("plugin-item").count();
expect(pluginList).toBe(4);
}); });

View File

@ -43,9 +43,5 @@
"@janhq/core": "^0.1.6", "@janhq/core": "^0.1.6",
"pouchdb-find": "^8.0.1", "pouchdb-find": "^8.0.1",
"pouchdb-node": "^8.0.1" "pouchdb-node": "^8.0.1"
}, }
"bundleDependencies": [
"pouchdb-node",
"pouchdb-find"
]
} }

View File

@ -9,13 +9,17 @@ import {
} from "@janhq/core"; } from "@janhq/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
const initModel = async (product) => invokePluginFunc(MODULE_PATH, "initModel", product); const initModel = async (product) =>
invokePluginFunc(MODULE_PATH, "initModel", product);
const stopModel = () => { const stopModel = () => {
invokePluginFunc(MODULE_PATH, "killSubprocess"); invokePluginFunc(MODULE_PATH, "killSubprocess");
}; };
function requestInference(recentMessages: any[], bot?: any): Observable<string> { function requestInference(
recentMessages: any[],
bot?: any
): Observable<string> {
return new Observable((subscriber) => { return new Observable((subscriber) => {
const requestBody = JSON.stringify({ const requestBody = JSON.stringify({
messages: recentMessages, messages: recentMessages,
@ -69,10 +73,15 @@ function requestInference(recentMessages: any[], bot?: any): Observable<string>
async function retrieveLastTenMessages(conversationId: string, bot?: any) { async function retrieveLastTenMessages(conversationId: string, bot?: any) {
// TODO: Common collections should be able to access via core functions instead of store // TODO: Common collections should be able to access via core functions instead of store
const messageHistory = (await store.findMany("messages", { conversationId }, [{ createdAt: "asc" }])) ?? []; const messageHistory =
(await store.findMany("messages", { conversationId }, [
{ createdAt: "asc" },
])) ?? [];
let recentMessages = messageHistory let recentMessages = messageHistory
.filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant")) .filter(
(e) => e.message !== "" && (e.user === "user" || e.user === "assistant")
)
.slice(-9) .slice(-9)
.map((message) => ({ .map((message) => ({
content: message.message.trim(), content: message.message.trim(),
@ -81,10 +90,13 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) {
if (bot && bot.systemPrompt) { if (bot && bot.systemPrompt) {
// append bot's system prompt // append bot's system prompt
recentMessages = [{ recentMessages = [
content: `[INST] ${bot.systemPrompt}`, {
role: 'system' content: `[INST] ${bot.systemPrompt}`,
},...recentMessages]; role: "system",
},
...recentMessages,
];
} }
console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`); console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`);
@ -93,13 +105,19 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) {
} }
async function handleMessageRequest(data: NewMessageRequest) { async function handleMessageRequest(data: NewMessageRequest) {
const conversation = await store.findOne("conversations", data.conversationId); const conversation = await store.findOne(
"conversations",
data.conversationId
);
let bot = undefined; let bot = undefined;
if (conversation.botId != null) { if (conversation.botId != null) {
bot = await store.findOne("bots", conversation.botId); bot = await store.findOne("bots", conversation.botId);
} }
const recentMessages = await retrieveLastTenMessages(data.conversationId, bot); const recentMessages = await retrieveLastTenMessages(
data.conversationId,
bot
);
const message = { const message = {
...data, ...data,
message: "", message: "",
@ -124,7 +142,8 @@ async function handleMessageRequest(data: NewMessageRequest) {
await store.updateOne("messages", message._id, message); await store.updateOne("messages", message._id, message);
}, },
error: async (err) => { error: async (err) => {
message.message = message.message.trim() + "\n" + "Error occurred: " + err; message.message =
message.message.trim() + "\n" + "Error occurred: " + err;
// TODO: Common collections should be able to access via core functions instead of store // TODO: Common collections should be able to access via core functions instead of store
await store.updateOne("messages", message._id, message); await store.updateOne("messages", message._id, message);
}, },
@ -140,7 +159,10 @@ async function inferenceRequest(data: NewMessageRequest): Promise<any> {
}; };
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const recentMessages = await retrieveLastTenMessages(data.conversationId); const recentMessages = await retrieveLastTenMessages(data.conversationId);
requestInference([...recentMessages, { role: "user", content: data.message }]).subscribe({ requestInference([
...recentMessages,
{ role: "user", content: data.message },
]).subscribe({
next: (content) => { next: (content) => {
message.message = content; message.message = content;
}, },
@ -166,5 +188,9 @@ export function init({ register }) {
register(PluginService.OnStart, PLUGIN_NAME, onStart); register(PluginService.OnStart, PLUGIN_NAME, onStart);
register(InferenceService.InitModel, initModel.name, initModel); register(InferenceService.InitModel, initModel.name, initModel);
register(InferenceService.StopModel, stopModel.name, stopModel); register(InferenceService.StopModel, stopModel.name, stopModel);
register(InferenceService.InferenceRequest, inferenceRequest.name, inferenceRequest); register(
InferenceService.InferenceRequest,
inferenceRequest.name,
inferenceRequest
);
} }

View File

@ -1,7 +1,7 @@
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import React from 'react' import React from 'react'
import ModelTable from '../ModelTable' import ModelTable from '../ModelTable'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
const ActiveModelTable: React.FC = () => { const ActiveModelTable: React.FC = () => {
const activeModel = useAtomValue(activeAssistantModelAtom) const activeModel = useAtomValue(activeAssistantModelAtom)

View File

@ -2,8 +2,7 @@ import DownloadModelContent from '../DownloadModelContent'
import ModelDownloadButton from '../ModelDownloadButton' import ModelDownloadButton from '../ModelDownloadButton'
import ModelDownloadingButton from '../ModelDownloadingButton' import ModelDownloadingButton from '../ModelDownloadingButton'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { AssistantModel } from '@/_models/AssistantModel'
type Props = { type Props = {
model: AssistantModel model: AssistantModel

View File

@ -7,7 +7,7 @@ type Props = {
const Avatar: React.FC<Props> = ({ allowEdit = false }) => ( const Avatar: React.FC<Props> = ({ allowEdit = false }) => (
<div className="mx-auto flex flex-col gap-5"> <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"> <span className="mx-auto inline-block h-10 w-10 overflow-hidden rounded-full bg-gray-100">
<svg <svg
className="mx-auto h-full w-full text-gray-300" className="mx-auto h-full w-full text-gray-300"
fill="currentColor" fill="currentColor"

View File

@ -3,7 +3,7 @@
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { InformationCircleIcon } from '@heroicons/react/24/outline' import { InformationCircleIcon } from '@heroicons/react/24/outline'
import SendButton from '../SendButton' import SendButton from '../SendButton'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
const BasicPromptAccessories: React.FC = () => { const BasicPromptAccessories: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)
@ -11,7 +11,7 @@ const BasicPromptAccessories: React.FC = () => {
const shouldShowAdvancedPrompt = false const shouldShowAdvancedPrompt = false
return ( return (
<div className="absolute inset-x-0 bottom-0 flex justify-between py-2 pl-3 pr-2"> <div className="absolute inset-x-0 bottom-0 flex justify-between p-3">
{/* Add future accessories here, e.g upload a file */} {/* Add future accessories here, e.g upload a file */}
<div className="flex items-center space-x-5"> <div className="flex items-center space-x-5">
<div className="flex items-center"> <div className="flex items-center">

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { ChevronLeftIcon } from '@heroicons/react/24/outline' import { ChevronLeftIcon } from '@heroicons/react/24/outline'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
const BasicPromptButton: React.FC = () => { const BasicPromptButton: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom)

View File

@ -1,11 +1,11 @@
'use client' 'use client'
import { currentPromptAtom } from '@/_helpers/JotaiWrapper' import { currentPromptAtom } from '@helpers/JotaiWrapper'
import { getActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom' import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
import { selectedModelAtom } from '@/_helpers/atoms/Model.atom' import { selectedModelAtom } from '@helpers/atoms/Model.atom'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import useInitModel from '@/_hooks/useInitModel' import useInitModel from '@hooks/useInitModel'
import useSendChatMessage from '@/_hooks/useSendChatMessage' import useSendChatMessage from '@hooks/useSendChatMessage'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { ChangeEvent, useEffect, useRef } from 'react' import { ChangeEvent, useEffect, useRef } from 'react'
@ -68,7 +68,7 @@ const BasicPromptInput: React.FC = () => {
} }
return ( return (
<div className="overflow-hidden rounded-lg shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600"> <div className=" border-border rounded-lg border shadow-sm">
<textarea <textarea
ref={textareaRef} ref={textareaRef}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
@ -76,7 +76,7 @@ const BasicPromptInput: React.FC = () => {
onChange={handleMessageChange} onChange={handleMessageChange}
name="comment" name="comment"
id="comment" id="comment"
className="block w-full resize-none border-0 bg-transparent py-1.5 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" className="text-background-reverse block w-full resize-none border-0 bg-transparent py-1.5 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="Message ..." placeholder="Message ..."
rows={1} rows={1}
style={{ overflow: 'auto' }} style={{ overflow: 'auto' }}

View File

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

View File

@ -1,14 +1,14 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react' import React from 'react'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import useDeleteBot from '@/_hooks/useDeleteBot' import useDeleteBot from '@hooks/useDeleteBot'
import { import {
setMainViewStateAtom, setMainViewStateAtom,
MainViewState, MainViewState,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
const BotInfoContainer: React.FC = () => { const BotInfoContainer: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom) const activeBot = useAtomValue(activeBotAtom)
@ -44,9 +44,7 @@ const BotInfoContainer: React.FC = () => {
<div className="flex h-full w-full pt-4"> <div className="flex h-full w-full pt-4">
<div className="mx-auto flex w-[672px] min-w-max flex-col gap-4"> <div className="mx-auto flex w-[672px] min-w-max flex-col gap-4">
<Avatar /> <Avatar />
<h1 className="text-center text-2xl font-bold"> <h1 className="text-center text-2xl font-bold">{activeBot?.name}</h1>
{activeBot?.name}
</h1>
<div className="flex gap-4"> <div className="flex gap-4">
<PrimaryButton <PrimaryButton
fullWidth fullWidth
@ -55,7 +53,7 @@ const BotInfoContainer: React.FC = () => {
/> />
<PrimaryButton <PrimaryButton
fullWidth fullWidth
className='bg-red-500 hover:bg-red-400' className="bg-red-500 hover:bg-red-400"
title="Delete bot" title="Delete bot"
onClick={onDeleteBotClick} onClick={onDeleteBotClick}
/> />

View File

@ -1,14 +1,14 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { Bot } from '@/_models/Bot'
import { useAtom, useSetAtom } from 'jotai' import { useAtom, useSetAtom } from 'jotai'
import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
const BotListContainer: React.FC = () => { const BotListContainer: React.FC = () => {
const [open, setOpen] = useAtom(showingBotListModalAtom) const [open, setOpen] = useAtom(showingBotListModalAtom)
@ -16,6 +16,7 @@ const BotListContainer: React.FC = () => {
const [activeBot, setActiveBot] = useAtom(activeBotAtom) const [activeBot, setActiveBot] = useAtom(activeBotAtom)
const [bots, setBots] = useState<Bot[]>([]) const [bots, setBots] = useState<Bot[]>([])
const { getAllBots } = useGetBots() const { getAllBots } = useGetBots()
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
useEffect(() => { useEffect(() => {
if (open) { if (open) {
@ -23,31 +24,34 @@ const BotListContainer: React.FC = () => {
setBots(res) setBots(res)
}) })
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]) }, [open])
const onBotSelected = (bot: Bot) => { const onBotSelected = (bot: Bot) => {
if (bot._id !== activeBot?._id) { if (bot._id !== activeBot?._id) {
setMainView(MainViewState.BotInfo) setMainView(MainViewState.BotInfo)
setActiveBot(bot) setActiveBot(bot)
setRightSideBarVisibility(true)
} }
setOpen(false) setOpen(false)
} }
return ( return (
<div className="overflow-hidden bg-white shadow sm:rounded-md"> <div className="bg-background/50 border-border overflow-hidden border sm:rounded-md">
<ul role="list" className="divide-y divide-gray-200"> <ul role="list" className="divide-y divide-gray-200">
{bots.map((bot) => ( {bots.map((bot, i) => (
<li <li
role="button" role="button"
key={bot._id} key={i}
className="flex gap-4 p-4 hover:bg-hover-light sm:px-6" className="flex items-center gap-4 p-4 hover:bg-hover-light sm:px-6"
onClick={() => onBotSelected(bot)} onClick={() => onBotSelected(bot)}
> >
<Avatar /> <Avatar />
<div className="flex flex-1 flex-col"> <div className="flex flex-1 flex-col">
<p className="line-clamp-1">{bot.name}</p> <p className="line-clamp-1">{bot.name}</p>
<p className="line-clamp-1 text-ellipsis">{bot._id}</p> <p className="text-muted-foreground mt-1 line-clamp-1 text-ellipsis">
{bot._id}
</p>
</div> </div>
</li> </li>
))} ))}

View File

@ -1,4 +1,4 @@
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
@ -19,10 +19,10 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 w-screen overflow-y-auto"> <div className="fixed inset-0 z-50 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"> <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@ -33,8 +33,8 @@ const BotListModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 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"> <Dialog.Panel className="border-border bg-background/90 relative transform overflow-hidden rounded-lg border px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<h1 className="mb-4 text-lg text-black font-bold">Your bots</h1> <h1 className="mb-4 font-bold">Your bots</h1>
<BotListContainer /> <BotListContainer />
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

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

View File

@ -1,28 +1,26 @@
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import ExpandableHeader from '../ExpandableHeader' import ExpandableHeader from '../ExpandableHeader'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'
import useUpdateBot from '@/_hooks/useUpdateBot' import useUpdateBot from '@hooks/useUpdateBot'
import ProgressSetting from '../ProgressSetting' import { formatTwoDigits } from '@utils/converter'
import { set } from 'react-hook-form'
const delayBeforeUpdateInMs = 1000 const delayBeforeUpdateInMs = 1000
const BotSetting: React.FC = () => { const BotSetting: React.FC = () => {
const activeBot = useAtomValue(activeBotAtom) const activeBot = useAtomValue(activeBotAtom)
const [temperature, setTemperature] = useState(0) const [temperature, setTemperature] = useState(
const [maxTokens, setMaxTokens] = useState(0) activeBot?.customTemperature ?? 0
const [frequencyPenalty, setFrequencyPenalty] = useState(0) )
const [presencePenalty, setPresencePenalty] = useState(0)
useEffect(() => { const [maxTokens, setMaxTokens] = useState(activeBot?.maxTokens ?? 0)
if (!activeBot) return const [frequencyPenalty, setFrequencyPenalty] = useState(
setMaxTokens(activeBot.maxTokens ?? 0) activeBot?.frequencyPenalty ?? 0
setTemperature(activeBot.customTemperature ?? 0) )
setFrequencyPenalty(activeBot.frequencyPenalty ?? 0) const [presencePenalty, setPresencePenalty] = useState(
setPresencePenalty(activeBot.presencePenalty ?? 0) activeBot?.presencePenalty ?? 0
}, [activeBot?._id]) )
const { updateBot } = useUpdateBot() const { updateBot } = useUpdateBot()
@ -60,71 +58,109 @@ const BotSetting: React.FC = () => {
return ( return (
<div className="my-3 flex flex-col"> <div className="my-3 flex flex-col">
<ExpandableHeader <ExpandableHeader title="BOT SETTINGS" />
title="BOT SETTINGS"
expanded={true}
onClick={() => {}}
/>
<div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4"> <div className="mx-2 mt-3 flex flex-shrink-0 flex-col gap-4">
{/* System prompt */} {/* System prompt */}
<div> <div>
<label <label htmlFor="comment" className="block">
htmlFor="comment"
className="block text-sm font-medium leading-6 text-gray-900"
>
System prompt System prompt
</label> </label>
<div className="mt-2"> <div className="mt-1">
<textarea <textarea
rows={4} rows={4}
name="comment" name="comment"
id="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" className="bg-background/80 text-background-reverse ring-border placeholder:text-muted-foreground focus:ring-accent/50 block w-full resize-none rounded-md border-0 py-1.5 text-xs leading-relaxed shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset"
defaultValue={activeBot.systemPrompt} defaultValue={activeBot.systemPrompt}
onChange={(e) => debouncedSystemPrompt(e.target.value)} onChange={(e) => debouncedSystemPrompt(e.target.value)}
/> />
</div> </div>
</div> </div>
<ProgressSetting {/* TODO: clean up this code */}
title="Max tokens" {/* Max temp */}
min={0} <p>Max tokens</p>
max={4096} <div className="mt-2 flex items-center gap-2">
step={1} <input
value={maxTokens} className="flex-1"
onValueChanged={(value) => debouncedMaxToken(value)} type="range"
/> defaultValue={activeBot.maxTokens ?? 0}
min={0}
max={4096}
step={1}
onChange={(e) => {
const value = Number(e.target.value)
setMaxTokens(value)
debouncedMaxToken(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(maxTokens)}
</span>
</div>
<ProgressSetting <p>Frequency penalty</p>
min={0} <div className="mt-2 flex items-center gap-2">
max={1} <input
step={0.01} className="flex-1"
title="Temperature" type="range"
value={temperature} defaultValue={activeBot.frequencyPenalty ?? 0}
onValueChanged={(value) => debouncedTemperature(value)} min={0}
/> max={1}
step={0.01}
onChange={(e) => {
const value = Number(e.target.value)
setFrequencyPenalty(value)
debouncedFreqPenalty(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(frequencyPenalty)}
</span>
</div>
<ProgressSetting <p>Presence penalty</p>
title="Frequency penalty" <div className="mt-2 flex items-center gap-2">
value={frequencyPenalty} <input
min={0} className="flex-1"
max={1} type="range"
step={0.01} defaultValue={activeBot.maxTokens ?? 0}
onValueChanged={(value) => debouncedFreqPenalty(value)} min={0}
/> max={1}
step={0.01}
onChange={(e) => {
const value = Number(e.target.value)
setPresencePenalty(value)
debouncedPresencePenalty(value)
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(presencePenalty)}
</span>
</div>
<ProgressSetting {/* Custom temp */}
min={0} <p>Temperature</p>
max={1} <div className="mt-2 flex items-center gap-2">
step={0.01} <input
title="Presence penalty" className="flex-1"
value={presencePenalty} type="range"
onValueChanged={(value) => { id="volume"
setPresencePenalty(value) name="volume"
debouncedPresencePenalty(value) defaultValue={activeBot.customTemperature ?? 0}
}} min="0"
/> max="1"
step="0.01"
onChange={(e) => {
const newTemp = Number(e.target.value)
setTemperature(newTemp)
debouncedTemperature(Number(e.target.value))
}}
/>
<span className="border-accent rounded-md border px-2 py-1">
{formatTwoDigits(temperature)}
</span>
</div>
</div> </div>
</div> </div>
) )

View File

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

View File

@ -2,12 +2,11 @@
import React, { useCallback, useRef, useState, useEffect } from 'react' import React, { useCallback, useRef, useState, useEffect } from 'react'
import ChatItem from '../ChatItem' import ChatItem from '../ChatItem'
import { ChatMessage } from '@/_models/ChatMessage' import useChatMessages from '@hooks/useChatMessages'
import useChatMessages from '@/_hooks/useChatMessages'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { selectAtom } from 'jotai/utils' import { selectAtom } from 'jotai/utils'
import { getActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom' import { getActiveConvoIdAtom } from '@helpers/atoms/Conversation.atom'
import { chatMessages } from '@/_helpers/atoms/ChatMessage.atom' import { chatMessages } from '@helpers/atoms/ChatMessage.atom'
const ChatBody: React.FC = () => { const ChatBody: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? '' const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ''
@ -54,7 +53,7 @@ const ChatBody: React.FC = () => {
}, [messageList, lastPostRef]) }, [messageList, lastPostRef])
return ( return (
<div className="scroll flex flex-1 flex-col-reverse overflow-y-auto py-4"> <div className="[&>*:nth-child(odd)]:bg-background flex h-full flex-1 flex-col-reverse overflow-y-auto">
{content} {content}
</div> </div>
) )

View File

@ -1,7 +1,8 @@
import SimpleControlNetMessage from '../SimpleControlNetMessage' import SimpleControlNetMessage from '../SimpleControlNetMessage'
import SimpleImageMessage from '../SimpleImageMessage' import SimpleImageMessage from '../SimpleImageMessage'
import SimpleTextMessage from '../SimpleTextMessage' import SimpleTextMessage from '../SimpleTextMessage'
import { ChatMessage, MessageType } from '@/_models/ChatMessage' import { ChatMessage, MessageType } from '@models/ChatMessage'
export default function renderChatMessage({ export default function renderChatMessage({
id, id,

View File

@ -1,7 +1,6 @@
/* eslint-disable react/display-name */ /* eslint-disable react/display-name */
import React, { forwardRef } from 'react' import React, { forwardRef } from 'react'
import renderChatMessage from '../ChatBody/renderChatMessage' import renderChatMessage from '../ChatBody/renderChatMessage'
import { ChatMessage } from '@/_models/ChatMessage'
type Props = { type Props = {
message: ChatMessage message: ChatMessage

View File

@ -1,16 +0,0 @@
import React from 'react'
import JanImage from '../JanImage'
import { useSetAtom } from 'jotai'
import { setActiveConvoIdAtom } from '@/_helpers/atoms/Conversation.atom'
const CompactLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
return (
<button onClick={() => setActiveConvoId(undefined)}>
<JanImage imageUrl="icons/app_icon.svg" width={28} height={28} />
</button>
)
}
export default React.memo(CompactLogo)

View File

@ -1,5 +1,5 @@
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmDeleteConversationModalAtom } from '@helpers/atoms/Modal.atom'
import useDeleteConversation from '@/_hooks/useDeleteConversation' import useDeleteConversation from '@hooks/useDeleteConversation'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
@ -31,10 +31,10 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> <div className="fixed inset-0 z-40 h-full bg-gray-950/90 transition-opacity dark:backdrop-blur-sm" />
</Transition.Child> </Transition.Child>
<div className="fixed inset-0 z-10 overflow-y-auto"> <div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> <div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
@ -45,7 +45,7 @@ const ConfirmDeleteConversationModal: React.FC = () => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 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-lg sm:p-6"> <Dialog.Panel className="relative transform overflow-hidden rounded-lg border border-border bg-background/90 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start"> <div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon <ExclamationTriangleIcon
@ -54,14 +54,11 @@ const ConfirmDeleteConversationModal: React.FC = () => {
/> />
</div> </div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left"> <div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title <Dialog.Title as="h3" className="font-semibold leading-6">
as="h3"
className="text-base font-semibold leading-6 text-gray-900"
>
Delete Conversation Delete Conversation
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-muted-foreground">
Are you sure you want to delete this conversation? All Are you sure you want to delete this conversation? All
of messages will be permanently removed. This action of messages will be permanently removed. This action
cannot be undone. cannot be undone.
@ -72,14 +69,14 @@ const ConfirmDeleteConversationModal: React.FC = () => {
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse"> <div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button <button
type="button" type="button"
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto" className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-xs font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={() => onConfirmDelete()} onClick={() => onConfirmDelete()}
> >
Delete Delete
</button> </button>
<button <button
type="button" type="button"
className="mt-3 inline-flex w-full justify-center 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 sm:mt-0 sm:w-auto" className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
onClick={() => setShow(false)} onClick={() => setShow(false)}
ref={cancelButtonRef} ref={cancelButtonRef}
> >

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline' import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { showConfirmDeleteModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmDeleteModalAtom } from '@helpers/atoms/Modal.atom'
const ConfirmDeleteModelModal: React.FC = () => { const ConfirmDeleteModelModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmDeleteModalAtom) const [show, setShow] = useAtom(showConfirmDeleteModalAtom)

View File

@ -2,8 +2,8 @@ import React, { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline' import { QuestionMarkCircleIcon } from '@heroicons/react/24/outline'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import useSignOut from '@/_hooks/useSignOut' import useSignOut from '@hooks/useSignOut'
import { showConfirmSignOutModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
const ConfirmSignOutModal: React.FC = () => { const ConfirmSignOutModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmSignOutModalAtom) const [show, setShow] = useAtom(showConfirmSignOutModalAtom)

View File

@ -1,8 +1,7 @@
import React from 'react' import React from 'react'
import Image from 'next/image' import Image from 'next/image'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import { AssistantModel } from '@/_models/AssistantModel' import { PlayIcon } from '@heroicons/react/24/outline'
import { PlayIcon } from "@heroicons/react/24/outline"
type Props = { type Props = {
model: AssistantModel model: AssistantModel

View File

@ -1,4 +1,3 @@
import { AssistantModel } from '@/_models/AssistantModel'
import ConversationalCard from '../ConversationalCard' import ConversationalCard from '../ConversationalCard'
import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline' import { ChatBubbleBottomCenterTextIcon } from '@heroicons/react/24/outline'

View File

@ -5,18 +5,21 @@ import DropdownBox from '../DropdownBox'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import ToggleSwitch from '../ToggleSwitch' import ToggleSwitch from '../ToggleSwitch'
import CreateBotPromptInput from '../CreateBotPromptInput' import CreateBotPromptInput from '../CreateBotPromptInput'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { Bot } from '@/_models/Bot'
import { SubmitHandler, useForm } from 'react-hook-form' import { SubmitHandler, useForm } from 'react-hook-form'
import Avatar from '../Avatar' import Avatar from '../Avatar'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import DraggableProgressBar from '../DraggableProgressBar' import DraggableProgressBar from '../DraggableProgressBar'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import {
leftSideBarExpandStateAtom,
rightSideBarExpandStateAtom,
} from '@helpers/atoms/SideBarExpand.atom'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager' import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import { DataService } from '@janhq/core' import { DataService } from '@janhq/core'
@ -24,16 +27,18 @@ const CreateBotContainer: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
const setActiveBot = useSetAtom(activeBotAtom) const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom) const setMainViewState = useSetAtom(setMainViewStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const createBot = async (bot: Bot) => { const createBot = async (bot: Bot) => {
try { try {
await executeSerial(DataService.CreateBot, bot).then(async () => { await executeSerial(DataService.CreateBot, bot)
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
})
} catch (err) { } catch (err) {
alert(err) alert(err)
console.error(err) console.error(err)
} finally {
setMainViewState(MainViewState.BotInfo)
setActiveBot(bot)
setRightSideBarVisibility(true)
} }
} }
@ -73,9 +78,10 @@ const CreateBotContainer: React.FC = () => {
createBot(bot) createBot(bot)
} }
let models = downloadedModels.map((model) => { let models = downloadedModels.map((model: { _id: any }) => {
return model._id return model._id
}) })
models = ['Select a model', ...models] models = ['Select a model', ...models]
return ( return (
@ -84,7 +90,7 @@ const CreateBotContainer: React.FC = () => {
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
> >
<div className="mx-6 mt-3 flex items-center justify-between gap-3"> <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> <span className="text-lg font-bold">Create Bot</span>
<div className="flex gap-3"> <div className="flex gap-3">
<PrimaryButton isSubmit title="Create" /> <PrimaryButton isSubmit title="Create" />
</div> </div>
@ -108,7 +114,7 @@ const CreateBotContainer: React.FC = () => {
control={control} control={control}
/> />
<div className="flex flex-col gap-4 pb-2"> <div className="flex flex-col pb-2">
<DropdownBox <DropdownBox
id="modelId" id="modelId"
title="Model" title="Model"
@ -116,30 +122,23 @@ const CreateBotContainer: React.FC = () => {
control={control} control={control}
required={true} required={true}
/> />
</div>
<CreateBotPromptInput <CreateBotPromptInput id="systemPrompt" control={control} required />
id="systemPrompt"
<div className="flex flex-col gap-0.5">
<label className="block">Bot access</label>
<span className="mb-4 mt-1 text-muted-foreground">
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} 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 <DraggableProgressBar
id="maxTokens" id="maxTokens"
control={control} control={control}

View File

@ -0,0 +1,98 @@
import React, { Fragment, useState } from 'react'
import ToggleSwitch from '../ToggleSwitch'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
import CutomBotTemperature from '../CustomBotTemperature'
import DraggableProgressBar from '../DraggableProgressBar'
type Props = {
control?: any
}
const CreateBotInAdvance: React.FC<Props> = ({ control }) => {
const [showAdvanced, setShowAdvanced] = useState(true)
const handleShowAdvanced = (e: React.MouseEvent<HTMLButtonElement>) => {
setShowAdvanced(!showAdvanced)
}
return (
<div className="flex flex-col gap-2">
<div className="flex items-start">
<button
className="mb-2 flex items-center justify-center font-bold"
onClick={handleShowAdvanced}
>
Advanced
{showAdvanced ? (
<ChevronDownIcon width={16} className="ml-2" />
) : (
<ChevronUpIcon width={16} className="ml-2" />
)}
</button>
</div>
{showAdvanced && (
<>
<div>
<p className="text-bold">Max tokens</p>
<DraggableProgressBar
id="maxTokens"
control={control}
min={0}
max={4096}
step={1}
/>
</div>
<div>
<p className="text-bold">Custom temperature</p>
<DraggableProgressBar
id="customTemperature"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
<div>
<p className="text-bold">Frequency penalty</p>
<DraggableProgressBar
id="frequencyPenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
<div>
<p className="text-bold">Presence penalty</p>
<DraggableProgressBar
id="presencePenalty"
control={control}
min={0}
max={1}
step={0.01}
/>
</div>
</>
)}
{/* {showAdvanced && (
<Fragment>
<ToggleSwitch
id="suggestReplies"
title="Suggest replies"
control={control}
/>
<ToggleSwitch
id="renderMarkdownContent"
title="Render markdown content"
control={control}
/>
<CutomBotTemperature control={control} />
</Fragment>
)} */}
</div>
)
}
export default CreateBotInAdvance

View File

@ -1,30 +1,27 @@
import React, { Fragment, use } from "react"; import React, { Fragment, use } from 'react'
import ToggleSwitch from "../ToggleSwitch"; import ToggleSwitch from '../ToggleSwitch'
import { useController } from "react-hook-form"; import { useController } from 'react-hook-form'
type Props = { type Props = {
id: string; id: string
control?: any; control?: any
required?: boolean; required?: boolean
}; }
const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => { const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
const { field } = useController({ const { field } = useController({
name: id, name: id,
control: control, control: control,
rules: { required: required }, rules: { required: required },
}); })
return ( return (
<Fragment> <Fragment>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label <label htmlFor="comment" className="block font-bold ">
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
Prompt Prompt
</label> </label>
<p className="text-sm text-gray-400 font-normal"> <p className="mt-1 font-normal text-gray-400">
All conversations with this bot will start with your prompt but it 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 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 prompt message to be visible to the user, consider using an intro
@ -32,18 +29,18 @@ const CreateBotPromptInput: React.FC<Props> = ({ id, control, required }) => {
</p> </p>
<ToggleSwitch <ToggleSwitch
id="visibleFromBotProfile" id="visibleFromBotProfile"
title={"Prompt visible from bot profile"} title={'Prompt visible from bot profile'}
control={control} control={control}
/> />
<textarea <textarea
rows={4} 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" className="block w-full resize-none rounded-md border-0 bg-background/80 py-1.5 text-xs leading-relaxed text-background-reverse shadow-sm ring-1 ring-inset ring-border placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-accent/50"
placeholder="Talk to me like a pirate" placeholder="Talk to me like a pirate"
{...field} {...field}
/> />
</div> </div>
</Fragment> </Fragment>
); )
}; }
export default CreateBotPromptInput; export default CreateBotPromptInput

View File

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

View File

@ -1,4 +1,3 @@
import { AssistantModel } from '@/_models/AssistantModel'
import DownloadModelContent from '../DownloadModelContent' import DownloadModelContent from '../DownloadModelContent'
type Props = { type Props = {

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import SearchBar from '../SearchBar' import SearchBar from '../SearchBar'
import ModelTable from '../ModelTable' import ModelTable from '../ModelTable'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
const DownloadedModelTable: React.FC = () => { const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels() const { downloadedModels } = useGetDownloadedModels()
@ -9,11 +9,11 @@ const DownloadedModelTable: React.FC = () => {
if (!downloadedModels || downloadedModels.length === 0) return null if (!downloadedModels || downloadedModels.length === 0) return null
return ( return (
<div className="pl-[63px] pr-[89px]"> <div className="mt-5">
<h3 className="mt-[50px] text-xl leading-[25px]">Downloaded Models</h3> {/* <h3 className="mt-[50px] text-xl leading-[25px]">Downloaded Models</h3> */}
<div className="w-[568px] py-5"> {/* <div className="w-[568px] py-5">
<SearchBar /> <SearchBar />
</div> </div> */}
<ModelTable models={downloadedModels} /> <ModelTable models={downloadedModels} />
</div> </div>
) )

View File

@ -1,8 +1,7 @@
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import ModelDownloadingTable from '../ModelDownloadingTable' import ModelDownloadingTable from '../ModelDownloadingTable'
import { DownloadState } from '@/_models/DownloadState'
const DownloadingModelTable: React.FC = () => { const DownloadingModelTable: React.FC = () => {
const modelDownloadState = useAtomValue(modelDownloadStateAtom) const modelDownloadState = useAtomValue(modelDownloadStateAtom)

View File

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

View File

@ -24,9 +24,9 @@ const DropdownBox: React.FC<Props> = ({
return ( return (
<Fragment> <Fragment>
<label className="block text-base font-bold text-gray-900">{title}</label> <label className="block font-bold">{title}</label>
<select <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" className="bg-background/80 ring-border focus:ring-accent/50 mt-1 block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-xs ring-1 ring-inset focus:ring-2 sm:leading-6"
{...field} {...field}
> >
{data.map((option) => ( {data.map((option) => (

View File

@ -2,22 +2,11 @@ import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
type Props = { type Props = {
title: string title: string
expanded: boolean
onClick: () => void
} }
const ExpandableHeader: React.FC<Props> = ({ title, expanded, onClick }) => ( const ExpandableHeader: React.FC<Props> = ({ title }) => (
<button onClick={onClick} className="flex items-center justify-between px-2"> <button className="flex items-center justify-between">
<h2 className="pl-1 text-xs font-bold leading-[12px] text-gray-400"> <h2 className="text-muted-foreground pl-1 font-bold">{title}</h2>
{title}
</h2>
<div className="mr-2">
{expanded ? (
<ChevronDownIcon width={12} height={12} color="#6B7280" />
) : (
<ChevronUpIcon width={12} height={12} color="#6B7280" />
)}
</div>
</button> </button>
) )

View File

@ -1,5 +1,5 @@
import HeaderTitle from '../HeaderTitle' import HeaderTitle from '../HeaderTitle'
import ExploreModelList from '../ExploreModelList' import ExploreModelList from '../../../screens/ExploreModels/ExploreModelList'
import ExploreModelFilter from '../ExploreModelFilter' import ExploreModelFilter from '../ExploreModelFilter'
const ExploreModelContainer: React.FC = () => ( const ExploreModelContainer: React.FC = () => (

View File

@ -16,7 +16,7 @@ const tags = [
const checkboxs = ['GGUF', 'TensorRT', 'Meow', 'JigglyPuff'] const checkboxs = ['GGUF', 'TensorRT', 'Meow', 'JigglyPuff']
const ExploreModelFilter: React.FC = () => { const ExploreModelFilter: React.FC = () => {
const enabled = false const enabled = true
if (!enabled) return null if (!enabled) return null
return ( return (

View File

@ -3,6 +3,7 @@
'use client' 'use client'
import ExploreModelItemHeader from '../ExploreModelItemHeader' import ExploreModelItemHeader from '../ExploreModelItemHeader'
import { Button } from '@uikit'
import ModelVersionList from '../ModelVersionList' import ModelVersionList from '../ModelVersionList'
import { Fragment, forwardRef, useEffect, useState } from 'react' import { Fragment, forwardRef, useEffect, useState } from 'react'
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
@ -14,10 +15,9 @@ import {
UsecaseTag, UsecaseTag,
VersionTag, VersionTag,
} from '@/_components/SimpleTag/TagType' } from '@/_components/SimpleTag/TagType'
import { displayDate } from '@/_utils/datetime' import { displayDate } from '@utils/datetime'
import { Product } from '@/_models/Product' import useGetMostSuitableModelVersion from '@hooks/useGetMostSuitableModelVersion'
import useGetMostSuitableModelVersion from '@/_hooks/useGetMostSuitableModelVersion' import { toGigabytes } from '@utils/converter'
import { toGigabytes } from '@/_utils/converter'
type Props = { type Props = {
model: Product model: Product
@ -32,6 +32,7 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
useEffect(() => { useEffect(() => {
getMostSuitableModelVersion(availableVersions) getMostSuitableModelVersion(availableVersions)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [availableVersions]) }, [availableVersions])
if (!suitableModel) { if (!suitableModel) {
@ -43,25 +44,30 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
return ( return (
<div <div
ref={ref} ref={ref}
className="mb-4 flex flex-col rounded-md border border-gray-200" className="border-border bg-background/60 mb-4 flex flex-col rounded-md border"
> >
<ExploreModelItemHeader <ExploreModelItemHeader
suitableModel={suitableModel} suitableModel={suitableModel}
exploreModel={model} exploreModel={model}
/> />
<div className="flex flex-col px-[26px] py-[22px]"> <div className="flex flex-col p-4">
<div className="mb-4 flex flex-col gap-1">
<span className="font-semibold">About</span>
<span className="text-muted-foreground leading-relaxed">
{model.longDescription}
</span>
</div>
<div className="flex justify-between"> <div className="flex justify-between">
<div className="flex flex-1 flex-col gap-8"> <div className="flex flex-1 flex-col gap-y-4">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="text-sm font-medium text-gray-500"> <div className="font-semibold">Release Date</div>
Release Date <p className="text-muted-foreground mt-1">
</div>
<div className="text-sm font-normal text-gray-900">
{displayDate(model.releaseDate)} {displayDate(model.releaseDate)}
</div> </p>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">Version</div> <div className="font-semibold">Version</div>
<div className="flex gap-2"> <div className="flex gap-2">
<SimpleTag <SimpleTag
title={model.version} title={model.version}
@ -81,17 +87,14 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-1 flex-col gap-8">
<div className="flex flex-1 flex-col gap-y-4">
<div> <div>
<div className="text-sm font-medium text-gray-500">Author</div> <div className="font-semibold">Author</div>
<div className="text-sm font-normal text-gray-900"> <p className="text-muted-foreground mt-1">{model.author}</p>
{model.author}
</div>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500"> <div className="font-semibold">Compatibility</div>
Compatibility
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<SimpleTag <SimpleTag
title={usecase} title={usecase}
@ -106,44 +109,42 @@ const ExploreModelItem = forwardRef<HTMLDivElement, Props>(({ model }, ref) => {
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="mt-[26px] flex flex-col gap-1"> <div className="flex flex-1 flex-col gap-y-4">
<span className="text-sm font-medium text-gray-500">About</span> <div>
<span className="text-sm font-normal text-gray-500"> <div className="font-medium">Tags</div>
{model.longDescription} <div className="mt-1 flex flex-wrap gap-2">
</span> {model.tags.map((tag) => (
</div> <SimpleTag
<div className="mt-5 flex flex-col gap-2"> key={tag}
<span className="text-sm font-medium text-gray-500">Tags</span> title={tag}
<div className="flex flex-wrap gap-2"> type={MiscellanousTag.MiscellanousDefault}
{model.tags.map((tag) => ( clickable={false}
<SimpleTag />
key={tag} ))}
title={tag} </div>
type={MiscellanousTag.MiscellanousDefault} </div>
clickable={false}
/>
))}
</div> </div>
</div> </div>
{model.availableVersions?.length > 0 && (
<div className="border-border bg-background mt-5 w-full rounded-md border p-2">
<button onClick={() => setShow(!show)} className="w-full">
{!show
? '+ Show Available Versions'
: '- Collapse Available Versions'}
</button>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
recommendedVersion={suitableModel?._id ?? ''}
/>
)}
</div>
)}
</div> </div>
{model.availableVersions?.length > 0 && (
<Fragment>
{show && (
<ModelVersionList
model={model}
versions={model.availableVersions}
recommendedVersion={suitableModel?._id ?? ''}
/>
)}
<button
onClick={() => setShow(!show)}
className="border-t border-gray-200 bg-[#FBFBFB] px-4 py-2 text-left text-sm text-gray-500"
>
{!show ? '+ Show Available Versions' : '- Collapse'}
</button>
</Fragment>
)}
</div> </div>
) )
}) })

View File

@ -1,19 +1,18 @@
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import { formatDownloadPercentage, toGigabytes } from '@/_utils/converter' import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import SecondaryButton from '../SecondaryButton' import SecondaryButton from '../SecondaryButton'
import { Product } from '@/_models/Product'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo } from 'react'
import { ModelVersion } from '@/_models/ModelVersion' import useGetPerformanceTag from '@hooks/useGetPerformanceTag'
import useGetPerformanceTag from '@/_hooks/useGetPerformanceTag' import useDownloadModel from '@hooks/useDownloadModel'
import useDownloadModel from '@/_hooks/useDownloadModel' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom'
import { atom, useAtomValue, useSetAtom } from 'jotai' import { atom, useAtomValue, useSetAtom } from 'jotai'
import { Button } from '@uikit'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
type Props = { type Props = {
suitableModel: ModelVersion suitableModel: ModelVersion
@ -37,35 +36,36 @@ const ExploreModelItemHeader: React.FC<Props> = ({
useEffect(() => { useEffect(() => {
getPerformanceForModel(suitableModel) getPerformanceForModel(suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [suitableModel]) }, [suitableModel])
const onDownloadClick = useCallback(() => { const onDownloadClick = useCallback(() => {
downloadModel(exploreModel, suitableModel) downloadModel(exploreModel, suitableModel)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [exploreModel, suitableModel]) }, [exploreModel, suitableModel])
const isDownloaded = const isDownloaded =
downloadedModels.find((model) => model._id === suitableModel._id) != null downloadedModels.find((model) => model._id === suitableModel._id) != null
let downloadButton = ( let downloadButton = (
<PrimaryButton <Button themes="accent" onClick={() => onDownloadClick()}>
title={ {suitableModel.size
suitableModel.size ? `Download (${toGigabytes(suitableModel.size)})`
? `Download (${toGigabytes(suitableModel.size)})` : 'Download'}
: 'Download' </Button>
}
onClick={() => onDownloadClick()}
/>
) )
if (isDownloaded) { if (isDownloaded) {
downloadButton = ( downloadButton = (
<PrimaryButton <Button
title="View Downloaded Model" size="sm"
themes="accent"
onClick={() => { onClick={() => {
setMainViewState(MainViewState.MyModel) setMainViewState(MainViewState.MyModel)
}} }}
className="bg-green-500 hover:bg-green-400" >
/> View Downloaded Model
</Button>
) )
} }
@ -82,7 +82,7 @@ const ExploreModelItemHeader: React.FC<Props> = ({
} }
return ( return (
<div className="flex items-center justify-between border-b border-gray-200 p-4"> <div className="border-border bg-background/50 flex items-center justify-between rounded-t-md border-b px-4 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span>{exploreModel.name}</span> <span>{exploreModel.name}</span>
{performanceTag && ( {performanceTag && (

View File

@ -1,28 +0,0 @@
import React, { useEffect } from 'react'
import ExploreModelItem from '../ExploreModelItem'
import { getConfiguredModels } from '@/_hooks/useGetDownloadedModels'
import useGetConfiguredModels from '@/_hooks/useGetConfiguredModels'
import { Waveform } from '@uiball/loaders'
const ExploreModelList: React.FC = () => {
const { loading, models } = useGetConfiguredModels()
useEffect(() => {
getConfiguredModels()
}, [])
return (
<div className="scroll flex flex-1 flex-col overflow-y-auto">
{loading && (
<div className="mx-auto">
<Waveform size={24} color="#CBD5E0" />
</div>
)}
{models.map((item) => (
<ExploreModelItem key={item._id} model={item} />
))}
</div>
)
}
export default ExploreModelList

View File

@ -1,23 +0,0 @@
import Image from 'next/image'
import Link from 'next/link'
// DEPRECATED
export default function Footer() {
return (
<div className="container m-auto flex items-center justify-between">
<div className="flex items-center gap-3">
<Image src={'icons/app_icon.svg'} width={32} height={32} alt="" />
<span>Jan</span>
</div>
<div className="my-6 flex gap-4">
<Link href="/privacy" className="cursor-pointer">
Privacy
</Link>
<span>&#8226;</span>
<Link href="/support" className="cursor-pointer">
Support
</Link>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { showingMobilePaneAtom } from '@/_helpers/atoms/Modal.atom' import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
import { Bars3Icon } from '@heroicons/react/24/outline' import { Bars3Icon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import React from 'react' import React from 'react'

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import Image from 'next/image' import Image from 'next/image'
import { Conversation } from '@/_models/Conversation'
import { ModelManagementService } from '@janhq/core' import { ModelManagementService } from '@janhq/core'
import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager' import { executeSerial } from '../../../../electron/core/plugin-manager/execution/extension-manager'
import { import {
@ -9,13 +8,14 @@ import {
setActiveConvoIdAtom, setActiveConvoIdAtom,
updateConversationErrorAtom, updateConversationErrorAtom,
updateConversationWaitingForResponseAtom, updateConversationWaitingForResponseAtom,
} from '@/_helpers/atoms/Conversation.atom' } from '@helpers/atoms/Conversation.atom'
import { import {
setMainViewStateAtom, setMainViewStateAtom,
MainViewState, MainViewState,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import useInitModel from '@/_hooks/useInitModel' import useInitModel from '@hooks/useInitModel'
import { displayDate } from '@/_utils/datetime' import { displayDate } from '@utils/datetime'
import { twMerge } from 'tailwind-merge'
type Props = { type Props = {
conversation: Conversation conversation: Conversation
@ -35,9 +35,7 @@ const HistoryItem: React.FC<Props> = ({
const setMainViewState = useSetAtom(setMainViewStateAtom) const setMainViewState = useSetAtom(setMainViewStateAtom)
const activeConvoId = useAtomValue(getActiveConvoIdAtom) const activeConvoId = useAtomValue(getActiveConvoIdAtom)
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom) const setActiveConvoId = useSetAtom(setActiveConvoIdAtom)
const updateConvWaiting = useSetAtom( const updateConvWaiting = useSetAtom(updateConversationWaitingForResponseAtom)
updateConversationWaitingForResponseAtom
)
const updateConvError = useSetAtom(updateConversationErrorAtom) const updateConvError = useSetAtom(updateConversationErrorAtom)
const isSelected = activeConvoId === conversation._id const isSelected = activeConvoId === conversation._id
@ -62,43 +60,33 @@ const HistoryItem: React.FC<Props> = ({
setMainViewState(MainViewState.Conversation) setMainViewState(MainViewState.Conversation)
setActiveConvoId(conversation._id) setActiveConvoId(conversation._id)
} }
}; }
const backgroundColor = isSelected const backgroundColor = isSelected ? 'bg-background/80' : 'bg-background/20'
? "bg-gray-100 dark:bg-gray-700" const description = conversation?.lastMessage ?? 'No new message'
: "bg-white dark:bg-gray-500"
const description = conversation?.lastMessage ?? "No new message"
return ( return (
<li <li
role="button" role="button"
className={`ml-3 mr-2 flex flex-row rounded p-3 ${backgroundColor} hover:bg-hover-light`} className={twMerge(
'flex flex-row rounded-md border border-border p-3',
backgroundColor
)}
onClick={onClick} onClick={onClick}
> >
<div className="h-8 w-8"> <div className="flex flex-1 flex-col">
<Image
width={32}
height={32}
src={avatarUrl ?? 'icons/app_icon.svg'}
className="aspect-square rounded-full"
alt=""
/>
</div>
<div className="ml-2 flex flex-1 flex-col">
{/* title */} {/* title */}
<div className="flex">
<span className="line-clamp-1 flex-1 text-gray-900"> <span className="mb-1 line-clamp-1 leading-5 text-muted-foreground">
{summary ?? name} {updatedAt && displayDate(new Date(updatedAt).getTime())}
</span> </span>
<span className="line-clamp-1 text-xs leading-5 text-gray-500">
{updatedAt && displayDate(new Date(updatedAt).getTime())} <span className="line-clamp-1">{summary ?? name}</span>
</span>
</div>
{/* description */} {/* description */}
<span className="mt-1 line-clamp-2 text-gray-400">{description}</span> <span className="mt-1 line-clamp-2 text-muted-foreground">
{description}
</span>
</div> </div>
</li> </li>
) )

View File

@ -2,15 +2,15 @@ import HistoryItem from '../HistoryItem'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import ExpandableHeader from '../ExpandableHeader' import ExpandableHeader from '../ExpandableHeader'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { searchAtom } from '@/_helpers/JotaiWrapper' import { searchAtom } from '@helpers/JotaiWrapper'
import useGetUserConversations from '@/_hooks/useGetUserConversations' import useGetUserConversations from '@hooks/useGetUserConversations'
import SidebarEmptyHistory from '../SidebarEmptyHistory' import SidebarEmptyHistory from '../SidebarEmptyHistory'
import { userConversationsAtom } from '@/_helpers/atoms/Conversation.atom' import { userConversationsAtom } from '@helpers/atoms/Conversation.atom'
import { twMerge } from 'tailwind-merge'
const HistoryList: React.FC = () => { const HistoryList: React.FC = () => {
const conversations = useAtomValue(userConversationsAtom) const conversations = useAtomValue(userConversationsAtom)
const searchText = useAtomValue(searchAtom) const searchText = useAtomValue(searchAtom)
const [expand, setExpand] = useState<boolean>(true)
const { getUserConversations } = useGetUserConversations() const { getUserConversations } = useGetUserConversations()
useEffect(() => { useEffect(() => {
@ -18,17 +18,9 @@ const HistoryList: React.FC = () => {
}, []) }, [])
return ( return (
<div className="flex flex-grow flex-col gap-2 overflow-hidden pt-3"> <div className="flex flex-grow flex-col gap-2">
<ExpandableHeader <ExpandableHeader title="CHAT HISTORY" />
title="CHAT HISTORY" <ul className={twMerge('mt-1 flex flex-col gap-y-3 overflow-y-auto')}>
expanded={expand}
onClick={() => setExpand(!expand)}
/>
<ul
className={`scroll mt-1 flex flex-col gap-1 overflow-y-auto ${
!expand ? 'hidden ' : 'block'
}`}
>
{conversations.length > 0 ? ( {conversations.length > 0 ? (
conversations conversations
.filter( .filter(

View File

@ -1,21 +1,22 @@
/* eslint-disable react-hooks/rules-of-hooks */
'use client' 'use client'
import BasicPromptInput from '../BasicPromptInput' import BasicPromptInput from '../BasicPromptInput'
import BasicPromptAccessories from '../BasicPromptAccessories' import BasicPromptAccessories from '../BasicPromptAccessories'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom'
import SecondaryButton from '../SecondaryButton' import SecondaryButton from '../SecondaryButton'
import { Fragment, useEffect, useState } from 'react' import { Fragment, useEffect, useState } from 'react'
import { PlusIcon } from '@heroicons/react/24/outline' import { PlusIcon } from '@heroicons/react/24/outline'
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { import {
currentConversationAtom, currentConversationAtom,
currentConvoStateAtom, currentConvoStateAtom,
} from '@/_helpers/atoms/Conversation.atom' } from '@helpers/atoms/Conversation.atom'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { activeBotAtom } from '@/_helpers/atoms/Bot.atom' import { activeBotAtom } from '@helpers/atoms/Bot.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
const InputToolbar: React.FC = () => { const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom) const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom)
@ -62,6 +63,7 @@ const InputToolbar: React.FC = () => {
} }
} }
getReplyState() getReplyState()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentConvo]) }, [currentConvo])
const onNewConversationClick = () => { const onNewConversationClick = () => {
@ -70,26 +72,19 @@ const InputToolbar: React.FC = () => {
} }
} }
if (showingAdvancedPrompt) { if (inputState === 'loading') return <div>Loading..</div>
return <div />
}
if (inputState === 'loading') {
return <div>Loading..</div>
}
if (inputState === 'disabled') {
// text italic
if (inputState === 'disabled')
return ( return (
<p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center text-sm italic text-gray-600"> <div className="bg-background/90 sticky bottom-0 flex items-center justify-center">
{error} <p className="mx-auto my-5 line-clamp-2 text-ellipsis text-center italic text-gray-600">
</p> {error}
</p>
</div>
) )
}
return ( return (
<Fragment> <div className="bg-background/90 sticky bottom-0 w-full px-5 py-0">
{currentConvoState?.error && ( {currentConvoState?.error && (
<div className="flex flex-row justify-center"> <div className="flex flex-row justify-center">
<span className="mx-5 my-2 text-sm text-red-500"> <span className="mx-5 my-2 text-sm text-red-500">
@ -105,13 +100,13 @@ const InputToolbar: React.FC = () => {
/> />
</div> </div>
{/* My text input */} {/* My text input */}
<div className="mx-12 mb-5 flex items-start space-x-4 md:mx-32 2xl:mx-64"> <div className="mb-5 flex items-start space-x-4">
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
<BasicPromptInput /> <BasicPromptInput />
<BasicPromptAccessories /> <BasicPromptAccessories />
</div> </div>
</div> </div>
</Fragment> </div>
) )
} }

View File

@ -1,9 +1,9 @@
import React, { Fragment } from "react" import React, { Fragment } from 'react'
import HistoryList from "../HistoryList" import HistoryList from '../HistoryList'
import LeftHeaderAction from "../LeftHeaderAction" import LeftHeaderAction from '../LeftHeaderAction'
import { leftSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom" import { leftSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import { useAtomValue } from "jotai" import { useAtomValue } from 'jotai'
import { Variants, motion } from "framer-motion" import { Variants, motion } from 'framer-motion'
const leftSideBarVariants: Variants = { const leftSideBarVariants: Variants = {
show: { show: {
@ -13,7 +13,7 @@ const leftSideBarVariants: Variants = {
transition: { duration: 0.3 }, transition: { duration: 0.3 },
}, },
hide: { hide: {
x: "-100%", x: '-100%',
width: 0, width: 0,
opacity: 0, opacity: 0,
transition: { duration: 0.3 }, transition: { duration: 0.3 },
@ -26,13 +26,13 @@ const LeftContainer: React.FC = () => {
return ( return (
<motion.div <motion.div
initial={false} initial={false}
animate={isVisible ? "show" : "hide"} animate={isVisible ? 'show' : 'hide'}
variants={leftSideBarVariants} variants={leftSideBarVariants}
className="flex flex-col w-80 flex-shrink-0 border-r border-gray-200" className="flex w-80 flex-shrink-0 flex-col dark:bg-gray-950/50"
> >
{isVisible && ( {isVisible && (
<Fragment> <Fragment>
<LeftHeaderAction /> {/* <LeftHeaderAction /> */}
<HistoryList /> <HistoryList />
</Fragment> </Fragment>
)} )}

View File

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

View File

@ -2,9 +2,9 @@ import {
MainViewState, MainViewState,
getMainViewStateAtom, getMainViewStateAtom,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import Image from 'next/image'
import CompactLogo from '../CompactLogo' import CompactLogo from '../../../containers/Logo/CompactLogo'
import { import {
ChatBubbleOvalLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon,
Cog8ToothIcon, Cog8ToothIcon,
@ -13,24 +13,26 @@ import {
Squares2X2Icon, Squares2X2Icon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { showingBotListModalAtom } from '@/_helpers/atoms/Modal.atom' import { showingBotListModalAtom } from '@helpers/atoms/Modal.atom'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import useGetBots from '@/_hooks/useGetBots' import useGetBots from '@hooks/useGetBots'
import { Icons, Toggle } from '@uikit'
const menu = [ const menu = [
{ // {
name: 'Explore Models', // name: 'Explore Models',
iconComponent: <CpuChipIcon width={24} height={24} color="#C7D2FE" />, // icon: <CpuChipIcon />,
state: MainViewState.ExploreModel, // state: MainViewState.ExploreModel,
}, // },
{ {
name: 'My Models', name: 'My Models',
iconComponent: <Squares2X2Icon width={24} height={24} color="#C7D2FE" />, icon: <Icons name="layout-grid" />,
state: MainViewState.MyModel, state: MainViewState.MyModel,
}, },
{ {
name: 'Settings', name: 'Settings',
iconComponent: <Cog8ToothIcon width={24} height={24} color="#C7D2FE" />, icon: <Icons name="settings" />,
state: MainViewState.Setting, state: MainViewState.Setting,
}, },
] ]
@ -51,7 +53,7 @@ const LeftRibbonNav: React.FC = () => {
const bgColor = isConversationView ? 'bg-gray-500' : '' const bgColor = isConversationView ? 'bg-gray-500' : ''
const onConversationClick = () => { const onConversationClick = () => {
if (currentState === MainViewState.Conversation) return // if (currentState === MainViewState.Conversation) return
setMainViewState(MainViewState.Conversation) setMainViewState(MainViewState.Conversation)
} }
@ -71,52 +73,42 @@ const LeftRibbonNav: React.FC = () => {
} }
return ( return (
<nav className="flex h-screen w-20 flex-col bg-gray-900"> <nav className="flex h-screen flex-shrink-0 flex-col items-center pt-10">
<div className='mx-auto mt-4'>
<CompactLogo /> <CompactLogo />
</div> <div className="flex w-full flex-1 flex-col items-center justify-between">
<div className="flex flex-col pt-4">
<div className="flex w-full flex-1 flex-col items-center justify-between px-3 py-6"> <button onClick={onConversationClick}>
<div className="flex flex-1 flex-col gap-2">
<button
onClick={onConversationClick}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`}
>
<ChatBubbleOvalLeftEllipsisIcon <ChatBubbleOvalLeftEllipsisIcon
width={24} width={24}
height={24} height={24}
color="#C7D2FE" color="text-white"
/> />
</button> </button>
<button <button onClick={onBotListClick}>
onClick={onBotListClick} <CubeTransparentIcon />
className={`rounded-lg p-4 hover:bg-gray-400`}
>
<CubeTransparentIcon width={24} height={24} color="#C7D2FE" />
</button> </button>
</div> </div>
<ul className="flex flex-col gap-3"> <ul className="flex flex-col gap-3 py-8">
{menu.map((item) => { {menu.map((item) => {
const bgColor = currentState === item.state ? 'bg-gray-500' : '' const bgColor = currentState === item.state ? 'bg-gray-500' : ''
return ( return (
<li <li
role="button" role="button"
data-testid={item.name}
key={item.name} key={item.name}
className={`rounded-lg p-4 ${bgColor} hover:bg-gray-400`} className="item-center flex gap-x-2"
onClick={() => onMenuClick(item.state)} onClick={() => onMenuClick(item.state)}
> >
{item.iconComponent} {item.icon}
<span className="text-xs">{item.name}</span>
</li> </li>
) )
})} })}
</ul> </ul>
</div> </div>
{/* User avatar */} {/* User avatar */}
<div className="flex items-center justify-center pb-5"> {/* <div className="pb-5 flex items-center justify-center">
<Image src={'/icons/avatar.svg'} width={40} height={40} alt="" /> <Image src={"/icons/avatar.svg"} width={40} height={40} alt="" />
</div> </div> */}
</nav> </nav>
) )
} }

View File

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

View File

@ -1,21 +1,19 @@
import React from "react" import React from 'react'
import LeftContainer from "../LeftContainer" import LeftContainer from '../LeftContainer'
import LeftRibbonNav from "../LeftRibbonNav" import LeftRibbonNav from '../LeftRibbonNav'
import MonitorBar from "../MonitorBar" import MonitorBar from '../MonitorBar'
import RightContainer from "../RightContainer" import RightContainer from '../RightContainer'
import CenterContainer from "../CenterContainer" import CenterContainer from '../CenterContainer'
const MainContainer: React.FC = () => ( const MainContainer: React.FC = () => (
<div className="flex h-screen"> <div className="flex h-screen">
<LeftRibbonNav /> <LeftRibbonNav />
<div className="flex flex-1 flex-col ">
<div className="flex flex-1 flex-col h-full">
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<LeftContainer /> <LeftContainer />
<CenterContainer /> <CenterContainer />
<RightContainer /> <RightContainer />
</div> </div>
<MonitorBar /> <MonitorBar />
</div> </div>
</div> </div>

View File

@ -1,19 +1,17 @@
import { currentConversationAtom } from '@/_helpers/atoms/Conversation.atom' import { currentConversationAtom } from '@helpers/atoms/Conversation.atom'
import { import {
leftSideBarExpandStateAtom, leftSideBarExpandStateAtom,
rightSideBarExpandStateAtom, rightSideBarExpandStateAtom,
showRightSideBarToggleAtom, } from '@helpers/atoms/SideBarExpand.atom'
} from '@/_helpers/atoms/LeftSideBarExpand.atom' import { showConfirmDeleteConversationModalAtom } from '@helpers/atoms/Modal.atom'
import { TrashIcon } from '@heroicons/react/24/outline' import { ChartPieIcon, TrashIcon } from '@heroicons/react/24/outline'
import { showConfirmDeleteConversationModalAtom } from '@/_helpers/atoms/Modal.atom'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import React from 'react' import React from 'react'
import Image from 'next/image' import { Trash2 } from 'lucide-react'
const MainHeader: React.FC = () => { const MainHeader: React.FC = () => {
const setLeftSideBarVisibility = useSetAtom(leftSideBarExpandStateAtom) const setLeftSideBarVisibility = useSetAtom(leftSideBarExpandStateAtom)
const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom) const setRightSideBarVisibility = useSetAtom(rightSideBarExpandStateAtom)
const showRightSideBarToggle = useAtomValue(showRightSideBarToggleAtom)
const setShowConfirmDeleteConversationModal = useSetAtom( const setShowConfirmDeleteConversationModal = useSetAtom(
showConfirmDeleteConversationModalAtom showConfirmDeleteConversationModalAtom
) )
@ -22,43 +20,19 @@ const MainHeader: React.FC = () => {
const currentConvo = useAtomValue(currentConversationAtom) const currentConvo = useAtomValue(currentConversationAtom)
let title = currentConvo?.name ?? '' let title = currentConvo?.name ?? ''
return ( if (!activeConversation) return null
<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"> return (
{title} <div className="sticky top-0 border-b border-border bg-background/90 px-4 py-2">
</span> <span className="font-semibold text-muted-foreground">{title}</span>
{/* right most */} {/* right most */}
<div className="flex gap-4"> <div className="absolute right-4 top-2">
{activeConversation != null && ( <Trash2
<TrashIcon role="button"
role="button" size={16}
width={24} onClick={() => setShowConfirmDeleteConversationModal(true)}
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>
</div> </div>
) )

View File

@ -8,11 +8,10 @@ import ExploreModelContainer from '../ExploreModelContainer'
import { import {
MainViewState, MainViewState,
getMainViewStateAtom, getMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import EmptyChatContainer from '../EmptyChatContainer' import EmptyChatContainer from '../EmptyChatContainer'
import MainChat from '../MainChat' import MainChat from '../MainChat'
import CreateBotContainer from '../CreateBotContainer' import CreateBotContainer from '../CreateBotContainer'
import BotInfoContainer from '../BotInfoContainer'
const MainView: React.FC = () => { const MainView: React.FC = () => {
const viewState = useAtomValue(getMainViewStateAtom) const viewState = useAtomValue(getMainViewStateAtom)
@ -38,9 +37,6 @@ const MainView: React.FC = () => {
case MainViewState.Welcome: case MainViewState.Welcome:
children = <Welcome /> children = <Welcome />
break break
case MainViewState.BotInfo:
children = <BotInfoContainer />
break
default: default:
children = <MainChat /> children = <MainChat />
break break

View File

@ -3,7 +3,7 @@ import { Popover, Transition } from '@headlessui/react'
import { Fragment } from 'react' import { Fragment } from 'react'
// import useGetCurrentUser from "@/_hooks/useGetCurrentUser"; // import useGetCurrentUser from "@/_hooks/useGetCurrentUser";
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { showConfirmSignOutModalAtom } from '@/_helpers/atoms/Modal.atom' import { showConfirmSignOutModalAtom } from '@helpers/atoms/Modal.atom'
export const MenuHeader: React.FC = () => { export const MenuHeader: React.FC = () => {
const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom) const setShowConfirmSignOutModal = useSetAtom(showConfirmSignOutModalAtom)

View File

@ -3,7 +3,7 @@ import { Dialog } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/24/outline' import { XMarkIcon } from '@heroicons/react/24/outline'
import Image from 'next/image' import Image from 'next/image'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { showingMobilePaneAtom } from '@/_helpers/atoms/Modal.atom' import { showingMobilePaneAtom } from '@helpers/atoms/Modal.atom'
const MobileMenuPane: React.FC = () => { const MobileMenuPane: React.FC = () => {
const [show, setShow] = useAtom(showingMobilePaneAtom) const [show, setShow] = useAtom(showingMobilePaneAtom)

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PrimaryButton from '../PrimaryButton' import { Button } from '@uikit'
import ModelActionMenu from '../ModelActionMenu'
export enum ModelActionType { export enum ModelActionType {
Start = 'Start', Start = 'Start',
@ -8,41 +9,45 @@ export enum ModelActionType {
type ModelActionStyle = { type ModelActionStyle = {
title: string title: string
backgroundColor: string
textColor: string
} }
const modelActionMapper: Record<ModelActionType, ModelActionStyle> = { const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
[ModelActionType.Start]: { [ModelActionType.Start]: {
title: 'Start', title: 'Start',
backgroundColor: 'bg-blue-500 hover:bg-blue-600',
textColor: 'text-white',
}, },
[ModelActionType.Stop]: { [ModelActionType.Stop]: {
title: 'Stop', title: 'Stop',
backgroundColor: 'bg-red-500 hover:bg-red-600',
textColor: 'text-white',
}, },
} }
type Props = { type Props = {
type: ModelActionType type: ModelActionType
onActionClick: (type: ModelActionType) => void onActionClick: (type: ModelActionType) => void
onDeleteClick: () => void
} }
const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => { const ModelActionButton: React.FC<Props> = ({
type,
onActionClick,
onDeleteClick,
}) => {
const styles = modelActionMapper[type] const styles = modelActionMapper[type]
const onClick = () => { const onClick = () => {
onActionClick(type) onActionClick(type)
} }
return ( return (
<td className="whitespace-nowrap px-6 py-4 text-sm"> <td className="whitespace-nowrap px-3 py-2 text-right">
<PrimaryButton <div className="flex items-center justify-end gap-x-4">
title={styles.title} <ModelActionMenu onDeleteClick={onDeleteClick} />
onClick={onClick} <Button
className={styles.backgroundColor} size="sm"
/> themes={styles.title === 'Start' ? 'accent' : 'default'}
onClick={onClick}
>
{styles.title} Model
</Button>
</div>
</td> </td>
) )
} }

View File

@ -7,36 +7,9 @@ type Props = {
} }
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => ( const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => (
<Menu as="div" className="relative flex-none"> <button className="text-muted-foreground text-xs" onClick={onDeleteClick}>
<Menu.Button className="block text-gray-500 hover:text-gray-900"> Delete
<span className="sr-only">Open options</span> </button>
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
className={`${
active ? 'bg-violet-500 text-white' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={onDeleteClick}
>
Delete
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
) )
export default ModelActionMenu export default ModelActionMenu

View File

@ -1,4 +1,4 @@
import { toGigabytes } from '@/_utils/converter' import { toGigabytes } from '@utils/converter'
type Props = { type Props = {
total: number total: number

View File

@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import { DownloadState } from '@/_models/DownloadState'
import { import {
formatDownloadPercentage, formatDownloadPercentage,
formatDownloadSpeed, formatDownloadSpeed,
toGigabytes, toGigabytes,
} from '@/_utils/converter' } from '@utils/converter'
type Props = { type Props = {
downloadState: DownloadState downloadState: DownloadState

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import ModelTableHeader from '../ModelTableHeader' import ModelTableHeader from '../ModelTableHeader'
import { DownloadState } from '@/_models/DownloadState'
import ModelDownloadingRow from '../ModelDownloadingRow' import ModelDownloadingRow from '../ModelDownloadingRow'
type Props = { type Props = {

View File

@ -1,13 +1,11 @@
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { ModelStatus, ModelStatusComponent } from '../ModelStatusComponent' import { ModelStatus, ModelStatusComponent } from '../ModelStatusComponent'
import ModelActionMenu from '../ModelActionMenu'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import ModelActionButton, { ModelActionType } from '../ModelActionButton' import ModelActionButton, { ModelActionType } from '../ModelActionButton'
import useStartStopModel from '@/_hooks/useStartStopModel' import useStartStopModel from '@hooks/useStartStopModel'
import useDeleteModel from '@/_hooks/useDeleteModel' import useDeleteModel from '@hooks/useDeleteModel'
import { AssistantModel } from '@/_models/AssistantModel' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { toGigabytes } from '@utils/converter'
import { toGigabytes } from '@/_utils/converter'
type Props = { type Props = {
model: AssistantModel model: AssistantModel
@ -38,32 +36,31 @@ const ModelRow: React.FC<Props> = ({ model }) => {
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
deleteModel(model) deleteModel(model)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [model]) }, [model])
return ( return (
<tr className="border-b border-gray-200 last:rounded-lg last:border-b-0"> <tr className="bg-background/50 border-border border-b last:rounded-lg last:border-b-0">
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900"> <td className="text-muted-foreground whitespace-nowrap px-3 font-semibold">
{model.name} {model.name}
<span className="font-normal text-gray-500">{model.version}</span> <span className="ml-2 font-semibold">v{model.version}</span>
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="text-muted-foreground whitespace-nowrap px-3">
<div className="flex flex-col justify-start"> <div className="flex flex-col justify-start">
<span>GGUF</span> <span>GGUF</span>
</div> </div>
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="text-muted-foreground whitespace-nowrap px-3">
{toGigabytes(model.size)} {toGigabytes(model.size)}
</td> </td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500"> <td className="text-muted-foreground whitespace-nowrap px-3">
<ModelStatusComponent status={status} /> <ModelStatusComponent status={status} />
</td> </td>
<ModelActionButton <ModelActionButton
type={actionButtonType} type={actionButtonType}
onActionClick={onModelActionClick} onActionClick={onModelActionClick}
onDeleteClick={onDeleteClick}
/> />
<td className="relative w-fit whitespace-nowrap px-6 py-4 text-right text-sm font-medium">
<ModelActionMenu onDeleteClick={onDeleteClick} />
</td>
</tr> </tr>
) )
} }

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { searchingModelText } from '@/_helpers/JotaiWrapper' import { searchingModelText } from '@helpers/JotaiWrapper'
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'

View File

@ -2,9 +2,8 @@ import { Fragment, useEffect } from 'react'
import { Listbox, Transition } from '@headlessui/react' import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid' import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { selectedModelAtom } from '@/_helpers/atoms/Model.atom' import { selectedModelAtom } from '@helpers/atoms/Model.atom'
import { downloadedModelAtom } from '@/_helpers/atoms/DownloadedModel.atom' import { downloadedModelAtom } from '@helpers/atoms/DownloadedModel.atom'
import { AssistantModel } from '@/_models/AssistantModel'
function classNames(...classes: any) { function classNames(...classes: any) {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')

View File

@ -16,17 +16,17 @@ export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
[ModelStatus.Installed]: { [ModelStatus.Installed]: {
title: 'Installed', title: 'Installed',
textColor: 'text-black', textColor: 'text-black',
backgroundColor: 'bg-gray-100', backgroundColor: 'bg-gray-200 text-gray-600',
}, },
[ModelStatus.Active]: { [ModelStatus.Active]: {
title: 'Active', title: 'Active',
textColor: 'text-black', textColor: 'text-green-800',
backgroundColor: 'bg-green-100', backgroundColor: 'bg-green-100 dark:bg-green-300',
}, },
[ModelStatus.RunningInNitro]: { [ModelStatus.RunningInNitro]: {
title: 'Running in Nitro', title: 'Running in Nitro',
textColor: 'text-black', textColor: 'text-green-800',
backgroundColor: 'bg-green-100', backgroundColor: 'bg-green-100 dark:bg-green-300',
}, },
} }
@ -38,7 +38,7 @@ export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
const statusType = ModelStatusMapper[status] const statusType = ModelStatusMapper[status]
return ( return (
<div <div
className={`w-fit rounded-[10px] px-2.5 py-0.5 text-xs font-medium ${statusType.backgroundColor}`} className={`w-fit rounded-full px-2.5 py-0.5 text-xs font-medium ${statusType.backgroundColor}`}
> >
{statusType.title} {statusType.title}
</div> </div>

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import ModelRow from '../ModelRow' import ModelRow from '../ModelRow'
import ModelTableHeader from '../ModelTableHeader' import ModelTableHeader from '../ModelTableHeader'
import { AssistantModel } from '@/_models/AssistantModel'
type Props = { type Props = {
models: AssistantModel[] models: AssistantModel[]
@ -10,25 +9,25 @@ type Props = {
const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS'] const tableHeaders = ['MODEL', 'FORMAT', 'SIZE', 'STATUS', 'ACTIONS']
const ModelTable: React.FC<Props> = ({ models }) => ( const ModelTable: React.FC<Props> = ({ models }) => (
<div className="flow-root min-w-full rounded-lg border border-gray-200 align-middle shadow-lg"> <>
<table className="min-w-full"> <div className="border-border overflow-hidden rounded-lg border align-middle shadow-lg">
<thead className="border-b border-gray-200 bg-gray-50"> <table className="min-w-full">
<tr className="rounded-t-lg"> <thead className="bg-background">
{tableHeaders.map((item) => ( <tr className="rounded-t-lg">
<ModelTableHeader key={item} title={item} /> {tableHeaders.map((item) => (
<ModelTableHeader key={item} title={item} />
))}
</tr>
</thead>
<tbody>
{models.map((model) => (
<ModelRow key={model._id} model={model} />
))} ))}
<th scope="col" className="relative w-fit px-6 py-3"> </tbody>
<span className="sr-only">Edit</span> </table>
</th> </div>
</tr> <div className="relative"></div>
</thead> </>
<tbody>
{models.map((model) => (
<ModelRow key={model._id} model={model} />
))}
</tbody>
</table>
</div>
) )
export default React.memo(ModelTable) export default React.memo(ModelTable)

View File

@ -7,7 +7,7 @@ type Props = {
const ModelTableHeader: React.FC<Props> = ({ title }) => ( const ModelTableHeader: React.FC<Props> = ({ title }) => (
<th <th
scope="col" scope="col"
className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-gray-500 first:rounded-tl-lg last:rounded-tr-lg" className="text-muted-foreground border-border border-b p-3 text-left text-xs font-semibold uppercase first:rounded-tl-lg last:rounded-tr-lg last:text-right"
> >
{title} {title}
</th> </th>

View File

@ -1,12 +1,10 @@
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { formatDownloadPercentage, toGigabytes } from '@/_utils/converter' import { formatDownloadPercentage, toGigabytes } from '@utils/converter'
import Image from 'next/image' import Image from 'next/image'
import { Product } from '@/_models/Product' import useDownloadModel from '@hooks/useDownloadModel'
import useDownloadModel from '@/_hooks/useDownloadModel' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom'
import { atom, useAtomValue } from 'jotai' import { atom, useAtomValue } from 'jotai'
import { ModelVersion } from '@/_models/ModelVersion' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels'
import SimpleTag from '../SimpleTag' import SimpleTag from '../SimpleTag'
import { RamRequired, UsecaseTag } from '../SimpleTag/TagType' import { RamRequired, UsecaseTag } from '../SimpleTag/TagType'
@ -56,10 +54,9 @@ const ModelVersionItem: React.FC<Props> = ({
const { maxRamRequired, usecase } = modelVersion const { maxRamRequired, usecase } = modelVersion
return ( return (
<div className="flex items-center justify-between gap-4 border-t border-gray-200 pb-3 pl-3 pr-4 pt-3 first:border-t-0"> <div className="border-border flex items-center justify-between gap-4 border-t pb-3 pl-3 pr-4 pt-3 first:border-t-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Image src={'/icons/app_icon.svg'} width={14} height={20} alt="" /> <span className="font-sm text-muted-foreground mb-4 line-clamp-1 flex-1">
<span className="font-sm flex-1 text-gray-900">
{modelVersion.name} {modelVersion.name}
</span> </span>
</div> </div>
@ -75,9 +72,9 @@ const ModelVersionItem: React.FC<Props> = ({
type={RamRequired.RamDefault} type={RamRequired.RamDefault}
clickable={false} clickable={false}
/> />
</div> <div className="bg-background border-border rounded-full border px-2.5 py-0.5 font-medium">
<div className="rounded bg-gray-200 px-2.5 py-0.5 text-xs font-medium"> {toGigabytes(modelVersion.size)}
{toGigabytes(modelVersion.size)} </div>
</div> </div>
{downloadButton} {downloadButton}
</div> </div>

View File

@ -1,7 +1,5 @@
import React from 'react' import React from 'react'
import ModelVersionItem from '../ModelVersionItem' import ModelVersionItem from '../ModelVersionItem'
import { Product } from '@/_models/Product'
import { ModelVersion } from '@/_models/ModelVersion'
type Props = { type Props = {
model: Product model: Product
@ -15,20 +13,15 @@ const ModelVersionList: React.FC<Props> = ({
recommendedVersion, recommendedVersion,
}) => { }) => {
return ( return (
<div className="border-t border-gray-200 px-4 py-5"> <div className="pt-4">
<div className="text-sm font-medium text-gray-500"> {versions.map((item) => (
Available Versions <ModelVersionItem
</div> key={item._id}
<div className="overflow-hidden rounded-lg border border-gray-200"> model={model}
{versions.map((item) => ( modelVersion={item}
<ModelVersionItem isRecommended={item._id === recommendedVersion}
key={item._id} />
model={model} ))}
modelVersion={item}
isRecommended={item._id === recommendedVersion}
/>
))}
</div>
</div> </div>
) )
} }

View File

@ -1,13 +1,12 @@
import ProgressBar from '../ProgressBar' import ProgressBar from '@/_components/ProgressBar'
import SystemItem from '../SystemItem' import SystemItem from '@containers/SystemItem'
import { useAtomValue } from 'jotai' import { useAtomValue } from 'jotai'
import { appDownloadProgress } from '@/_helpers/JotaiWrapper' import { appDownloadProgress } from '@helpers/JotaiWrapper'
import useGetAppVersion from '@/_hooks/useGetAppVersion' import useGetAppVersion from '@hooks/useGetAppVersion'
import useGetSystemResources from '@/_hooks/useGetSystemResources' import useGetSystemResources from '@hooks/useGetSystemResources'
import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom'
import { DownloadState } from '@/_models/DownloadState' import { formatDownloadPercentage } from '@utils/converter'
import { formatDownloadPercentage } from '@/_utils/converter' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom'
const MonitorBar: React.FC = () => { const MonitorBar: React.FC = () => {
const progress = useAtomValue(appDownloadProgress) const progress = useAtomValue(appDownloadProgress)
@ -22,7 +21,7 @@ const MonitorBar: React.FC = () => {
} }
return ( return (
<div className="flex flex-row items-center justify-between border-t border-gray-200"> <div className="flex flex-row items-center justify-between">
{progress && progress >= 0 ? ( {progress && progress >= 0 ? (
<ProgressBar total={100} used={progress} /> <ProgressBar total={100} used={progress} />
) : null} ) : null}
@ -40,7 +39,7 @@ const MonitorBar: React.FC = () => {
{activeModel && ( {activeModel && (
<SystemItem name={`Active model: ${activeModel.name}`} value={''} /> <SystemItem name={`Active model: ${activeModel.name}`} value={''} />
)} )}
<span className="text-sm text-gray-900">v{version}</span> <span className="text-xs">v{version}</span>
</div> </div>
</div> </div>
) )

View File

@ -35,7 +35,6 @@ export const Preferences = () => {
useEffect(() => { useEffect(() => {
// @ts-ignore // @ts-ignore
import(/* webpackIgnore: true */ PLUGIN_CATALOGS).then((module) => { import(/* webpackIgnore: true */ PLUGIN_CATALOGS).then((module) => {
console.log(module)
setPluginCatalog(module.default) setPluginCatalog(module.default)
}) })
}, []) }, [])
@ -149,8 +148,11 @@ export const Preferences = () => {
if (timeout) { if (timeout) {
clearTimeout(timeout) clearTimeout(timeout)
} }
if(extensionPoints.get(PluginService.OnPreferencesUpdate)) if (extensionPoints.get(PluginService.OnPreferencesUpdate))
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100) timeout = setTimeout(
() => execute(PluginService.OnPreferencesUpdate),
100
)
} }
/** /**

View File

@ -16,9 +16,9 @@ const PrimaryButton: React.FC<Props> = ({
className, className,
}) => ( }) => (
<button <button
onClick={() => onClick?.()} onClick={onClick}
type={isSubmit ? "submit" : "button"} 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} ${ className={`line-clamp-1 flex-shrink-0 rounded-md bg-accent px-3 py-1 font-semibold text-white shadow-sm hover:bg-accent/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ${className} ${
fullWidth ? 'flex-1 ' : '' fullWidth ? 'flex-1 ' : ''
}}`} }}`}
> >

View File

@ -1,4 +1,4 @@
import { formatTwoDigits } from '@/_utils/converter' import { formatTwoDigits } from '@utils/converter'
import React from 'react' import React from 'react'
type Props = { type Props = {

View File

@ -1,9 +1,9 @@
import { rightSideBarExpandStateAtom } from "@/_helpers/atoms/LeftSideBarExpand.atom" import { rightSideBarExpandStateAtom } from '@helpers/atoms/SideBarExpand.atom'
import { Variants, motion } from "framer-motion" import { Variants, motion } from 'framer-motion'
import { useAtomValue } from "jotai" import { useAtomValue } from 'jotai'
import { Fragment } from "react" import { Fragment } from 'react'
import BotSetting from "../BotSetting" import BotSetting from '../BotSetting'
import BotInfo from "../BotInfo" import BotInfo from '../BotInfo'
const variants: Variants = { const variants: Variants = {
show: { show: {
@ -13,7 +13,7 @@ const variants: Variants = {
transition: { duration: 0.3 }, transition: { duration: 0.3 },
}, },
hide: { hide: {
x: "100%", x: '100%',
width: 0, width: 0,
opacity: 0, opacity: 0,
transition: { duration: 0.3 }, transition: { duration: 0.3 },
@ -26,9 +26,9 @@ const RightContainer = () => {
return ( return (
<motion.div <motion.div
initial={false} initial={false}
animate={isVisible ? "show" : "hide"} animate={isVisible ? 'show' : 'hide'}
variants={variants} variants={variants}
className="flex flex-col w-80 flex-shrink-0 py-3 border-l border-gray-200 overflow-y-auto scroll" className="scroll flex w-80 flex-shrink-0 flex-col overflow-y-auto border-l border-gray-200 py-3"
> >
{isVisible && ( {isVisible && (
<Fragment> <Fragment>

View File

@ -1,4 +1,4 @@
import { modelSearchAtom } from '@/_helpers/JotaiWrapper' import { modelSearchAtom } from '@helpers/JotaiWrapper'
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'
import { useSetAtom } from 'jotai' import { useSetAtom } from 'jotai'
import { useDebouncedCallback } from 'use-debounce' import { useDebouncedCallback } from 'use-debounce'

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { Button } from '@uikit'
type Props = { type Props = {
title: string title: string
@ -15,15 +16,10 @@ const SecondaryButton: React.FC<Props> = ({
className, className,
icon, icon,
}) => ( }) => (
<button <Button size="sm" disabled={disabled} type="button" onClick={onClick}>
disabled={disabled} {icon}&nbsp;
type="button"
onClick={onClick}
className={`flex items-center justify-center gap-1 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className} line-clamp-1 flex-shrink-0`}
>
{icon}
{title} {title}
</button> </Button>
) )
export default React.memo(SecondaryButton) export default React.memo(SecondaryButton)

View File

@ -1,7 +1,8 @@
import { currentPromptAtom } from '@/_helpers/JotaiWrapper' import { currentPromptAtom } from '@helpers/JotaiWrapper'
import { currentConvoStateAtom } from '@/_helpers/atoms/Conversation.atom' import { currentConvoStateAtom } from '@helpers/atoms/Conversation.atom'
import useSendChatMessage from '@/_hooks/useSendChatMessage' import useSendChatMessage from '@hooks/useSendChatMessage'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { Button } from '@uikit'
const SendButton: React.FC = () => { const SendButton: React.FC = () => {
const [currentPrompt] = useAtom(currentPromptAtom) const [currentPrompt] = useAtom(currentPromptAtom)
@ -11,19 +12,15 @@ const SendButton: React.FC = () => {
const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false const isWaitingForResponse = currentConvoState?.waitingForResponse ?? false
const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse const disabled = currentPrompt.trim().length === 0 || isWaitingForResponse
const disabledStyle = {
backgroundColor: '#F3F4F6',
}
return ( return (
<button <Button
themes="accent"
onClick={sendChatMessage} onClick={sendChatMessage}
style={disabled ? disabledStyle : {}} disabled={disabled}
type="submit" type="submit"
className="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
> >
Send Send
</button> </Button>
) )
} }

View File

@ -1,16 +1,17 @@
import useCreateConversation from '@/_hooks/useCreateConversation' import useCreateConversation from '@hooks/useCreateConversation'
import PrimaryButton from '../PrimaryButton' import PrimaryButton from '../PrimaryButton'
import { useAtomValue, useSetAtom } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { import {
MainViewState, MainViewState,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom'
import useInitModel from '@/_hooks/useInitModel' import useInitModel from '@hooks/useInitModel'
import { useGetDownloadedModels } from '@/_hooks/useGetDownloadedModels' import { useGetDownloadedModels } from '@hooks/useGetDownloadedModels'
import { AssistantModel } from '@/_models/AssistantModel' import { Button } from '@uikit'
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline"
import {MessageCircle} from "lucide-react"
enum ActionButton { enum ActionButton {
DownloadModel = 'Download a Model', DownloadModel = 'Download a Model',
@ -53,13 +54,15 @@ const SidebarEmptyHistory: React.FC = () => {
return ( return (
<div className="flex flex-col items-center gap-3 py-10"> <div className="flex flex-col items-center gap-3 py-10">
<ChatBubbleOvalLeftEllipsisIcon width={32} height={32} /> <MessageCircle size={32} />
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-y-2">
<div className="text-center text-sm text-gray-900">No Chat History</div> <h6 className="text-center text-base">No Chat History</h6>
<div className="text-center text-sm text-gray-500"> <p className="text-center text-muted-foreground mb-6">
Get started by creating a new chat. Get started by creating a new chat.
</div> </p>
<PrimaryButton title={action} onClick={onClick} /> <Button onClick={onClick} themes="accent" >
{action}
</Button>
</div> </div>
</div> </div>
) )

View File

@ -5,7 +5,7 @@ import {
MainViewState, MainViewState,
getMainViewStateAtom, getMainViewStateAtom,
setMainViewStateAtom, setMainViewStateAtom,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
type Props = { type Props = {
title: string title: string

View File

@ -1,9 +1,9 @@
import { displayDate } from '@/_utils/datetime' import { displayDate } from '../../utils/datetime'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
import JanImage from './JanImage' import JanImage from '../../containers/JanImage'
import Image from 'next/image' import Image from 'next/image'
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline" import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
type Props = { type Props = {
avatarUrl?: string avatarUrl?: string

View File

@ -1,6 +1,6 @@
import Image from 'next/image' import Image from 'next/image'
import JanImage from '../JanImage' import JanImage from '../../../containers/JanImage'
import { displayDate } from '@/_utils/datetime' import { displayDate } from '@utils/datetime'
import Link from 'next/link' import Link from 'next/link'
type Props = { type Props = {

View File

@ -1,21 +1,22 @@
import { TagType } from './TagType' import { TagType } from './TagType'
export const tagStyleMapper: Record<TagType, string> = { export const tagStyleMapper: Record<TagType, string> = {
GGUF: 'bg-yellow-100 text-yellow-800', GGUF: 'bg-yellow-100 dark:bg-yellow-200 text-yellow-800',
PerformancePositive: PerformancePositive:
'text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50', 'text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50 dark:bg-green-200',
PerformanceNeutral: PerformanceNeutral:
'bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20', 'bg-yellow-50 dark:bg-yellow-200 text-yellow-800 ring-1 ring-inset ring-yellow-600/20',
PerformanceNegative: PerformanceNegative:
'bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10', 'bg-red-50 dark:bg-red-200 ext-red-700 ring-1 ring-inset ring-red-600/10',
HardwareCompatible: 'bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10', HardwareCompatible:
'bg-red-50 dark:bg-red-200 ext-red-700 ring-1 ring-inset ring-red-600/10',
HardwareIncompatible: HardwareIncompatible:
'bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10', 'bg-red-50 dark:bg-red-200 text-red-700 ring-1 ring-inset ring-red-600/10',
FreeStyle: 'bg-gray-100 text-gray-800', FreeStyle: 'bg-gray-100 dark:bg-gray-200 text-gray-800',
ExpectPerformanceMedium: 'bg-yellow-100 text-yellow-800', ExpectPerformanceMedium: 'bg-yellow-100 dark:bg-yellow-200 text-yellow-800',
Version: 'bg-red-100 text-yellow-800', Version: 'bg-red-100 dark:bg-red-200 text-yellow-800',
Default: 'bg-blue-100 text-blue-800', Default: 'bg-blue-100 dark:bg-blue-200 text-blue-800',
RamDefault: 'bg-green-50 text-green-700', RamDefault: 'bg-green-50 dark:bg-green-200 text-green-700',
UsecaseDefault: 'bg-orange-100 text-yellow-800', UsecaseDefault: 'bg-orange-100 dark:bg-orange-200 text-yellow-800',
MiscellanousDefault: 'bg-blue-100 text-blue-800', MiscellanousDefault: 'bg-blue-100 dark:bg-blue-200 text-blue-800',
} }

View File

@ -19,7 +19,7 @@ const SimpleTag: React.FC<Props> = ({
if (!clickable) { if (!clickable) {
return ( return (
<div <div
className={`line-clamp-1 max-w-[40%] items-center rounded px-2.5 py-0.5 text-xs font-medium ${tagStyleMapper[type]}`} className={`line-clamp-1 max-w-[70%] items-center rounded-full px-2 py-0.5 text-xs dark:bg-opacity-10 ${tagStyleMapper[type]}`}
> >
{title} {title}
</div> </div>
@ -29,7 +29,7 @@ const SimpleTag: React.FC<Props> = ({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className={`line-clamp-1 max-w-[40%] items-center rounded px-2.5 py-0.5 text-xs font-medium ${tagStyleMapper[type]}`} className={`line-clamp-1 max-w-[70%] items-center rounded-full px-2 py-0.5 text-xs dark:bg-opacity-10 ${tagStyleMapper[type]}`}
> >
{title} x {title} x
</button> </button>

View File

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import { displayDate } from '@/_utils/datetime' import { displayDate } from '@utils/datetime'
import Image from 'next/image' import Image from 'next/image'
import { MessageSenderType } from '@/_models/ChatMessage'
import LoadingIndicator from '../LoadingIndicator' import LoadingIndicator from '../LoadingIndicator'
import { Marked } from 'marked' import { Marked } from 'marked'
import { markedHighlight } from 'marked-highlight' import { markedHighlight } from 'marked-highlight'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import { MessageSenderType } from '@models/ChatMessage'
type Props = { type Props = {
avatarUrl: string avatarUrl: string
@ -50,7 +51,7 @@ const SimpleTextMessage: React.FC<Props> = ({
return ( return (
<div <div
className={`flex items-start gap-2 px-12 md:px-32 2xl:px-64 ${backgroundColor} py-5`} className={`border-border/50 flex items-start gap-x-4 gap-y-2 border-b px-4 py-5 last:border-none`}
> >
<Image <Image
className="rounded-full" className="rounded-full"
@ -61,10 +62,10 @@ const SimpleTextMessage: React.FC<Props> = ({
/> />
<div className="flex w-full flex-col gap-1"> <div className="flex w-full flex-col gap-1">
<div className="flex items-baseline justify-start gap-1"> <div className="flex items-baseline justify-start gap-1">
<div className="text-sm font-extrabold leading-[15.2px] text-[#1B1B1B] dark:text-[#d1d5db]"> <div className="text-sm font-extrabold leading-[15.2px] ">
{senderName} {senderName}
</div> </div>
<div className="text-xs font-medium leading-[13.2px] text-gray-400"> <div className="text-xs font-medium leading-[13.2px]">
{displayDate(createdAt)} {displayDate(createdAt)}
</div> </div>
</div> </div>
@ -72,7 +73,7 @@ const SimpleTextMessage: React.FC<Props> = ({
<LoadingIndicator /> <LoadingIndicator />
) : ( ) : (
<span <span
className="text-sm font-normal leading-loose" className="text-muted-foreground text-xs font-normal leading-loose"
dangerouslySetInnerHTML={{ __html: parsedText }} dangerouslySetInnerHTML={{ __html: parsedText }}
/> />
)} )}

View File

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

View File

@ -1,14 +1,14 @@
import React from "react"; import React from 'react'
import { useController } from "react-hook-form"; import { useController } from 'react-hook-form'
type Props = { type Props = {
id: string; id: string
title: string; title: string
placeholder: string; placeholder: string
description?: string; description?: string
control?: any; control?: any
required?: boolean; required?: boolean
}; }
const TextAreaWithTitle: React.FC<Props> = ({ const TextAreaWithTitle: React.FC<Props> = ({
id, id,
@ -22,27 +22,24 @@ const TextAreaWithTitle: React.FC<Props> = ({
name: id, name: id,
control: control, control: control,
rules: { required: required }, rules: { required: required },
}); })
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label <label htmlFor="comment" className="block font-bold">
htmlFor="comment"
className="block text-base text-gray-900 font-bold"
>
{title} {title}
</label> </label>
{description && ( {description && (
<p className="text-sm text-gray-400 font-normal">{description}</p> <p className="mt-1 font-normal text-muted-foreground">{description}</p>
)} )}
<textarea <textarea
rows={4} 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" className="block w-full resize-none rounded-md border-0 bg-background/80 py-1.5 text-xs leading-relaxed text-background-reverse shadow-sm ring-1 ring-inset ring-border placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-accent/50"
placeholder={placeholder} placeholder={placeholder}
{...field} {...field}
/> />
</div> </div>
); )
}; }
export default TextAreaWithTitle; export default TextAreaWithTitle

View File

@ -1,14 +1,14 @@
import React from "react"; import React from 'react'
import { useController } from "react-hook-form"; import { useController } from 'react-hook-form'
type Props = { type Props = {
id: string; id: string
title: string; title: string
description: string; description: string
placeholder?: string; placeholder?: string
control?: any; control?: any
required?: boolean; required?: boolean
}; }
const TextInputWithTitle: React.FC<Props> = ({ const TextInputWithTitle: React.FC<Props> = ({
id, id,
@ -22,19 +22,19 @@ const TextInputWithTitle: React.FC<Props> = ({
name: id, name: id,
control: control, control: control,
rules: { required: required }, rules: { required: required },
}); })
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="text-gray-900 font-bold">{title}</div> <div className="font-bold">{title}</div>
<div className="text-sm pb-2 text-[#737d7d]">{description}</div> <div className="pb-2 text-muted-foreground">{description}</div>
<input <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" className="block w-full rounded-md border-0 bg-background/80 py-1.5 text-xs shadow-sm ring-1 ring-inset ring-border placeholder:text-muted-foreground focus:ring-2 focus:ring-inset focus:ring-accent/50 sm:leading-6"
placeholder={placeholder} placeholder={placeholder}
{...field} {...field}
/> />
</div> </div>
); )
}; }
export default TextInputWithTitle; export default TextInputWithTitle

View File

@ -1,17 +1,17 @@
import React from "react"; import React from 'react'
import { Switch } from "@headlessui/react"; import { Switch } from '@headlessui/react'
import { Controller } from "react-hook-form"; import { Controller } from 'react-hook-form'
function classNames(...classes: any) { function classNames(...classes: any) {
return classes.filter(Boolean).join(" "); return classes.filter(Boolean).join(' ')
} }
type Props = { type Props = {
id: string; id: string
title: string; title: string
control: any; control: any
required?: boolean; required?: boolean
}; }
const ToggleSwitch: React.FC<Props> = ({ const ToggleSwitch: React.FC<Props> = ({
id, id,
@ -20,7 +20,7 @@ const ToggleSwitch: React.FC<Props> = ({
required = false, required = false,
}) => ( }) => (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-gray-900 text-base">{title}</div> <div className="text-bold">{title}</div>
<Controller <Controller
name={id} name={id}
control={control} control={control}
@ -30,22 +30,22 @@ const ToggleSwitch: React.FC<Props> = ({
checked={value} checked={value}
onChange={onChange} onChange={onChange}
className={classNames( className={classNames(
value ? "bg-indigo-600" : "bg-gray-200", value ? 'bg-accent' : '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" '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 className="sr-only">Use setting</span>
<span <span
aria-hidden="true" aria-hidden="true"
className={classNames( className={classNames(
value ? "translate-x-5" : "translate-x-0", 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" 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out'
)} )}
/> />
</Switch> </Switch>
)} )}
/> />
</div> </div>
); )
export default ToggleSwitch; export default ToggleSwitch

View File

@ -3,9 +3,11 @@ import { useSetAtom } from 'jotai'
import { import {
setMainViewStateAtom, setMainViewStateAtom,
MainViewState, MainViewState,
} from '@/_helpers/atoms/MainView.atom' } from '@helpers/atoms/MainView.atom'
import SecondaryButton from '../SecondaryButton' import SecondaryButton from '../SecondaryButton'
import { Button, Icons } from '@uikit'
const Welcome: React.FC = () => { const Welcome: React.FC = () => {
const setMainViewState = useSetAtom(setMainViewStateAtom) const setMainViewState = useSetAtom(setMainViewStateAtom)
@ -18,6 +20,8 @@ const Welcome: React.FC = () => {
<br /> <br />
lets download your first model lets download your first model
</span> </span>
<Button>Button component</Button>
<Icons name="panel-left" />
<SecondaryButton <SecondaryButton
title={'Explore models'} title={'Explore models'}
onClick={() => setMainViewState(MainViewState.ExploreModel)} onClick={() => setMainViewState(MainViewState.ExploreModel)}

View File

@ -1,23 +0,0 @@
'use client'
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
}
export const ModalWrapper: React.FC<Props> = ({ children }) => (
<>
<MobileMenuPane />
<ConfirmDeleteConversationModal />
<ConfirmSignOutModal />
<ConfirmDeleteModelModal />
<BotListModal />
{children}
</>
)

View File

@ -1,15 +0,0 @@
'use client'
import { ThemeProvider } from 'next-themes'
import { ReactNode } from 'react'
type Props = {
children: ReactNode
}
// consider to use next-themes or not. This caused the error Warning: Extra attributes from the server: class,style at html after hydration
export const ThemeWrapper: React.FC<Props> = ({ children }) => (
<ThemeProvider enableSystem={false} attribute="class">
{children}
</ThemeProvider>
)

View File

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

View File

@ -1,22 +1,9 @@
import { Bot } from '@/_models/Bot'
import { executeSerial } from '../../../electron/core/plugin-manager/execution/extension-manager' 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() { export default function useCreateBot() {
const setActiveBot = useSetAtom(activeBotAtom)
const setMainViewState = useSetAtom(setMainViewStateAtom)
const createBot = async (bot: Bot) => { const createBot = async (bot: Bot) => {
try { try {
await executeSerial(DataService.CreateBot, bot) await executeSerial('createBot', bot)
setActiveBot(bot)
setMainViewState(MainViewState.BotInfo)
} catch (err) { } catch (err) {
alert(err) alert(err)
console.error(err) console.error(err)

View File

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

View File

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

View File

@ -1,6 +1,4 @@
import { Bot } from "@/_models/Bot"; import { executeSerial } from '../../../electron/core/plugin-manager/execution/extension-manager'
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "@janhq/core";
export default function useUpdateBot() { export default function useUpdateBot() {
const updateBot = async ( const updateBot = async (
@ -13,26 +11,26 @@ export default function useUpdateBot() {
for (const [key, value] of Object.entries(updatableField)) { for (const [key, value] of Object.entries(updatableField)) {
if (value !== undefined) { if (value !== undefined) {
//@ts-ignore //@ts-ignore
bot[key] = value; bot[key] = value
} }
} }
await executeSerial(DataService.UpdateBot, bot); await executeSerial('updateBot', bot)
console.debug("Bot updated", JSON.stringify(bot, null, 2)); console.debug('Bot updated', JSON.stringify(bot, null, 2))
} catch (err) { } catch (err) {
alert(`Update bot error: ${err}`); alert(`Update bot error: ${err}`)
console.error(err); console.error(err)
return; return
} }
}; }
return { updateBot }; return { updateBot }
} }
export type UpdatableField = { export type UpdatableField = {
presencePenalty?: number; presencePenalty?: number
frequencyPenalty?: number; frequencyPenalty?: number
maxTokens?: number; maxTokens?: number
customTemperature?: number; customTemperature?: number
systemPrompt?: number; systemPrompt?: number
}; }

View File

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

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