Merge branch 'main' into jan-182-drake

This commit is contained in:
0xSage 2023-10-03 11:14:09 +08:00 committed by GitHub
commit 67cd44353e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2011 additions and 861 deletions

View File

@ -168,7 +168,7 @@ function getFinishedDownloadModels() {
const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`; const query = `SELECT * FROM models WHERE finish_download_at != -1 ORDER BY finish_download_at DESC`;
db.all(query, (err: Error, row: any) => { db.all(query, (err: Error, row: any) => {
res(row); res(row.map((item: any) => parseToProduct(item)));
}); });
db.close(); db.close();
}); });
@ -184,6 +184,7 @@ function deleteDownloadModel(modelId: string) {
const stmt = db.prepare("DELETE FROM models WHERE id = ?"); const stmt = db.prepare("DELETE FROM models WHERE id = ?");
stmt.run(modelId); stmt.run(modelId);
stmt.finalize(); stmt.finalize();
res(modelId);
}); });
db.close(); db.close();
@ -352,7 +353,7 @@ function deleteConversation(id: any) {
); );
deleteMessages.run(id); deleteMessages.run(id);
deleteMessages.finalize(); deleteMessages.finalize();
res([]); res(id);
}); });
db.close(); db.close();
@ -373,6 +374,31 @@ function getConversationMessages(conversation_id: any) {
}); });
} }
function parseToProduct(row: any) {
const product = {
id: row.id,
slug: row.slug,
name: row.name,
description: row.description,
avatarUrl: row.avatar_url,
longDescription: row.long_description,
technicalDescription: row.technical_description,
author: row.author,
version: row.version,
modelUrl: row.model_url,
nsfw: row.nsfw,
greeting: row.greeting,
type: row.type,
inputs: row.inputs,
outputs: row.outputs,
createdAt: new Date(row.created_at),
updatedAt: new Date(row.updated_at),
fileName: row.file_name,
downloadUrl: row.download_url,
};
return product;
}
module.exports = { module.exports = {
init, init,
getConversations, getConversations,

View File

@ -2,18 +2,9 @@ const path = require("path");
const { app, dialog } = require("electron"); const { app, dialog } = require("electron");
const { spawn } = require("child_process"); const { spawn } = require("child_process");
const fs = require("fs"); const fs = require("fs");
var exec = require("child_process").exec;
let subprocess = null; let subprocess = null;
process.on("exit", () => {
// Perform cleanup tasks here
console.log("kill subprocess on exit");
if (subprocess) {
subprocess.kill();
}
});
async function initModel(product) { async function initModel(product) {
// fileName fallback // fileName fallback
if (!product.fileName) { if (!product.fileName) {
@ -63,7 +54,7 @@ async function initModel(product) {
: path.join(binaryFolder, "nitro"); : path.join(binaryFolder, "nitro");
// Execute the binary // Execute the binary
subprocess = spawn(binaryPath, [configFilePath], {cwd: binaryFolder}); subprocess = spawn(binaryPath, [configFilePath], { cwd: binaryFolder });
// Handle subprocess output // Handle subprocess output
subprocess.stdout.on("data", (data) => { subprocess.stdout.on("data", (data) => {
@ -80,7 +71,7 @@ async function initModel(product) {
}); });
} }
function killSubprocess() { function dispose() {
if (subprocess) { if (subprocess) {
subprocess.kill(); subprocess.kill();
subprocess = null; subprocess = null;
@ -92,5 +83,5 @@ function killSubprocess() {
module.exports = { module.exports = {
initModel, initModel,
killSubprocess, dispose,
}; };

View File

@ -1,30 +1,42 @@
import { import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
app,
BrowserWindow,
screen as electronScreen,
ipcMain,
dialog,
shell,
} from "electron";
import { readdirSync } from "fs"; import { readdirSync } 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";
import isDev = require("electron-is-dev");
import { init } from "./core/plugin-manager/pluginMgr"; import { init } from "./core/plugin-manager/pluginMgr";
import { setupMenu } from "./utils/menu";
import { dispose } from "./utils/disposable";
const isDev = require("electron-is-dev");
const request = require("request");
const progress = require("request-progress");
const { autoUpdater } = require("electron-updater"); const { autoUpdater } = require("electron-updater");
const Store = require("electron-store"); const Store = require("electron-store");
// @ts-ignore
import request = require("request");
// @ts-ignore
import progress = require("request-progress");
const requiredModules: Record<string, any> = {};
let mainWindow: BrowserWindow | undefined = undefined; let mainWindow: BrowserWindow | undefined = undefined;
const store = new Store();
autoUpdater.autoDownload = false; app
autoUpdater.autoInstallOnAppQuit = true; .whenReady()
.then(migratePlugins)
.then(setupPlugins)
.then(setupMenu)
.then(handleIPCs)
.then(handleAppUpdates)
.then(createMainWindow)
.then(() => {
app.on("activate", () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow();
}
});
});
const createMainWindow = () => { app.on("window-all-closed", () => {
dispose(requiredModules);
app.quit();
});
function createMainWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
height: 800, height: 800,
@ -37,26 +49,6 @@ const createMainWindow = () => {
}, },
}); });
ipcMain.handle(
"invokePluginFunc",
async (_event, modulePath, method, ...args) => {
const module = join(app.getPath("userData"), "plugins", modulePath);
return await import(/* webpackIgnore: true */ module)
.then((plugin) => {
if (typeof plugin[method] === "function") {
return plugin[method](...args);
} else {
console.log(plugin[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
})
.then((res) => {
return res;
})
.catch((err) => console.log(err));
}
);
const startURL = isDev const startURL = isDev
? "http://localhost:3000" ? "http://localhost:3000"
: `file://${join(__dirname, "../renderer/index.html")}`; : `file://${join(__dirname, "../renderer/index.html")}`;
@ -69,17 +61,69 @@ const createMainWindow = () => {
}); });
if (isDev) mainWindow.webContents.openDevTools(); if (isDev) mainWindow.webContents.openDevTools();
}; }
app function handleAppUpdates() {
.whenReady() /*New Update Available*/
.then(migratePlugins) autoUpdater.on("update-available", async (_info: any) => {
.then(() => { const action = await dialog.showMessageBox({
createMainWindow(); message: `Update available. Do you want to download the latest update?`,
setupPlugins(); buttons: ["Download", "Later"],
});
if (action.response === 0) await autoUpdater.downloadUpdate();
});
/*App Update Completion Message*/
autoUpdater.on("update-downloaded", async (_info: any) => {
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
const action = await dialog.showMessageBox({
message: `Update downloaded. Please restart the application to apply the updates.`,
buttons: ["Restart", "Later"],
});
if (action.response === 0) {
autoUpdater.quitAndInstall();
}
});
/*App Update Error */
autoUpdater.on("error", (info: any) => {
dialog.showMessageBox({ message: info.message });
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
});
/*App Update Progress */
autoUpdater.on("download-progress", (progress: any) => {
console.log("app update progress: ", progress.percent);
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
percent: progress.percent,
});
});
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.checkForUpdates(); autoUpdater.checkForUpdates();
}
ipcMain.handle("basePlugins", async (event) => { function handleIPCs() {
ipcMain.handle(
"invokePluginFunc",
async (_event, modulePath, method, ...args) => {
const module = require(/* webpackIgnore: true */ join(
app.getPath("userData"),
"plugins",
modulePath
));
requiredModules[modulePath] = module;
if (typeof module[method] === "function") {
return module[method](...args);
} else {
console.log(module[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
}
);
ipcMain.handle("basePlugins", async (_event) => {
const basePluginPath = join( const basePluginPath = join(
__dirname, __dirname,
"../", "../",
@ -90,13 +134,13 @@ app
.map((file) => join(basePluginPath, file)); .map((file) => join(basePluginPath, file));
}); });
ipcMain.handle("pluginPath", async (event) => { ipcMain.handle("pluginPath", async (_event) => {
return join(app.getPath("userData"), "plugins"); return join(app.getPath("userData"), "plugins");
}); });
ipcMain.handle("appVersion", async (event) => { ipcMain.handle("appVersion", async (_event) => {
return app.getVersion(); return app.getVersion();
}); });
ipcMain.handle("openExternalUrl", async (event, url) => { ipcMain.handle("openExternalUrl", async (_event, url) => {
shell.openExternal(url); shell.openExternal(url);
}); });
@ -116,9 +160,7 @@ app
} else { } else {
result = "File deleted successfully"; result = "File deleted successfully";
} }
console.log( console.log(`Delete file ${filePath} from ${fullPath} result: ${result}`);
`Delete file ${filePath} from ${fullPath} result: ${result}`
);
}); });
return result; return result;
@ -151,57 +193,11 @@ app
}) })
.pipe(createWriteStream(destination)); .pipe(createWriteStream(destination));
}); });
}
app.on("activate", () => {
if (!BrowserWindow.getAllWindows().length) {
createMainWindow();
}
});
});
/*New Update Available*/
autoUpdater.on("update-available", async (info: any) => {
const action = await dialog.showMessageBox({
message: `Update available. Do you want to download the latest update?`,
buttons: ["Download", "Later"],
});
if (action.response === 0) await autoUpdater.downloadUpdate();
});
/*App Update Completion Message*/
autoUpdater.on("update-downloaded", async (info: any) => {
mainWindow?.webContents.send("APP_UPDATE_COMPLETE", {});
const action = await dialog.showMessageBox({
message: `Update downloaded. Please restart the application to apply the updates.`,
buttons: ["Restart", "Later"],
});
if (action.response === 0) {
autoUpdater.quitAndInstall();
}
});
/*App Update Error */
autoUpdater.on("error", (info: any) => {
dialog.showMessageBox({ message: info.message });
mainWindow?.webContents.send("APP_UPDATE_ERROR", {});
});
/*App Update Progress */
autoUpdater.on("download-progress", (progress: any) => {
console.log("app update progress: ", progress.percent);
mainWindow?.webContents.send("APP_UPDATE_PROGRESS", {
percent: progress.percent,
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
function migratePlugins() { function migratePlugins() {
return new Promise((resolve) => { return new Promise((resolve) => {
const store = new Store();
if (store.get("migrated_version") !== app.getVersion()) { if (store.get("migrated_version") !== app.getVersion()) {
console.log("start migration:", store.get("migrated_version")); console.log("start migration:", store.get("migrated_version"));
const userDataPath = app.getPath("userData"); const userDataPath = app.getPath("userData");
@ -217,12 +213,12 @@ function migratePlugins() {
resolve(undefined); resolve(undefined);
} }
}); });
}; }
function setupPlugins() { function setupPlugins() {
init({ init({
// Function to check from the main process that user wants to install a plugin // Function to check from the main process that user wants to install a plugin
confirmInstall: async (plugins: string[]) => { confirmInstall: async (_plugins: string[]) => {
return true; return true;
}, },
// Path to install plugin to // Path to install plugin to

View File

@ -11,7 +11,7 @@
"files": [ "files": [
"renderer/**/*", "renderer/**/*",
"build/*.{js,map}", "build/*.{js,map}",
"build/core/plugin-manager/**/*", "build/**/*.{js,map}",
"core/pre-install" "core/pre-install"
], ],
"asarUnpack": [ "asarUnpack": [

View File

@ -0,0 +1,8 @@
export function dispose(requiredModules: Record<string, any>) {
for (const key in requiredModules) {
const module = requiredModules[key];
if (typeof module["dispose"] === "function") {
module["dispose"]();
}
}
}

111
electron/utils/menu.ts Normal file
View File

@ -0,0 +1,111 @@
// @ts-nocheck
const { app, Menu, dialog } = require("electron");
const isMac = process.platform === "darwin";
const { autoUpdater } = require("electron-updater");
import { compareSemanticVersions } from "./versionDiff";
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
{
label: "Check for Updates...",
click: () =>
autoUpdater.checkForUpdatesAndNotify().then((e) => {
if (
!e ||
compareSemanticVersions(
app.getVersion(),
e.updateInfo.version
) >= 0
)
dialog.showMessageBox({
message: `There are currently no updates available.`,
});
}),
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
]
: []),
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
...(isMac
? [
{ type: "separator" },
{ role: "front" },
{ type: "separator" },
{ role: "window" },
]
: [{ role: "close" }]),
],
},
{
role: "help",
submenu: [
{
label: "Learn More",
click: async () => {
const { shell } = require("electron");
await shell.openExternal("https://jan.ai/");
},
},
],
},
];
export const setupMenu = () => {
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
};

View File

@ -0,0 +1,21 @@
export const compareSemanticVersions = (a: string, b: string) => {
// 1. Split the strings into their parts.
const a1 = a.split('.');
const b1 = b.split('.');
// 2. Contingency in case there's a 4th or 5th version
const len = Math.min(a1.length, b1.length);
// 3. Look through each version number and compare.
for (let i = 0; i < len; i++) {
const a2 = +a1[ i ] || 0;
const b2 = +b1[ i ] || 0;
if (a2 !== b2) {
return a2 > b2 ? 1 : -1;
}
}
// 4. We hit this if the all checked versions so far are equal
//
return b1.length - a1.length;
};

View File

@ -0,0 +1,19 @@
import { useAtomValue } from "jotai";
import React, { Fragment } from "react";
import ModelTable from "../ModelTable";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ActiveModelTable: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom);
if (!activeModel) return null;
return (
<Fragment>
<h3 className="text-xl leading-[25px] mb-[13px]">Active Model(s)</h3>
<ModelTable models={[activeModel]} />
</Fragment>
);
};
export default ActiveModelTable;

View File

@ -2,9 +2,8 @@ import { Product } from "@/_models/Product";
import DownloadModelContent from "../DownloadModelContent"; import DownloadModelContent from "../DownloadModelContent";
import ModelDownloadButton from "../ModelDownloadButton"; import ModelDownloadButton from "../ModelDownloadButton";
import ModelDownloadingButton from "../ModelDownloadingButton"; import ModelDownloadingButton from "../ModelDownloadingButton";
import ViewModelDetailButton from "../ViewModelDetailButton";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/JotaiWrapper"; import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
type Props = { type Props = {
product: Product; product: Product;
@ -36,8 +35,6 @@ const AvailableModelCard: React.FC<Props> = ({
} }
} }
const handleViewDetails = () => {};
const downloadButton = isDownloading ? ( const downloadButton = isDownloading ? (
<div className="w-1/5 flex items-start justify-end"> <div className="w-1/5 flex items-start justify-end">
<ModelDownloadingButton total={total} value={transferred} /> <ModelDownloadingButton total={total} value={transferred} />

View File

@ -1,9 +1,9 @@
"use client"; "use client";
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import SecondaryButton from "../SecondaryButton"; import SecondaryButton from "../SecondaryButton";
import SendButton from "../SendButton"; import SendButton from "../SendButton";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
const BasicPromptAccessories: React.FC = () => { const BasicPromptAccessories: React.FC = () => {
const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom); const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom);

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/JotaiWrapper"; 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,22 +1,45 @@
"use client"; "use client";
import { currentPromptAtom } from "@/_helpers/JotaiWrapper"; import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel";
import useSendChatMessage from "@/_hooks/useSendChatMessage"; import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { useAtom } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { ChangeEvent } from "react";
const BasicPromptInput: React.FC = () => { const BasicPromptInput: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom);
const selectedModel = useAtomValue(selectedModelAtom);
const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom); const [currentPrompt, setCurrentPrompt] = useAtom(currentPromptAtom);
const { sendChatMessage } = useSendChatMessage(); const { sendChatMessage } = useSendChatMessage();
const { requestCreateConvo } = useCreateConversation();
const handleMessageChange = (event: any) => { const { initModel } = useInitModel();
const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setCurrentPrompt(event.target.value); setCurrentPrompt(event.target.value);
}; };
const handleKeyDown = (event: any) => { const handleKeyDown = async (
event: React.KeyboardEvent<HTMLTextAreaElement>
) => {
if (event.key === "Enter") { if (event.key === "Enter") {
if (!event.shiftKey) { if (!event.shiftKey) {
if (activeConversationId) {
event.preventDefault(); event.preventDefault();
sendChatMessage(); sendChatMessage();
} else {
if (!selectedModel) {
console.log("No model selected");
return;
}
await requestCreateConvo(selectedModel);
await initModel(selectedModel);
sendChatMessage();
}
} }
} }
}; };

View File

@ -4,14 +4,12 @@ import React, { useCallback, useRef, useState } from "react";
import ChatItem from "../ChatItem"; import ChatItem from "../ChatItem";
import { ChatMessage } from "@/_models/ChatMessage"; import { ChatMessage } from "@/_models/ChatMessage";
import useChatMessages from "@/_hooks/useChatMessages"; import useChatMessages from "@/_hooks/useChatMessages";
import { import { showingTyping } from "@/_helpers/JotaiWrapper";
chatMessages,
getActiveConvoIdAtom,
showingTyping,
} from "@/_helpers/JotaiWrapper";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils"; import { selectAtom } from "jotai/utils";
import LoadingIndicator from "../LoadingIndicator"; import LoadingIndicator from "../LoadingIndicator";
import { getActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { chatMessages } from "@/_helpers/atoms/ChatMessage.atom";
const ChatBody: React.FC = () => { const ChatBody: React.FC = () => {
const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? ""; const activeConversationId = useAtomValue(getActiveConvoIdAtom) ?? "";

View File

@ -4,7 +4,7 @@ import SimpleTextMessage from "../SimpleTextMessage";
import { ChatMessage, MessageType } from "@/_models/ChatMessage"; import { ChatMessage, MessageType } from "@/_models/ChatMessage";
import StreamTextMessage from "../StreamTextMessage"; import StreamTextMessage from "../StreamTextMessage";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper"; import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
export default function renderChatMessage({ export default function renderChatMessage({
id, id,

View File

@ -1,11 +1,16 @@
"use client"; "use client";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { MainViewState, getMainViewStateAtom } from "@/_helpers/JotaiWrapper";
import { ReactNode } from "react"; import { ReactNode } from "react";
import ModelManagement from "../ModelManagement";
import Welcome from "../WelcomeContainer"; import Welcome from "../WelcomeContainer";
import { Preferences } from "../Preferences"; import { Preferences } from "../Preferences";
import MyModelContainer from "../MyModelContainer";
import ExploreModelContainer from "../ExploreModelContainer";
import {
MainViewState,
getMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import EmptyChatContainer from "../EmptyChatContainer";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
@ -15,12 +20,15 @@ export default function ChatContainer({ children }: Props) {
const viewState = useAtomValue(getMainViewStateAtom); const viewState = useAtomValue(getMainViewStateAtom);
switch (viewState) { switch (viewState) {
case MainViewState.ConversationEmptyModel:
return <EmptyChatContainer />
case MainViewState.ExploreModel: case MainViewState.ExploreModel:
return <ModelManagement />; return <ExploreModelContainer />;
case MainViewState.Setting: case MainViewState.Setting:
return <Preferences />; return <Preferences />;
case MainViewState.ResourceMonitor: case MainViewState.ResourceMonitor:
case MainViewState.MyModel: case MainViewState.MyModel:
return <MyModelContainer />;
case MainViewState.Welcome: case MainViewState.Welcome:
return <Welcome />; return <Welcome />;
default: default:

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import JanImage from "../JanImage"; import JanImage from "../JanImage";
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
const CompactLogo: React.FC = () => { const CompactLogo: React.FC = () => {
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);

View File

@ -1,4 +1,4 @@
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/JotaiWrapper"; 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";

View File

@ -1,17 +1,13 @@
import React, { Fragment } from "react"; 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 { showConfirmDeleteModalAtom } from "@/_helpers/JotaiWrapper";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import useSignOut from "@/_hooks/useSignOut"; 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);
const { signOut } = useSignOut();
const onLogOutClick = () => { const onConfirmDelete = () => {};
signOut().then(() => setShow(false));
};
return ( return (
<Transition.Root show={show} as={Fragment}> <Transition.Root show={show} as={Fragment}>
@ -65,7 +61,7 @@ const ConfirmDeleteModelModal: React.FC = () => {
<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-sm font-semibold text-white shadow-sm hover:bg-red-500 sm:ml-3 sm:w-auto"
onClick={onLogOutClick} onClick={onConfirmDelete}
> >
Log out Log out
</button> </button>

View File

@ -1,9 +1,9 @@
import React, { Fragment } from "react"; 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 { showConfirmSignOutModalAtom } from "@/_helpers/JotaiWrapper";
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";
const ConfirmSignOutModal: React.FC = () => { const ConfirmSignOutModal: React.FC = () => {
const [show, setShow] = useAtom(showConfirmSignOutModalAtom); const [show, setShow] = useAtom(showConfirmSignOutModalAtom);

View File

@ -1,8 +1,5 @@
import { Product } from "@/_models/Product"; import { Product } from "@/_models/Product";
import DownloadModelContent from "../DownloadModelContent"; import DownloadModelContent from "../DownloadModelContent";
import ViewModelDetailButton from "../ViewModelDetailButton";
import { executeSerial } from "@/_services/pluginService";
import { InfereceService } from "../../../shared/coreService";
type Props = { type Props = {
product: Product; product: Product;
@ -17,11 +14,7 @@ const DownloadedModelCard: React.FC<Props> = ({
isRecommend, isRecommend,
required, required,
onDeleteClick, onDeleteClick,
}) => { }) => (
const handleViewDetails = () => {};
return (
<div className="border rounded-lg border-gray-200"> <div className="border rounded-lg border-gray-200">
<div className="flex justify-between py-4 px-3 gap-[10px]"> <div className="flex justify-between py-4 px-3 gap-[10px]">
<DownloadModelContent <DownloadModelContent
@ -36,9 +29,7 @@ const DownloadedModelCard: React.FC<Props> = ({
<button onClick={() => onDeleteClick?.(product)}>Delete</button> <button onClick={() => onDeleteClick?.(product)}>Delete</button>
</div> </div>
</div> </div>
{/* <ViewModelDetailButton callback={handleViewDetails} /> */}
</div> </div>
); );
};
export default DownloadedModelCard; export default DownloadedModelCard;

View File

@ -0,0 +1,20 @@
import React, { Fragment } from "react";
import SearchBar from "../SearchBar";
import ModelTable from "../ModelTable";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
const DownloadedModelTable: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
return (
<Fragment>
<h3 className="text-xl leading-[25px] mt-[50px]">Downloaded Models</h3>
<div className="py-5 w-[568px]">
<SearchBar />
</div>
<ModelTable models={downloadedModels} />
</Fragment>
);
};
export default DownloadedModelTable;

View File

@ -0,0 +1,14 @@
import React from "react";
import SelectModels from "../ModelSelector";
import InputToolbar from "../InputToolbar";
const EmptyChatContainer: React.FC = () => (
<div className="flex flex-col flex-1">
<div className="flex flex-1 items-center justify-center">
<SelectModels />
</div>
<InputToolbar />
</div>
);
export default EmptyChatContainer;

View File

@ -0,0 +1,55 @@
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
import ExploreModelItem from "../ExploreModelItem";
import HeaderTitle from "../HeaderTitle";
import SearchBar from "../SearchBar";
import SimpleCheckbox from "../SimpleCheckbox";
import SimpleTag, { TagType } from "../SimpleTag";
const tags = [
"Roleplay",
"Llama",
"Story",
"Casual",
"Professional",
"CodeLlama",
"Coding",
];
const checkboxs = ["GGUF", "TensorRT", "Meow", "JigglyPuff"];
const ExploreModelContainer: React.FC = () => {
const { allAvailableModels } = useGetAvailableModels();
return (
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
<HeaderTitle title="Explore Models" />
<SearchBar placeholder="Search or HuggingFace URL" />
<div className="flex gap-x-14 mt-[38px]">
<div className="flex-1 flex-shrink-0">
<h2 className="font-semibold text-xs mb-[15px]">Tags</h2>
<SearchBar placeholder="Filter by tags" />
<div className="flex flex-wrap gap-[9px] mt-[14px]">
{tags.map((item) => (
<SimpleTag key={item} title={item} type={item as TagType} />
))}
</div>
<hr className="my-10" />
<fieldset>
{checkboxs.map((item) => (
<SimpleCheckbox key={item} name={item} />
))}
</fieldset>
</div>
<div className="flex-[3_3_0%]">
<h2 className="font-semibold text-xs mb-[18px]">Results</h2>
<div className="flex flex-col gap-[31px]">
{allAvailableModels.map((item) => (
<ExploreModelItem key={item.id} model={item} />
))}
</div>
</div>
</div>
</div>
);
};
export default ExploreModelContainer;

View File

@ -0,0 +1,101 @@
"use client";
import ExploreModelItemHeader from "../ExploreModelItemHeader";
import ModelVersionList from "../ModelVersionList";
import { useMemo, useState } from "react";
import { Product } from "@/_models/Product";
import SimpleTag, { TagType } from "../SimpleTag";
import { displayDate } from "@/_utils/datetime";
import useDownloadModel from "@/_hooks/useDownloadModel";
import { atom, useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
type Props = {
model: Product;
};
const ExploreModelItem: React.FC<Props> = ({ model }) => {
const downloadAtom = useMemo(
() => atom((get) => get(modelDownloadStateAtom)[model.fileName ?? ""]),
[model.fileName ?? ""]
);
const downloadState = useAtomValue(downloadAtom);
const { downloadModel } = useDownloadModel();
const [show, setShow] = useState(false);
return (
<div className="flex flex-col border border-gray-200 rounded-[5px]">
<ExploreModelItemHeader
name={model.name}
status={TagType.Recommended}
total={model.totalSize}
downloadState={downloadState}
onDownloadClick={() => downloadModel(model)}
/>
<div className="flex flex-col px-[26px] py-[22px]">
<div className="flex justify-between">
<div className="flex-1 flex flex-col gap-8">
<div className="flex flex-col gap-1">
<div className="text-sm font-medium text-gray-500">
Model Format
</div>
<div className="px-[10px] py-0.5 bg-gray-100 text-xs text-gray-800 w-fit">
GGUF
</div>
</div>
<div className="flex flex-col">
<div className="text-sm font-medium text-gray-500">
Hardware Compatibility
</div>
<div className="flex gap-2">
<SimpleTag
clickable={false}
title={TagType.Compatible}
type={TagType.Compatible}
/>
</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-8">
<div>
<div className="text-sm font-medium text-gray-500">
Release Date
</div>
<div className="text-sm font-normal text-gray-900">
{displayDate(model.releaseDate)}
</div>
</div>
<div className="flex flex-col gap-2">
<div className="text-sm font-medium text-gray-500">
Expected Performance
</div>
<SimpleTag
title={TagType.Medium}
type={TagType.Medium}
clickable={false}
/>
</div>
</div>
</div>
<div className="flex flex-col gap-1 mt-[26px]">
<span className="text-sm font-medium text-gray-500">About</span>
<span className="text-sm font-normal text-gray-500">
{model.longDescription}
</span>
</div>
<div className="flex flex-col">
<span className="text-sm font-medium text-gray-500">Tags</span>
</div>
</div>
{show && <ModelVersionList />}
<button
onClick={() => setShow(!show)}
className="bg-[#FBFBFB] text-gray-500 text-sm text-left py-2 px-4 border-t border-gray-200"
>
{!show ? "+ Show Available Versions" : "- Collapse"}
</button>
</div>
);
};
export default ExploreModelItem;

View File

@ -0,0 +1,44 @@
import SimpleTag, { TagType } from "../SimpleTag";
import PrimaryButton from "../PrimaryButton";
import { formatDownloadPercentage, toGigabytes } from "@/_utils/converter";
import { DownloadState } from "@/_models/DownloadState";
import SecondaryButton from "../SecondaryButton";
type Props = {
name: string;
total: number;
status: TagType;
downloadState?: DownloadState;
onDownloadClick?: () => void;
};
const ExploreModelItemHeader: React.FC<Props> = ({
name,
status,
total,
downloadState,
onDownloadClick,
}) => (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<div className="flex items-center gap-2">
<span>{name}</span>
<SimpleTag title={status} type={status} clickable={false} />
</div>
{downloadState != null ? (
<SecondaryButton
disabled
title={`Downloading (${formatDownloadPercentage(
downloadState.percent
)})`}
onClick={() => {}}
/>
) : (
<PrimaryButton
title={total ? `Download (${toGigabytes(total)})` : "Download"}
onClick={() => onDownloadClick?.()}
/>
)}
</div>
);
export default ExploreModelItemHeader;

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper"; 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,22 +1,20 @@
import React from "react"; import React from "react";
import JanImage from "../JanImage"; import JanImage from "../JanImage";
import {
MainViewState,
activeModel,
conversationStatesAtom,
currentProductAtom,
getActiveConvoIdAtom,
setActiveConvoIdAtom,
setMainViewStateAtom,
} from "@/_helpers/JotaiWrapper";
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 { Conversation } from "@/_models/Conversation";
import { DataService, InfereceService } from "../../../shared/coreService"; import { DataService } from "../../../shared/coreService";
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
import { import {
execute, conversationStatesAtom,
executeSerial, getActiveConvoIdAtom,
} from "../../../../electron/core/plugin-manager/execution/extension-manager"; setActiveConvoIdAtom,
} from "@/_helpers/atoms/Conversation.atom";
import {
setMainViewStateAtom,
MainViewState,
} from "@/_helpers/atoms/MainView.atom";
import useInitModel from "@/_hooks/useInitModel";
type Props = { type Props = {
conversation: Conversation; conversation: Conversation;
@ -36,23 +34,20 @@ const HistoryItem: React.FC<Props> = ({
const activeConvoId = useAtomValue(getActiveConvoIdAtom); const activeConvoId = useAtomValue(getActiveConvoIdAtom);
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
const isSelected = activeConvoId === conversation.id; const isSelected = activeConvoId === conversation.id;
const setActiveModel = useSetAtom(activeModel);
const setActiveProduct = useSetAtom(currentProductAtom); const { initModel } = useInitModel();
const onClick = async () => { const onClick = async () => {
const convoModel = await executeSerial( const model = await executeSerial(
DataService.GET_MODEL_BY_ID, DataService.GET_MODEL_BY_ID,
conversation.model_id conversation.model_id
); );
if (!convoModel) { if (!model) {
alert( alert(
`Model ${conversation.model_id} not found! Please re-download the model first.` `Model ${conversation.model_id} not found! Please re-download the model first.`
); );
} else { } else {
setActiveProduct(convoModel) initModel(model);
executeSerial(InfereceService.INIT_MODEL, convoModel)
.then(() => console.info(`Init model success`))
.catch((err) => console.log(`Init model error ${err}`));
setActiveModel(convoModel.name);
} }
if (activeConvoId !== conversation.id) { if (activeConvoId !== conversation.id) {
setMainViewState(MainViewState.Conversation); setMainViewState(MainViewState.Conversation);

View File

@ -2,9 +2,10 @@ 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, userConversationsAtom } 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";
const HistoryList: React.FC = () => { const HistoryList: React.FC = () => {
const conversations = useAtomValue(userConversationsAtom); const conversations = useAtomValue(userConversationsAtom);

View File

@ -2,8 +2,8 @@
import BasicPromptInput from "../BasicPromptInput"; import BasicPromptInput from "../BasicPromptInput";
import BasicPromptAccessories from "../BasicPromptAccessories"; import BasicPromptAccessories from "../BasicPromptAccessories";
import { showingAdvancedPromptAtom } from "@/_helpers/JotaiWrapper";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { showingAdvancedPromptAtom } from "@/_helpers/atoms/Modal.atom";
const InputToolbar: React.FC = () => { const InputToolbar: React.FC = () => {
const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom); const showingAdvancedPrompt = useAtomValue(showingAdvancedPromptAtom);

View File

@ -1,4 +1,4 @@
import { setActiveConvoIdAtom } from "@/_helpers/JotaiWrapper"; import { setActiveConvoIdAtom } from "@/_helpers/atoms/Conversation.atom";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import Image from "next/image"; import Image from "next/image";
import React from "react"; import React from "react";

View File

@ -3,10 +3,12 @@ import SidebarFooter from "../SidebarFooter";
import SidebarHeader from "../SidebarHeader"; import SidebarHeader from "../SidebarHeader";
import SidebarMenu from "../SidebarMenu"; import SidebarMenu from "../SidebarMenu";
import HistoryList from "../HistoryList"; import HistoryList from "../HistoryList";
import NewChatButton from "../NewChatButton";
const LeftContainer: React.FC = () => ( const LeftContainer: React.FC = () => (
<div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col"> <div className="w-[323px] flex-shrink-0 p-3 h-screen border-r border-gray-200 flex flex-col">
<SidebarHeader /> <SidebarHeader />
<NewChatButton />
<HistoryList /> <HistoryList />
<SidebarMenu /> <SidebarMenu />
<SidebarFooter /> <SidebarFooter />

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/JotaiWrapper"; 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

@ -2,8 +2,8 @@ import React, { useRef } from "react";
import { Dialog } from "@headlessui/react"; 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 { showingMobilePaneAtom } from "@/_helpers/JotaiWrapper";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
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

@ -0,0 +1,46 @@
import React from "react";
import PrimaryButton from "../PrimaryButton";
export enum ModelActionType {
Start = "Start",
Stop = "Stop",
}
type ModelActionStyle = {
title: string;
backgroundColor: string;
textColor: string;
};
const modelActionMapper: Record<ModelActionType, ModelActionStyle> = {
[ModelActionType.Start]: {
title: "Start",
backgroundColor: "bg-blue-500 hover:bg-blue-600",
textColor: "text-white",
},
[ModelActionType.Stop]: {
title: "Stop",
backgroundColor: "bg-red-500 hover:bg-red-600",
textColor: "text-white",
},
};
type Props = {
type: ModelActionType;
onActionClick: (type: ModelActionType) => void;
};
const ModelActionButton: React.FC<Props> = ({ type, onActionClick }) => {
const styles = modelActionMapper[type];
const onClick = () => {
onActionClick(type);
};
return (
<td className="whitespace-nowrap px-6 py-4 text-sm">
<PrimaryButton title={styles.title} onClick={onClick} />
</td>
);
};
export default ModelActionButton;

View File

@ -0,0 +1,35 @@
import { Menu, Transition } from "@headlessui/react";
import { EllipsisVerticalIcon } from "@heroicons/react/20/solid";
import { Fragment } from "react";
type Props = {
onDeleteClick: () => void;
};
const ModelActionMenu: React.FC<Props> = ({ onDeleteClick }) => {
return (
<Menu as="div" className="relative flex-none">
<Menu.Button className="block text-gray-500 hover:text-gray-900">
<span className="sr-only">Open options</span>
<EllipsisVerticalIcon className="h-5 w-5" aria-hidden="true" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-50 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
<Menu.Item>
<button onClick={onDeleteClick}>Delete</button>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
};
export default ModelActionMenu;

View File

@ -1,3 +1,5 @@
import { toGigabytes } from "@/_utils/converter";
type Props = { type Props = {
total: number; total: number;
value: number; value: number;
@ -18,16 +20,4 @@ const ModelDownloadingButton: React.FC<Props> = ({ total, value }) => {
); );
}; };
const toGigabytes = (input: number) => {
if (input > 1024 ** 3) {
return (input / 1000 ** 3).toFixed(2) + "GB";
} else if (input > 1024 ** 2) {
return (input / 1000 ** 2).toFixed(2) + "MB";
} else if (input > 1024) {
return (input / 1000).toFixed(2) + "KB";
} else {
return input + "B";
}
};
export default ModelDownloadingButton; export default ModelDownloadingButton;

View File

@ -1,92 +1,36 @@
"use client"; "use client";
import { useEffect, useState } from "react";
import { execute, executeSerial } from "@/_services/pluginService";
import {
DataService,
ModelManagementService,
} from "../../../shared/coreService";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import { searchingModelText } from "@/_helpers/JotaiWrapper";
modelDownloadStateAtom,
searchingModelText,
} from "@/_helpers/JotaiWrapper";
import { Product } from "@/_models/Product"; import { Product } from "@/_models/Product";
import DownloadedModelCard from "../DownloadedModelCard"; import DownloadedModelCard from "../DownloadedModelCard";
import AvailableModelCard from "../AvailableModelCard"; import AvailableModelCard from "../AvailableModelCard";
import useDeleteModel from "@/_hooks/useDeleteModel";
import useGetAvailableModels from "@/_hooks/useGetAvailableModels";
import useDownloadModel from "@/_hooks/useDownloadModel";
const ModelListContainer: React.FC = () => { const ModelListContainer: React.FC = () => {
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
const [availableModels, setAvailableModels] = useState<Product[]>([]);
const downloadState = useAtomValue(modelDownloadStateAtom);
const searchText = useAtomValue(searchingModelText); const searchText = useAtomValue(searchingModelText);
const { deleteModel } = useDeleteModel();
const { downloadModel } = useDownloadModel();
useEffect(() => { const {
const getDownloadedModels = async () => { availableModels,
const avails = await executeSerial( downloadedModels,
ModelManagementService.GET_AVAILABLE_MODELS getAvailableModelExceptDownloaded,
); } = useGetAvailableModels();
const downloaded: Product[] = await executeSerial(
ModelManagementService.GET_DOWNLOADED_MODELS
);
const downloadedSucessfullyModels: Product[] = [];
const availableOrDownloadingModels: Product[] = avails;
downloaded.forEach((item) => {
if (item.fileName && downloadState[item.fileName] == null) {
downloadedSucessfullyModels.push(item);
} else {
availableOrDownloadingModels.push(item);
}
});
setAvailableModels(availableOrDownloadingModels);
setDownloadedModels(downloadedSucessfullyModels);
};
getDownloadedModels();
}, [downloadState]);
const onDeleteClick = async (product: Product) => { const onDeleteClick = async (product: Product) => {
execute(DataService.DELETE_DOWNLOAD_MODEL, product.id); await deleteModel(product);
await executeSerial(ModelManagementService.DELETE_MODEL, product.fileName); await getAvailableModelExceptDownloaded();
const getDownloadedModels = async () => {
const avails = await executeSerial(
ModelManagementService.GET_AVAILABLE_MODELS
);
const downloaded: Product[] = await executeSerial(
ModelManagementService.GET_DOWNLOADED_MODELS
);
const downloadedSucessfullyModels: Product[] = [];
const availableOrDownloadingModels: Product[] = avails;
downloaded.forEach((item) => {
if (item.fileName && downloadState[item.fileName] == null) {
downloadedSucessfullyModels.push(item);
} else {
availableOrDownloadingModels.push(item);
}
});
setAvailableModels(availableOrDownloadingModels);
setDownloadedModels(downloadedSucessfullyModels);
};
getDownloadedModels();
}; };
const onDownloadClick = async (product: Product) => { const onDownloadClick = async (model: Product) => {
await executeSerial(DataService.STORE_MODEL, product); await downloadModel(model);
await executeSerial(ModelManagementService.DOWNLOAD_MODEL, {
downloadUrl: product.downloadUrl,
fileName: product.fileName,
});
}; };
return ( return (
<div className="flex flex-col gap-5"> <div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px] overflow-y-auto">
<div className="pb-5 flex flex-col gap-2"> <div className="pb-5 flex flex-col gap-2">
<Title title="Downloaded models" /> <Title title="Downloaded models" />
{downloadedModels {downloadedModels

View File

@ -1,12 +1,10 @@
"use client"; "use client";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import {
currentProductAtom,
showConfirmDeleteConversationModalAtom,
} from "@/_helpers/JotaiWrapper";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PlusIcon, TrashIcon } from "@heroicons/react/24/outline";
import useCreateConversation from "@/_hooks/useCreateConversation"; import useCreateConversation from "@/_hooks/useCreateConversation";
import { showConfirmDeleteConversationModalAtom } from "@/_helpers/atoms/Modal.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const ModelMenu: React.FC = () => { const ModelMenu: React.FC = () => {
const currentProduct = useAtomValue(currentProductAtom); const currentProduct = useAtomValue(currentProductAtom);

View File

@ -0,0 +1,78 @@
import React from "react";
import { Product } from "@/_models/Product";
import Image from "next/image";
import { ModelStatus, ModelStatusComponent } from "../ModelStatusComponent";
import ModelActionMenu from "../ModelActionMenu";
import { useAtomValue } from "jotai";
import ModelActionButton, { ModelActionType } from "../ModelActionButton";
import useStartStopModel from "@/_hooks/useStartStopModel";
import useDeleteModel from "@/_hooks/useDeleteModel";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
type Props = {
model: Product;
};
const ModelRow: React.FC<Props> = ({ model }) => {
const { startModel } = useStartStopModel();
const activeModel = useAtomValue(currentProductAtom);
const { deleteModel } = useDeleteModel();
let status = ModelStatus.Installed;
if (activeModel && activeModel.id === model.id) {
status = ModelStatus.Active;
}
let actionButtonType = ModelActionType.Start;
if (activeModel && activeModel.id === model.id) {
actionButtonType = ModelActionType.Stop;
}
const onModelActionClick = (action: ModelActionType) => {
if (action === ModelActionType.Start) {
startModel(model.id);
}
};
const onDeleteClick = () => {
deleteModel(model);
};
return (
<tr
className="border-b border-gray-200 last:border-b-0 last:rounded-lg"
key={model.id}
>
<td className="flex flex-col whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">
{model.name}
<span className="text-gray-500 font-normal">{model.version}</span>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<div className="flex flex-col justify-start">
<span>{model.format}</span>
{model.accelerated && (
<span className="flex items-center text-gray-500 text-sm font-normal gap-0.5">
<Image src={"/icons/flash.svg"} width={20} height={20} alt="" />
GPU Accelerated
</span>
)}
</div>
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
{model.totalSize}
</td>
<td className="whitespace-nowrap px-6 py-4 text-sm text-gray-500">
<ModelStatusComponent status={status} />
</td>
<ModelActionButton
type={actionButtonType}
onActionClick={onModelActionClick}
/>
<td className="relative whitespace-nowrap px-6 py-4 w-fit text-right text-sm font-medium">
<ModelActionMenu onDeleteClick={onDeleteClick} />
</td>
</tr>
);
};
export default ModelRow;

View File

@ -0,0 +1,118 @@
import { Fragment, useEffect } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { Product } from "@/_models/Product";
import { useAtom } from "jotai";
import { selectedModelAtom } from "@/_helpers/atoms/Model.atom";
function classNames(...classes: any) {
return classes.filter(Boolean).join(" ");
}
const SelectModels: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom);
useEffect(() => {
if (downloadedModels && downloadedModels.length > 0) {
onModelSelected(downloadedModels[0]);
}
}, [downloadedModels]);
const onModelSelected = (model: Product) => {
setSelectedModel(model);
};
if (!selectedModel) {
return <div>You have not downloaded any model!</div>;
}
return (
<Listbox value={selectedModel} onChange={onModelSelected}>
{({ open }) => (
<div className="w-[461px]">
<Listbox.Label className="block text-sm font-medium leading-6 text-gray-900">
Select a Model:
</Listbox.Label>
<div className="relative mt-[19px]">
<Listbox.Button className="relative w-full cursor-default rounded-md bg-white py-1.5 pl-3 pr-10 text-left text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 sm:text-sm sm:leading-6">
<span className="flex items-center">
<img
src={selectedModel.avatarUrl}
alt=""
className="h-5 w-5 flex-shrink-0 rounded-full"
/>
<span className="ml-3 block truncate">
{selectedModel.name}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 ml-3 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-[188px] w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{downloadedModels.map((model) => (
<Listbox.Option
key={model.id}
className={({ active }) =>
classNames(
active ? "bg-indigo-600 text-white" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={model}
>
{({ selected, active }) => (
<>
<div className="flex items-center">
<img
src={model.avatarUrl}
alt=""
className="h-5 w-5 flex-shrink-0 rounded-full"
/>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"ml-3 block truncate"
)}
>
{model.name}
</span>
</div>
{selected ? (
<span
className={classNames(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</div>
)}
</Listbox>
);
};
export default SelectModels;

View File

@ -0,0 +1,46 @@
import React from "react";
export type ModelStatusType = {
title: string;
textColor: string;
backgroundColor: string;
};
export enum ModelStatus {
Installed,
Active,
RunningInNitro,
}
export const ModelStatusMapper: Record<ModelStatus, ModelStatusType> = {
[ModelStatus.Installed]: {
title: "Installed",
textColor: "text-black",
backgroundColor: "bg-gray-100",
},
[ModelStatus.Active]: {
title: "Active",
textColor: "text-black",
backgroundColor: "bg-green-100",
},
[ModelStatus.RunningInNitro]: {
title: "Running in Nitro",
textColor: "text-black",
backgroundColor: "bg-green-100",
},
};
type Props = {
status: ModelStatus;
};
export const ModelStatusComponent: React.FC<Props> = ({ status }) => {
const statusType = ModelStatusMapper[status];
return (
<div
className={`rounded-[10px] py-0.5 px-[10px] w-fit text-xs font-medium ${statusType.backgroundColor}`}
>
{statusType.title}
</div>
);
};

View File

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

View File

@ -0,0 +1,16 @@
import React from "react";
type Props = {
title: string;
};
const ModelTableHeader: React.FC<Props> = ({ title }) => (
<th
scope="col"
className="px-6 py-3 text-left first:rounded-tl-lg last:rounded-tr-lg text-xs font-medium uppercase tracking-wide text-gray-500"
>
{title}
</th>
);
export default React.memo(ModelTableHeader);

View File

@ -0,0 +1,25 @@
import React from "react";
import { toGigabytes } from "@/_utils/converter";
import Image from "next/image";
type Props = {
title: string;
totalSizeInByte: number;
};
const ModelVersionItem: React.FC<Props> = ({ title, totalSizeInByte }) => (
<div className="flex justify-between items-center gap-4 pl-[13px] pt-[13px] pr-[17px] pb-3 border-t border-gray-200 first:border-t-0">
<div className="flex items-center gap-4">
<Image src={"/icons/app_icon.svg"} width={14} height={20} alt="" />
<span className="font-sm text-gray-900">{title}</span>
</div>
<div className="flex items-center gap-4">
<div className="px-[10px] py-0.5 bg-gray-200 text-xs font-medium rounded">
{toGigabytes(totalSizeInByte)}
</div>
<button className="text-indigo-600 text-sm font-medium">Download</button>
</div>
</div>
);
export default ModelVersionItem;

View File

@ -0,0 +1,38 @@
import React from "react";
import ModelVersionItem from "../ModelVersionItem";
const data = [
{
name: "Q4_K_M.gguf",
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
{
name: "Q4_K_M.gguf",
total: 5600,
},
];
const ModelVersionList: React.FC = () => {
return (
<div className="px-4 py-5 border-t border-gray-200">
<div className="text-sm font-medium text-gray-500">
Available Versions
</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
{data.map((item, index) => (
<ModelVersionItem
key={index}
title={item.name}
totalSizeInByte={item.total}
/>
))}
</div>
</div>
);
};
export default ModelVersionList;

View File

@ -1,42 +1,22 @@
import ProgressBar from "../ProgressBar"; import ProgressBar from "../ProgressBar";
import SystemItem from "../SystemItem"; import SystemItem from "../SystemItem";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { import { appDownloadProgress } from "@/_helpers/JotaiWrapper";
activeModel,
appDownloadProgress,
getSystemBarVisibilityAtom,
} from "@/_helpers/JotaiWrapper";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager"; import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
import { SystemMonitoringService } from "../../../shared/coreService"; import { SystemMonitoringService } from "../../../shared/coreService";
import { getSystemBarVisibilityAtom } from "@/_helpers/atoms/SystemBar.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
const MonitorBar: React.FC = () => { const MonitorBar: React.FC = () => {
const show = useAtomValue(getSystemBarVisibilityAtom); const show = useAtomValue(getSystemBarVisibilityAtom);
const progress = useAtomValue(appDownloadProgress); const progress = useAtomValue(appDownloadProgress);
const modelName = useAtomValue(activeModel); const activeModel = useAtomValue(currentProductAtom);
const [ram, setRam] = useState<number>(0); const [ram, setRam] = useState<number>(0);
const [gpu, setGPU] = useState<number>(0); const [gpu, setGPU] = useState<number>(0);
const [cpu, setCPU] = useState<number>(0); const [cpu, setCPU] = useState<number>(0);
const [version, setVersion] = useState<string>(""); const [version, setVersion] = useState<string>("");
const data = [
{
name: "CPU",
total: 1400,
used: 750,
},
{
name: "Ram",
total: 16000,
used: 4500,
},
{
name: "VRAM",
total: 1400,
used: 1300,
},
];
useEffect(() => { useEffect(() => {
const getSystemResources = async () => { const getSystemResources = async () => {
const resourceInfor = await executeSerial( const resourceInfor = await executeSerial(
@ -77,8 +57,8 @@ const MonitorBar: React.FC = () => {
<SystemItem name="CPU" value={`${cpu}%`} /> <SystemItem name="CPU" value={`${cpu}%`} />
<SystemItem name="Mem" value={`${ram}%`} /> <SystemItem name="Mem" value={`${ram}%`} />
{modelName && modelName.length > 0 && ( {activeModel && (
<SystemItem name="Active Models" value={"1"} /> <SystemItem name={`Active model: ${activeModel.name}`} value={"1"} />
)} )}
<span className="text-gray-900 text-sm">v{version}</span> <span className="text-gray-900 text-sm">v{version}</span>
</div> </div>

View File

@ -0,0 +1,13 @@
import HeaderTitle from "../HeaderTitle";
import DownloadedModelTable from "../DownloadedModelTable";
import ActiveModelTable from "../ActiveModelTable";
const MyModelContainer: React.FC = () => (
<div className="flex flex-col w-full h-full pl-[63px] pr-[89px] pt-[60px]">
<HeaderTitle title="My Models" />
<ActiveModelTable />
<DownloadedModelTable />
</div>
);
export default MyModelContainer;

View File

@ -0,0 +1,39 @@
"use client";
import React from "react";
import SecondaryButton from "../SecondaryButton";
import { useAtomValue, useSetAtom } from "jotai";
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useCreateConversation from "@/_hooks/useCreateConversation";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
const NewChatButton: React.FC = () => {
const activeModel = useAtomValue(currentProductAtom);
const setMainView = useSetAtom(setMainViewStateAtom);
const { requestCreateConvo } = useCreateConversation();
const { initModel } = useInitModel();
const onClick = () => {
if (!activeModel) {
setMainView(MainViewState.ConversationEmptyModel);
} else {
createConversationAndInitModel(activeModel);
}
};
const createConversationAndInitModel = async (model: Product) => {
await requestCreateConvo(model);
await initModel(model);
};
return (
<SecondaryButton title={"New Chat"} onClick={onClick} className="my-5" />
);
};
export default NewChatButton;

View File

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

View File

@ -2,11 +2,15 @@ import { searchAtom } 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";
const SearchBar: React.FC = () => { type Props = {
const setText = useSetAtom(searchAtom); placeholder?: string;
};
const SearchBar: React.FC<Props> = ({ placeholder }) => {
const setText = useSetAtom(searchAtom);
let placeholderText = placeholder ? placeholder : "Search (⌘K)";
return ( return (
<div className="relative mx-3 mt-3 flex items-center"> <div className="relative mt-3 flex items-center">
<div className="absolute top-0 left-2 h-full flex items-center"> <div className="absolute top-0 left-2 h-full flex items-center">
<MagnifyingGlassIcon <MagnifyingGlassIcon
width={16} width={16}
@ -19,7 +23,7 @@ const SearchBar: React.FC = () => {
type="text" type="text"
name="search" name="search"
id="search" id="search"
placeholder="Search (⌘K)" placeholder={placeholderText}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" className="block w-full rounded-md border-0 py-1.5 pl-8 pr-14 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
/> />

View File

@ -2,14 +2,20 @@ type Props = {
title: string; title: string;
onClick: () => void; onClick: () => void;
disabled?: boolean; disabled?: boolean;
className?: string;
}; };
const SecondaryButton: React.FC<Props> = ({ title, onClick, disabled }) => ( const SecondaryButton: React.FC<Props> = ({
title,
onClick,
disabled,
className,
}) => (
<button <button
disabled={disabled} disabled={disabled}
type="button" type="button"
onClick={onClick} onClick={onClick}
className="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" className={`rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 ${className}`}
> >
{title} {title}
</button> </button>

View File

@ -1,7 +1,5 @@
import { import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
currentConvoStateAtom, import { currentConvoStateAtom } from "@/_helpers/atoms/Conversation.atom";
currentPromptAtom,
} from "@/_helpers/JotaiWrapper";
import useSendChatMessage from "@/_hooks/useSendChatMessage"; import useSendChatMessage from "@/_hooks/useSendChatMessage";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import Image from "next/image"; import Image from "next/image";

View File

@ -1,24 +0,0 @@
import Image from "next/image";
type Props = {
callback?: () => void;
className?: string;
icon: string;
width: number;
height: number;
title: string;
};
export const SidebarButton: React.FC<Props> = ({
callback,
height,
icon,
className,
width,
title,
}) => (
<button onClick={callback} className={className}>
<Image src={icon} width={width} height={height} alt="" />
<span>{title}</span>
</button>
);

View File

@ -1,28 +1,56 @@
import Image from "next/image"; import Image from "next/image";
import { SidebarButton } from "../SidebarButton";
import { executeSerial } from "../../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "../../../shared/coreService";
import useCreateConversation from "@/_hooks/useCreateConversation"; import useCreateConversation from "@/_hooks/useCreateConversation";
import PrimaryButton from "../PrimaryButton";
import { useAtomValue, useSetAtom } from "jotai";
import { useGetDownloadedModels } from "@/_hooks/useGetDownloadedModels";
import { useEffect, useState } from "react";
import {
MainViewState,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
import useInitModel from "@/_hooks/useInitModel";
import { Product } from "@/_models/Product";
enum ActionButton {
DownloadModel = "Download a Model",
StartChat = "Start a Conversation",
}
const SidebarEmptyHistory: React.FC = () => { const SidebarEmptyHistory: React.FC = () => {
const { downloadedModels } = useGetDownloadedModels();
const activeModel = useAtomValue(currentProductAtom);
const setMainView = useSetAtom(setMainViewStateAtom);
const { requestCreateConvo } = useCreateConversation(); const { requestCreateConvo } = useCreateConversation();
const startChat = async () => { const [action, setAction] = useState(ActionButton.DownloadModel);
// Host
if (window && !window.electronAPI) { const { initModel } = useInitModel();
// requestCreateConvo(); // TODO: get model id from somewhere
} useEffect(() => {
// Electron if (downloadedModels.length > 0) {
const downloadedModels = await executeSerial( setAction(ActionButton.StartChat);
DataService.GET_FINISHED_DOWNLOAD_MODELS
);
if (!downloadedModels || downloadedModels?.length === 0) {
alert(
"Seems like there is no model downloaded yet. Please download a model first."
);
} else { } else {
requestCreateConvo(downloadedModels[0]); setAction(ActionButton.DownloadModel);
}
}, [downloadedModels]);
const onClick = () => {
if (action === ActionButton.DownloadModel) {
setMainView(MainViewState.ExploreModel);
} else {
if (!activeModel) {
setMainView(MainViewState.ConversationEmptyModel);
} else {
createConversationAndInitModel(activeModel);
}
} }
}; };
const createConversationAndInitModel = async (model: Product) => {
await requestCreateConvo(model);
await initModel(model);
};
return ( return (
<div className="flex flex-col items-center py-10 gap-3"> <div className="flex flex-col items-center py-10 gap-3">
<Image <Image
@ -32,22 +60,11 @@ const SidebarEmptyHistory: React.FC = () => {
alt="" alt=""
/> />
<div className="flex flex-col items-center gap-6"> <div className="flex flex-col items-center gap-6">
<div> <div className="text-center text-gray-900 text-sm">No Chat History</div>
<div className="text-center text-gray-900 text-sm">
No Chat History
</div>
<div className="text-center text-gray-500 text-sm"> <div className="text-center text-gray-500 text-sm">
Get started by creating a new chat. Get started by creating a new chat.
</div> </div>
</div> <PrimaryButton title={action} onClick={onClick} />
<SidebarButton
callback={startChat}
className="flex items-center border bg-blue-600 rounded-lg py-[9px] pl-[15px] pr-[17px] gap-2 text-white font-medium text-sm"
height={14}
icon="icons/Icon_plus.svg"
title="New chat"
width={14}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,27 +1,21 @@
import React from "react"; import React from "react";
import { SidebarButton } from "../SidebarButton"; import SecondaryButton from "../SecondaryButton";
const SidebarFooter: React.FC = () => ( const SidebarFooter: React.FC = () => (
<div className="flex justify-between items-center gap-2"> <div className="flex justify-between items-center gap-2">
<SidebarButton <SecondaryButton
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm" title={"Discord"}
height={24} onClick={() =>
icon="icons/discord.svg" window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N")
title="Discord" }
width={24} className="flex-1"
callback={() => {
window.electronAPI?.openExternalUrl("https://discord.gg/AsJ8krTT3N");
}}
/> />
<SidebarButton <SecondaryButton
className="flex items-center border border-gray-200 rounded-lg p-2 gap-3 flex-1 justify-center text-gray-900 font-medium text-sm" title={"Discord"}
height={24} onClick={() =>
icon="icons/unicorn_twitter.svg" window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai")
title="Twitter" }
width={24} className="flex-1"
callback={() => {
window.electronAPI?.openExternalUrl("https://twitter.com/jan_dotai");
}}
/> />
</div> </div>
); );

View File

@ -1,17 +1,8 @@
import { import React from "react";
MainViewState, import SidebarMenuItem from "../SidebarMenuItem";
getMainViewStateAtom, import { MainViewState } from "@/_helpers/atoms/MainView.atom";
setMainViewStateAtom,
} from "@/_helpers/JotaiWrapper";
import classNames from "classnames";
import { useAtomValue, useSetAtom } from "jotai";
import Image from "next/image";
const SidebarMenu: React.FC = () => { const menu = [
const currentState = useAtomValue(getMainViewStateAtom);
const setMainViewState = useSetAtom(setMainViewStateAtom);
const menu = [
{ {
name: "Explore Models", name: "Explore Models",
icon: "Search_gray", icon: "Search_gray",
@ -27,43 +18,24 @@ const SidebarMenu: React.FC = () => {
icon: "Cog", icon: "Cog",
state: MainViewState.Setting, state: MainViewState.Setting,
}, },
]; ];
const onMenuClick = (state: MainViewState) => { const SidebarMenu: React.FC = () => (
if (state === currentState) return;
setMainViewState(state);
};
return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3"> <div className="text-gray-500 text-xs font-semibold py-2 pl-2 pr-3">
Your Configurations Your Configurations
</div> </div>
<ul role="list" className="-mx-2 mt-2 space-y-1"> <ul role="list" className="-mx-2 mt-2 space-y-1 mb-2">
{menu.map((item) => ( {menu.map((item) => (
<li key={item.name}> <SidebarMenuItem
<button title={item.name}
onClick={() => onMenuClick(item.state)} viewState={item.state}
className={classNames( iconName={item.icon}
currentState === item.state key={item.name}
? "bg-gray-50 text-indigo-600"
: "text-gray-600 hover:text-indigo-600 hover:bg-gray-50",
"group flex gap-x-3 rounded-md text-base py-2 px-3 w-full"
)}
>
<Image
src={`icons/${item.icon}.svg`}
width={24}
height={24}
alt=""
/> />
<span className="truncate">{item.name}</span>
</button>
</li>
))} ))}
</ul> </ul>
</div> </div>
); );
};
export default SidebarMenu; export default React.memo(SidebarMenu);

View File

@ -0,0 +1,41 @@
import React from "react";
import { useAtomValue, useSetAtom } from "jotai";
import Image from "next/image";
import {
MainViewState,
getMainViewStateAtom,
setMainViewStateAtom,
} from "@/_helpers/atoms/MainView.atom";
type Props = {
title: string;
viewState: MainViewState;
iconName: string;
};
const SidebarMenuItem: React.FC<Props> = ({ title, viewState, iconName }) => {
const currentState = useAtomValue(getMainViewStateAtom);
const setMainViewState = useSetAtom(setMainViewStateAtom);
let className =
"text-gray-600 hover:text-indigo-600 hover:bg-gray-50 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full";
if (currentState == viewState) {
className =
"bg-gray-100 text-indigo-600 group flex gap-x-3 rounded-md text-base py-2 px-3 w-full";
}
const onClick = () => {
setMainViewState(viewState);
};
return (
<li key={title}>
<button onClick={onClick} className={className}>
<Image src={`icons/${iconName}.svg`} width={24} height={24} alt="" />
<span className="truncate">{title}</span>
</button>
</li>
);
};
export default SidebarMenuItem;

View File

@ -0,0 +1,22 @@
type Props = {
name: string;
};
const SimpleCheckbox: React.FC<Props> = ({ name }) => (
<div className="relative flex items-center gap-[11px]">
<div className="flex h-6 items-center">
<input
id="offers"
aria-describedby="offers-description"
name="offers"
type="checkbox"
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
/>
</div>
<div className="text-xs">
<label htmlFor="offers">{name}</label>
</div>
</div>
);
export default SimpleCheckbox;

View File

@ -0,0 +1,89 @@
import React from "react";
export enum TagType {
Roleplay = "Roleplay",
Llama = "Llama",
Story = "Story",
Casual = "Casual",
Professional = "Professional",
CodeLlama = "CodeLlama",
Coding = "Coding",
// Positive
Recommended = "Recommended",
Compatible = "Compatible",
// Neutral
SlowOnDevice = "This model will be slow on your device",
// Negative
InsufficientRam = "Insufficient RAM",
Incompatible = "Incompatible with your device",
TooLarge = "This model is too large for your device",
// Performance
Medium = "Medium",
BalancedQuality = "Balanced Quality",
}
const tagStyleMapper: Record<TagType, string> = {
[TagType.Roleplay]: "bg-red-100 text-red-800",
[TagType.Llama]: "bg-green-100 text-green-800",
[TagType.Story]: "bg-blue-100 text-blue-800",
[TagType.Casual]: "bg-yellow-100 text-yellow-800",
[TagType.Professional]: "text-indigo-800 bg-indigo-100",
[TagType.CodeLlama]: "bg-pink-100 text-pink-800",
[TagType.Coding]: "text-purple-800 bg-purple-100",
[TagType.Recommended]:
"text-green-700 ring-1 ring-inset ring-green-600/20 bg-green-50",
[TagType.Compatible]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.SlowOnDevice]:
"bg-yellow-50 text-yellow-800 ring-1 ring-inset ring-yellow-600/20",
[TagType.Incompatible]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.InsufficientRam]:
"bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.TooLarge]: "bg-red-50 ext-red-700 ring-1 ring-inset ring-red-600/10",
[TagType.Medium]: "bg-yellow-100 text-yellow-800",
[TagType.BalancedQuality]: "bg-yellow-100 text-yellow-800",
};
type Props = {
title: string;
type: TagType;
clickable?: boolean;
onClick?: () => void;
};
const SimpleTag: React.FC<Props> = ({
onClick,
clickable = true,
title,
type,
}) => {
if (!clickable) {
return (
<div
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title}
</div>
);
}
return (
<button
onClick={onClick}
className={`px-[10px] py-0.5 rounded text-xs font-medium ${tagStyleMapper[type]}`}
>
{title} x
</button>
);
};
export default React.memo(SimpleTag);

View File

@ -4,7 +4,7 @@ import { TextCode } from "../TextCode";
import { getMessageCode } from "@/_utils/message"; import { getMessageCode } from "@/_utils/message";
import Image from "next/image"; import Image from "next/image";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { currentStreamingMessageAtom } from "@/_helpers/JotaiWrapper"; import { currentStreamingMessageAtom } from "@/_helpers/atoms/ChatMessage.atom";
type Props = { type Props = {
id: string; id: string;

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { currentConversationAtom } from "@/_helpers/JotaiWrapper"; import { currentConversationAtom } from "@/_helpers/atoms/Conversation.atom";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import Image from "next/image"; import Image from "next/image";

View File

@ -1,7 +1,10 @@
import Image from "next/image"; import Image from "next/image";
import { SidebarButton } from "../SidebarButton";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { MainViewState, setMainViewStateAtom } from "@/_helpers/JotaiWrapper"; import {
setMainViewStateAtom,
MainViewState,
} from "@/_helpers/atoms/MainView.atom";
import SecondaryButton from "../SecondaryButton";
const Welcome: React.FC = () => { const Welcome: React.FC = () => {
const setMainViewState = useSetAtom(setMainViewStateAtom); const setMainViewState = useSetAtom(setMainViewStateAtom);
@ -15,13 +18,9 @@ const Welcome: React.FC = () => {
<br /> <br />
lets download your first model lets download your first model
</span> </span>
<SidebarButton <SecondaryButton
callback={() => setMainViewState(MainViewState.ExploreModel)} title={"Explore models"}
className="flex flex-row-reverse items-center rounded-lg gap-2 px-3 py-2 text-xs font-medium border border-gray-200" onClick={() => setMainViewState(MainViewState.ExploreModel)}
icon={"icons/app_icon.svg"}
title="Explore models"
height={16}
width={16}
/> />
</div> </div>
</div> </div>

View File

@ -1,15 +1,15 @@
"use client"; "use client";
import { useAtom, useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect } from "react";
import { import { appDownloadProgress } from "./JotaiWrapper";
appDownloadProgress,
setDownloadStateAtom,
setDownloadStateSuccessAtom,
} from "./JotaiWrapper";
import { DownloadState } from "@/_models/DownloadState"; import { DownloadState } from "@/_models/DownloadState";
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager"; import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService } from "../../shared/coreService"; import { DataService } from "../../shared/coreService";
import {
setDownloadStateAtom,
setDownloadStateSuccessAtom,
} from "./atoms/DownloadState.atom";
type Props = { type Props = {
children: ReactNode; children: ReactNode;

View File

@ -1,9 +1,5 @@
"use client"; "use client";
import { ChatMessage, MessageStatus } from "@/_models/ChatMessage";
import { Conversation, ConversationState } from "@/_models/Conversation";
import { DownloadState } from "@/_models/DownloadState";
import { Product } from "@/_models/Product";
import { Provider, atom } from "jotai"; import { Provider, atom } from "jotai";
import { ReactNode } from "react"; import { ReactNode } from "react";
@ -15,286 +11,11 @@ export default function JotaiWrapper({ children }: Props) {
return <Provider>{children}</Provider>; return <Provider>{children}</Provider>;
} }
const activeConversationIdAtom = atom<string | undefined>(undefined);
export const getActiveConvoIdAtom = atom((get) =>
get(activeConversationIdAtom)
);
export const setActiveConvoIdAtom = atom(
null,
(_get, set, convoId: string | undefined) => {
if (convoId) {
console.log(`set active convo id to ${convoId}`);
set(setMainViewStateAtom, MainViewState.Conversation);
}
set(activeConversationIdAtom, convoId);
}
);
export const currentPromptAtom = atom<string>(""); export const currentPromptAtom = atom<string>("");
export const showingAdvancedPromptAtom = atom<boolean>(false);
export const showingProductDetailAtom = atom<boolean>(false);
export const showingMobilePaneAtom = atom<boolean>(false);
export const showingTyping = atom<boolean>(false); export const showingTyping = atom<boolean>(false);
export const appDownloadProgress = atom<number>(-1); export const appDownloadProgress = atom<number>(-1);
export const activeModel = atom<string | undefined>(undefined);
export const searchingModelText = atom<string>(""); export const searchingModelText = atom<string>("");
/**
* Stores all conversations for the current user
*/
export const userConversationsAtom = atom<Conversation[]>([]);
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
get(userConversationsAtom).find((c) => c.id === get(activeConversationIdAtom))
);
export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => {
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
if (!convo) return;
const newConvo: Conversation = {
...convo,
updated_at: new Date().toISOString(),
};
const newConversations: Conversation[] = get(userConversationsAtom).map((c) =>
c.id === convoId ? newConvo : c
);
set(userConversationsAtom, newConversations);
});
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
undefined
);
export const setConvoLastImageAtom = atom(
null,
(get, set, convoId: string, lastImageUrl: string) => {
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
if (!convo) return;
const newConvo: Conversation = { ...convo };
const newConversations: Conversation[] = get(userConversationsAtom).map(
(c) => (c.id === convoId ? newConvo : c)
);
set(userConversationsAtom, newConversations);
}
);
/**
* Stores all conversation states for the current user
*/
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
{}
);
export const currentConvoStateAtom = atom<ConversationState | undefined>(
(get) => {
const activeConvoId = get(activeConversationIdAtom);
if (!activeConvoId) {
console.log("active convo id is undefined");
return undefined;
}
return get(conversationStatesAtom)[activeConvoId];
}
);
export const addNewConversationStateAtom = atom(
null,
(get, set, conversationId: string, state: ConversationState) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = state;
set(conversationStatesAtom, currentState);
}
);
export const updateConversationWaitingForResponseAtom = atom(
null,
(get, set, conversationId: string, waitingForResponse: boolean) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = {
...currentState[conversationId],
waitingForResponse,
};
set(conversationStatesAtom, currentState);
}
);
export const updateConversationHasMoreAtom = atom(
null,
(get, set, conversationId: string, hasMore: boolean) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = { ...currentState[conversationId], hasMore };
set(conversationStatesAtom, currentState);
}
);
/**
* Stores all chat messages for all conversations
*/
export const chatMessages = atom<Record<string, ChatMessage[]>>({});
export const currentChatMessagesAtom = atom<ChatMessage[]>((get) => {
const activeConversationId = get(activeConversationIdAtom);
if (!activeConversationId) return [];
return get(chatMessages)[activeConversationId] ?? [];
});
export const addOldMessagesAtom = atom(
null,
(get, set, newMessages: ChatMessage[]) => {
const currentConvoId = get(activeConversationIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const updatedMessages = [...currentMessages, ...newMessages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = updatedMessages;
set(chatMessages, newData);
}
);
export const addNewMessageAtom = atom(
null,
(get, set, newMessage: ChatMessage) => {
const currentConvoId = get(activeConversationIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const updatedMessages = [newMessage, ...currentMessages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = updatedMessages;
set(chatMessages, newData);
}
);
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[id] = [];
set(chatMessages, newData);
});
export const updateMessageAtom = atom(
null,
(get, set, id: string, conversationId: string, text: string) => {
const messages = get(chatMessages)[conversationId] ?? [];
const message = messages.find((e) => e.id === id);
if (message) {
message.text = text;
const updatedMessages = [...messages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[conversationId] = updatedMessages;
set(chatMessages, newData);
}
}
);
/**
* For updating the status of the last AI message that is pending
*/
export const updateLastMessageAsReadyAtom = atom(
null,
(get, set, id, text: string) => {
const currentConvoId = get(activeConversationIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const messageToUpdate = currentMessages.find((e) => e.id === id);
// if message is not found, do nothing
if (!messageToUpdate) return;
const index = currentMessages.indexOf(messageToUpdate);
const updatedMsg: ChatMessage = {
...messageToUpdate,
status: MessageStatus.Ready,
text: text,
};
currentMessages[index] = updatedMsg;
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = currentMessages;
set(chatMessages, newData);
}
);
export const currentProductAtom = atom<Product | undefined>(undefined);
export const searchAtom = atom<string>(""); export const searchAtom = atom<string>("");
// modal atoms
export const showConfirmDeleteConversationModalAtom = atom(false);
export const showConfirmSignOutModalAtom = atom(false);
export const showConfirmDeleteModalAtom = atom(false);
export type FileDownloadStates = {
[key: string]: DownloadState;
};
// main view state
export enum MainViewState {
Welcome,
ExploreModel,
MyModel,
ResourceMonitor,
Setting,
Conversation,
}
const systemBarVisibilityAtom = atom<boolean>(true);
export const getSystemBarVisibilityAtom = atom((get) =>
get(systemBarVisibilityAtom)
);
const currentMainViewStateAtom = atom<MainViewState>(MainViewState.Welcome);
export const getMainViewStateAtom = atom((get) =>
get(currentMainViewStateAtom)
);
export const setMainViewStateAtom = atom(
null,
(_get, set, state: MainViewState) => {
if (state !== MainViewState.Conversation) {
set(activeConversationIdAtom, undefined);
}
const showSystemBar = state !== MainViewState.Conversation;
set(systemBarVisibilityAtom, showSystemBar);
set(currentMainViewStateAtom, state);
}
);
// download states
export const modelDownloadStateAtom = atom<FileDownloadStates>({});
export const setDownloadStateAtom = atom(
null,
(get, set, state: DownloadState) => {
const currentState = { ...get(modelDownloadStateAtom) };
console.debug(
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
);
currentState[state.fileName] = state;
set(modelDownloadStateAtom, currentState);
}
);
export const setDownloadStateSuccessAtom = atom(
null,
(get, set, fileName: string) => {
const currentState = { ...get(modelDownloadStateAtom) };
const state = currentState[fileName];
if (!state) {
console.error(`Cannot find download state for ${fileName}`);
return;
}
delete currentState[fileName];
set(modelDownloadStateAtom, currentState);
}
);

View File

@ -0,0 +1,109 @@
import { ChatMessage, MessageStatus } from "@/_models/ChatMessage";
import { atom } from "jotai";
import { getActiveConvoIdAtom } from "./Conversation.atom";
/**
* Stores all chat messages for all conversations
*/
export const chatMessages = atom<Record<string, ChatMessage[]>>({});
export const currentChatMessagesAtom = atom<ChatMessage[]>((get) => {
const activeConversationId = get(getActiveConvoIdAtom);
if (!activeConversationId) return [];
return get(chatMessages)[activeConversationId] ?? [];
});
export const addOldMessagesAtom = atom(
null,
(get, set, newMessages: ChatMessage[]) => {
const currentConvoId = get(getActiveConvoIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const updatedMessages = [...currentMessages, ...newMessages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = updatedMessages;
set(chatMessages, newData);
}
);
export const addNewMessageAtom = atom(
null,
(get, set, newMessage: ChatMessage) => {
const currentConvoId = get(getActiveConvoIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const updatedMessages = [newMessage, ...currentMessages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = updatedMessages;
set(chatMessages, newData);
}
);
export const deleteConversationMessage = atom(null, (get, set, id: string) => {
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[id] = [];
set(chatMessages, newData);
});
export const updateMessageAtom = atom(
null,
(get, set, id: string, conversationId: string, text: string) => {
const messages = get(chatMessages)[conversationId] ?? [];
const message = messages.find((e) => e.id === id);
if (message) {
message.text = text;
const updatedMessages = [...messages];
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[conversationId] = updatedMessages;
set(chatMessages, newData);
}
}
);
/**
* For updating the status of the last AI message that is pending
*/
export const updateLastMessageAsReadyAtom = atom(
null,
(get, set, id, text: string) => {
const currentConvoId = get(getActiveConvoIdAtom);
if (!currentConvoId) return;
const currentMessages = get(chatMessages)[currentConvoId] ?? [];
const messageToUpdate = currentMessages.find((e) => e.id === id);
// if message is not found, do nothing
if (!messageToUpdate) return;
const index = currentMessages.indexOf(messageToUpdate);
const updatedMsg: ChatMessage = {
...messageToUpdate,
status: MessageStatus.Ready,
text: text,
};
currentMessages[index] = updatedMsg;
const newData: Record<string, ChatMessage[]> = {
...get(chatMessages),
};
newData[currentConvoId] = currentMessages;
set(chatMessages, newData);
}
);
export const currentStreamingMessageAtom = atom<ChatMessage | undefined>(
undefined
);

View File

@ -0,0 +1,104 @@
import { atom } from "jotai";
import { MainViewState, setMainViewStateAtom } from "./MainView.atom";
import { Conversation, ConversationState } from "@/_models/Conversation";
/**
* Stores the current active conversation id.
*/
const activeConversationIdAtom = atom<string | undefined>(undefined);
export const getActiveConvoIdAtom = atom((get) =>
get(activeConversationIdAtom)
);
export const setActiveConvoIdAtom = atom(
null,
(_get, set, convoId: string | undefined) => {
if (convoId) {
console.debug(`Set active conversation id: ${convoId}`);
set(setMainViewStateAtom, MainViewState.Conversation);
}
set(activeConversationIdAtom, convoId);
}
);
/**
* Stores all conversation states for the current user
*/
export const conversationStatesAtom = atom<Record<string, ConversationState>>(
{}
);
export const currentConvoStateAtom = atom<ConversationState | undefined>(
(get) => {
const activeConvoId = get(activeConversationIdAtom);
if (!activeConvoId) {
console.log("active convo id is undefined");
return undefined;
}
return get(conversationStatesAtom)[activeConvoId];
}
);
export const addNewConversationStateAtom = atom(
null,
(get, set, conversationId: string, state: ConversationState) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = state;
set(conversationStatesAtom, currentState);
}
);
export const updateConversationWaitingForResponseAtom = atom(
null,
(get, set, conversationId: string, waitingForResponse: boolean) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = {
...currentState[conversationId],
waitingForResponse,
};
set(conversationStatesAtom, currentState);
}
);
export const updateConversationHasMoreAtom = atom(
null,
(get, set, conversationId: string, hasMore: boolean) => {
const currentState = { ...get(conversationStatesAtom) };
currentState[conversationId] = { ...currentState[conversationId], hasMore };
set(conversationStatesAtom, currentState);
}
);
/**
* Stores all conversations for the current user
*/
export const userConversationsAtom = atom<Conversation[]>([]);
export const currentConversationAtom = atom<Conversation | undefined>((get) =>
get(userConversationsAtom).find((c) => c.id === get(getActiveConvoIdAtom))
);
export const setConvoUpdatedAtAtom = atom(null, (get, set, convoId: string) => {
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
if (!convo) return;
const newConvo: Conversation = {
...convo,
updated_at: new Date().toISOString(),
};
const newConversations: Conversation[] = get(userConversationsAtom).map((c) =>
c.id === convoId ? newConvo : c
);
set(userConversationsAtom, newConversations);
});
export const setConvoLastImageAtom = atom(
null,
(get, set, convoId: string, lastImageUrl: string) => {
const convo = get(userConversationsAtom).find((c) => c.id === convoId);
if (!convo) return;
const newConvo: Conversation = { ...convo };
const newConversations: Conversation[] = get(userConversationsAtom).map(
(c) => (c.id === convoId ? newConvo : c)
);
set(userConversationsAtom, newConversations);
}
);

View File

@ -0,0 +1,32 @@
import { DownloadState } from "@/_models/DownloadState";
import { atom } from "jotai";
// download states
export const modelDownloadStateAtom = atom<Record<string, DownloadState>>({});
export const setDownloadStateAtom = atom(
null,
(get, set, state: DownloadState) => {
const currentState = { ...get(modelDownloadStateAtom) };
console.debug(
`current download state for ${state.fileName} is ${JSON.stringify(state)}`
);
currentState[state.fileName] = state;
set(modelDownloadStateAtom, currentState);
}
);
export const setDownloadStateSuccessAtom = atom(
null,
(get, set, fileName: string) => {
const currentState = { ...get(modelDownloadStateAtom) };
const state = currentState[fileName];
if (!state) {
console.error(`Cannot find download state for ${fileName}`);
return;
}
delete currentState[fileName];
set(modelDownloadStateAtom, currentState);
}
);

View File

@ -0,0 +1,54 @@
import { atom } from "jotai";
import { setActiveConvoIdAtom } from "./Conversation.atom";
import { systemBarVisibilityAtom } from "./SystemBar.atom";
export enum MainViewState {
Welcome,
ExploreModel,
MyModel,
ResourceMonitor,
Setting,
Conversation,
/**
* When user wants to create new conversation but haven't selected a model yet.
*/
ConversationEmptyModel,
}
/**
* Stores the current main view state. Default is Welcome.
*/
const currentMainViewStateAtom = atom<MainViewState>(MainViewState.Welcome);
/**
* Getter for current main view state.
*/
export const getMainViewStateAtom = atom((get) =>
get(currentMainViewStateAtom)
);
/**
* Setter for current main view state.
*/
export const setMainViewStateAtom = atom(
null,
(get, set, state: MainViewState) => {
// return if the state is already set
if (get(getMainViewStateAtom) === state) return;
if (state !== MainViewState.Conversation) {
// clear active conversation id if main view state is not Conversation
set(setActiveConvoIdAtom, undefined);
}
const showSystemBar =
state !== MainViewState.Conversation &&
state !== MainViewState.ConversationEmptyModel;
// show system bar if state is not Conversation nor ConversationEmptyModel
set(systemBarVisibilityAtom, showSystemBar);
set(currentMainViewStateAtom, state);
}
);

View File

@ -0,0 +1,8 @@
import { atom } from "jotai";
export const showConfirmDeleteConversationModalAtom = atom(false);
export const showConfirmSignOutModalAtom = atom(false);
export const showConfirmDeleteModalAtom = atom(false);
export const showingAdvancedPromptAtom = atom<boolean>(false);
export const showingProductDetailAtom = atom<boolean>(false);
export const showingMobilePaneAtom = atom<boolean>(false);

View File

@ -0,0 +1,6 @@
import { Product } from "@/_models/Product";
import { atom } from "jotai";
export const currentProductAtom = atom<Product | undefined>(undefined);
export const selectedModelAtom = atom<Product | undefined>(undefined);

View File

@ -0,0 +1,7 @@
import { atom } from "jotai";
export const systemBarVisibilityAtom = atom<boolean>(true);
export const getSystemBarVisibilityAtom = atom((get) =>
get(systemBarVisibilityAtom)
);

View File

@ -1,14 +1,14 @@
import {
addOldMessagesAtom,
conversationStatesAtom,
currentConversationAtom,
updateConversationHasMoreAtom,
} from "@/_helpers/JotaiWrapper";
import { ChatMessage, RawMessage, toChatMessage } from "@/_models/ChatMessage"; import { ChatMessage, RawMessage, toChatMessage } from "@/_models/ChatMessage";
import { executeSerial } from "@/_services/pluginService"; import { executeSerial } from "@/_services/pluginService";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DataService } from "../../shared/coreService"; import { DataService } from "../../shared/coreService";
import { addOldMessagesAtom } from "@/_helpers/atoms/ChatMessage.atom";
import {
currentConversationAtom,
conversationStatesAtom,
updateConversationHasMoreAtom,
} from "@/_helpers/atoms/Conversation.atom";
/** /**
* Custom hooks to get chat messages for current(active) conversation * Custom hooks to get chat messages for current(active) conversation

View File

@ -1,25 +1,22 @@
// import useGetCurrentUser from "./useGetCurrentUser";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import {
activeModel,
addNewConversationStateAtom,
currentProductAtom,
setActiveConvoIdAtom,
userConversationsAtom,
} from "@/_helpers/JotaiWrapper";
import { Conversation } from "@/_models/Conversation"; import { Conversation } from "@/_models/Conversation";
import { executeSerial } from "@/_services/pluginService"; import { executeSerial } from "@/_services/pluginService";
import { DataService, InfereceService } from "../../shared/coreService"; import { DataService } from "../../shared/coreService";
import { Product } from "@/_models/Product"; import { Product } from "@/_models/Product";
import {
userConversationsAtom,
setActiveConvoIdAtom,
addNewConversationStateAtom,
} from "@/_helpers/atoms/Conversation.atom";
import useInitModel from "./useInitModel";
const useCreateConversation = () => { const useCreateConversation = () => {
const { initModel } = useInitModel();
const [userConversations, setUserConversations] = useAtom( const [userConversations, setUserConversations] = useAtom(
userConversationsAtom userConversationsAtom
); );
const setActiveConvoId = useSetAtom(setActiveConvoIdAtom); const setActiveConvoId = useSetAtom(setActiveConvoIdAtom);
const addNewConvoState = useSetAtom(addNewConversationStateAtom); const addNewConvoState = useSetAtom(addNewConversationStateAtom);
const setActiveModel = useSetAtom(activeModel);
const setActiveProduct = useSetAtom(currentProductAtom);
const requestCreateConvo = async (model: Product) => { const requestCreateConvo = async (model: Product) => {
const conv: Conversation = { const conv: Conversation = {
@ -30,9 +27,7 @@ const useCreateConversation = () => {
name: "Conversation", name: "Conversation",
}; };
const id = await executeSerial(DataService.CREATE_CONVERSATION, conv); const id = await executeSerial(DataService.CREATE_CONVERSATION, conv);
await executeSerial(InfereceService.INIT_MODEL, model); await initModel(model);
setActiveProduct(model);
setActiveModel(model.name);
const mappedConvo: Conversation = { const mappedConvo: Conversation = {
id, id,

View File

@ -1,15 +1,17 @@
import { import { currentPromptAtom } from "@/_helpers/JotaiWrapper";
currentPromptAtom,
deleteConversationMessage,
getActiveConvoIdAtom,
setActiveConvoIdAtom,
showingAdvancedPromptAtom,
showingProductDetailAtom,
userConversationsAtom,
} from "@/_helpers/JotaiWrapper";
import { execute } from "@/_services/pluginService"; import { execute } from "@/_services/pluginService";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { DataService } from "../../shared/coreService"; import { DataService } from "../../shared/coreService";
import { deleteConversationMessage } from "@/_helpers/atoms/ChatMessage.atom";
import {
userConversationsAtom,
getActiveConvoIdAtom,
setActiveConvoIdAtom,
} from "@/_helpers/atoms/Conversation.atom";
import {
showingProductDetailAtom,
showingAdvancedPromptAtom,
} from "@/_helpers/atoms/Modal.atom";
export default function useDeleteConversation() { export default function useDeleteConversation() {
const [userConversations, setUserConversations] = useAtom( const [userConversations, setUserConversations] = useAtom(

View File

@ -0,0 +1,12 @@
import { execute, executeSerial } from "@/_services/pluginService";
import { DataService, ModelManagementService } from "../../shared/coreService";
import { Product } from "@/_models/Product";
export default function useDeleteModel() {
const deleteModel = async (model: Product) => {
execute(DataService.DELETE_DOWNLOAD_MODEL, model.id);
await executeSerial(ModelManagementService.DELETE_MODEL, model.fileName);
};
return { deleteModel };
}

View File

@ -0,0 +1,17 @@
import { executeSerial } from "@/_services/pluginService";
import { DataService, ModelManagementService } from "../../shared/coreService";
import { Product } from "@/_models/Product";
export default function useDownloadModel() {
const downloadModel = async (model: Product) => {
await executeSerial(DataService.STORE_MODEL, model);
await executeSerial(ModelManagementService.DOWNLOAD_MODEL, {
downloadUrl: model.downloadUrl,
fileName: model.fileName,
});
};
return {
downloadModel,
};
}

View File

@ -0,0 +1,54 @@
import { Product } from "@/_models/Product";
import { executeSerial } from "@/_services/pluginService";
import { ModelManagementService } from "../../shared/coreService";
import { useEffect, useState } from "react";
import { getModelFiles } from "./useGetDownloadedModels";
import { useAtomValue } from "jotai";
import { modelDownloadStateAtom } from "@/_helpers/atoms/DownloadState.atom";
export default function useGetAvailableModels() {
const downloadState = useAtomValue(modelDownloadStateAtom);
const [allAvailableModels, setAllAvailableModels] = useState<Product[]>([]);
const [availableModels, setAvailableModels] = useState<Product[]>([]);
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
const getAvailableModelExceptDownloaded = async () => {
const avails = await getAvailableModels();
const downloaded = await getModelFiles();
setAllAvailableModels(avails);
const availableOrDownloadingModels: Product[] = avails;
const successfullDownloadModels: Product[] = [];
downloaded.forEach((item) => {
if (item.fileName && downloadState[item.fileName] == null) {
// if not downloading, consider as downloaded
successfullDownloadModels.push(item);
} else {
availableOrDownloadingModels.push(item);
}
});
setAvailableModels(availableOrDownloadingModels);
setDownloadedModels(successfullDownloadModels);
};
useEffect(() => {
getAvailableModelExceptDownloaded();
}, []);
return {
allAvailableModels,
availableModels,
downloadedModels,
getAvailableModelExceptDownloaded,
};
}
export async function getAvailableModels(): Promise<Product[]> {
const avails: Product[] = await executeSerial(
ModelManagementService.GET_AVAILABLE_MODELS
);
return avails ?? [];
}

View File

@ -0,0 +1,30 @@
import { Product } from "@/_models/Product";
import { useEffect, useState } from "react";
import { executeSerial } from "../../../electron/core/plugin-manager/execution/extension-manager";
import { DataService, ModelManagementService } from "../../shared/coreService";
export function useGetDownloadedModels() {
const [downloadedModels, setDownloadedModels] = useState<Product[]>([]);
useEffect(() => {
getDownloadedModels().then((downloadedModels) => {
setDownloadedModels(downloadedModels);
});
}, []);
return { downloadedModels };
}
export async function getDownloadedModels(): Promise<Product[]> {
const downloadedModels: Product[] = await executeSerial(
DataService.GET_FINISHED_DOWNLOAD_MODELS
);
return downloadedModels ?? [];
}
export async function getModelFiles(): Promise<Product[]> {
const downloadedModels: Product[] = await executeSerial(
ModelManagementService.GET_DOWNLOADED_MODELS
);
return downloadedModels ?? [];
}

View File

@ -1,11 +1,11 @@
import { Conversation, ConversationState } from "@/_models/Conversation"; import { Conversation, ConversationState } from "@/_models/Conversation";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { executeSerial } from "@/_services/pluginService";
import { DataService } from "../../shared/coreService";
import { import {
conversationStatesAtom, conversationStatesAtom,
userConversationsAtom, userConversationsAtom,
} from "@/_helpers/JotaiWrapper"; } from "@/_helpers/atoms/Conversation.atom";
import { executeSerial } from "@/_services/pluginService";
import { DataService } from "../../shared/coreService";
const useGetUserConversations = () => { const useGetUserConversations = () => {
const setConversationStates = useSetAtom(conversationStatesAtom); const setConversationStates = useSetAtom(conversationStatesAtom);

View File

@ -0,0 +1,25 @@
import { Product } from "@/_models/Product";
import { executeSerial } from "@/_services/pluginService";
import { InfereceService } from "../../shared/coreService";
import { useAtom } from "jotai";
import { currentProductAtom } from "@/_helpers/atoms/Model.atom";
export default function useInitModel() {
const [activeModel, setActiveModel] = useAtom(currentProductAtom);
const initModel = async (model: Product) => {
if (activeModel && activeModel.id === model.id) {
console.debug(`Model ${model.id} is already init. Ignore..`);
return;
}
try {
await executeSerial(InfereceService.INIT_MODEL, model);
console.debug(`Init model ${model.name} successfully!`);
setActiveModel(model);
} catch (err) {
console.error(`Init model ${model.name} failed: ${err}`);
}
};
return { initModel };
}

View File

@ -1,14 +1,4 @@
import { import { currentPromptAtom, showingTyping } from "@/_helpers/JotaiWrapper";
addNewMessageAtom,
chatMessages,
currentConversationAtom,
currentPromptAtom,
currentStreamingMessageAtom,
getActiveConvoIdAtom,
showingTyping,
updateMessageAtom,
} from "@/_helpers/JotaiWrapper";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { selectAtom } from "jotai/utils"; import { selectAtom } from "jotai/utils";
import { DataService, InfereceService } from "../../shared/coreService"; import { DataService, InfereceService } from "../../shared/coreService";
@ -19,6 +9,16 @@ import {
} from "@/_models/ChatMessage"; } from "@/_models/ChatMessage";
import { executeSerial } from "@/_services/pluginService"; import { executeSerial } from "@/_services/pluginService";
import { useCallback } from "react"; import { useCallback } from "react";
import {
addNewMessageAtom,
updateMessageAtom,
chatMessages,
currentStreamingMessageAtom,
} from "@/_helpers/atoms/ChatMessage.atom";
import {
currentConversationAtom,
getActiveConvoIdAtom,
} from "@/_helpers/atoms/Conversation.atom";
export default function useSendChatMessage() { export default function useSendChatMessage() {
const currentConvo = useAtomValue(currentConversationAtom); const currentConvo = useAtomValue(currentConversationAtom);
@ -50,9 +50,9 @@ export default function useSendChatMessage() {
const newChatMessage = await toChatMessage(newMessage); const newChatMessage = await toChatMessage(newMessage);
addNewMessage(newChatMessage); addNewMessage(newChatMessage);
const messageHistory = chatMessagesHistory ?? [];
const recentMessages = [ const recentMessages = [
...chatMessagesHistory.sort((a, b) => parseInt(a.id) - parseInt(b.id)), ...messageHistory.sort((a, b) => parseInt(a.id) - parseInt(b.id)),
newChatMessage, newChatMessage,
] ]
.slice(-10) .slice(-10)

View File

@ -0,0 +1,20 @@
import { executeSerial } from "@/_services/pluginService";
import { DataService } from "../../shared/coreService";
import useInitModel from "./useInitModel";
export default function useStartStopModel() {
const { initModel } = useInitModel();
const startModel = async (modelId: string) => {
const model = await executeSerial(DataService.GET_MODEL_BY_ID, modelId);
if (!model) {
alert(`Model ${modelId} not found! Please re-download the model first.`);
} else {
await initModel(model);
}
};
const stopModel = async (modelId: string) => {};
return { startModel, stopModel };
}

View File

@ -27,4 +27,10 @@ export interface Product {
updatedAt?: number; updatedAt?: number;
fileName?: string; fileName?: string;
downloadUrl?: string; downloadUrl?: string;
accelerated: boolean; // TODO: add this in the database
totalSize: number; // TODO: add this in the database
format: string; // TODO: add this in the database // GGUF or something else
status: string; // TODO: add this in the database // Downloaded, Active
releaseDate: number; // TODO: add this in the database
} }

View File

@ -0,0 +1,15 @@
export const toGigabytes = (input: number) => {
if (input > 1024 ** 3) {
return (input / 1000 ** 3).toFixed(2) + "GB";
} else if (input > 1024 ** 2) {
return (input / 1000 ** 2).toFixed(2) + "MB";
} else if (input > 1024) {
return (input / 1000).toFixed(2) + "KB";
} else {
return input + "B";
}
};
export const formatDownloadPercentage = (input: number) => {
return (input * 100).toFixed(2) + "%";
};