diff --git a/electron/main.ts b/electron/main.ts index 9a4daacc6..22a5278b7 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,11 @@ -import { app, BrowserWindow, ipcMain, dialog, shell } from "electron"; +import { + app, + BrowserWindow, + ipcMain, + dialog, + shell, + nativeTheme, +} from "electron"; import { readdirSync, writeFileSync } from "fs"; import { resolve, join, extname } from "path"; import { rmdir, unlink, createWriteStream } from "fs"; @@ -36,12 +43,30 @@ app.on("window-all-closed", () => { app.quit(); }); +ipcMain.handle("setNativeThemeLight", () => { + nativeTheme.themeSource = "light"; +}); + +ipcMain.handle("setNativeThemeDark", () => { + nativeTheme.themeSource = "dark"; +}); + +ipcMain.handle("setNativeThemeSystem", () => { + nativeTheme.themeSource = "system"; +}); + function createMainWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, + frame: false, show: false, - backgroundColor: "white", + trafficLightPosition: { + x: 16, + y: 10, + }, + titleBarStyle: "hidden", + vibrancy: "sidebar", webPreferences: { nodeIntegration: true, preload: join(__dirname, "preload.js"), @@ -118,11 +143,13 @@ function handleIPCs() { ipcMain.handle( "invokePluginFunc", async (_event, modulePath, method, ...args) => { - const module = require(/* webpackIgnore: true */ join( - app.getPath("userData"), - "plugins", - modulePath - )); + const module = require( + /* webpackIgnore: true */ join( + app.getPath("userData"), + "plugins", + modulePath + ) + ); requiredModules[modulePath] = module; if (typeof module[method] === "function") { diff --git a/electron/preload.ts b/electron/preload.ts index a52a76c81..dac5aef6f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld("electronAPI", { invokePluginFunc: (plugin: any, method: any, ...args: any[]) => ipcRenderer.invoke("invokePluginFunc", plugin, method, ...args), + setNativeThemeLight: () => ipcRenderer.invoke("setNativeThemeLight"), + + setNativeThemeDark: () => ipcRenderer.invoke("setNativeThemeDark"), + + setNativeThemeSystem: () => ipcRenderer.invoke("setNativeThemeSystem"), + basePlugins: () => ipcRenderer.invoke("basePlugins"), pluginPath: () => ipcRenderer.invoke("pluginPath"), @@ -23,19 +29,27 @@ contextBridge.exposeInMainWorld("electronAPI", { deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath), - installRemotePlugin: (pluginName: string) => ipcRenderer.invoke("installRemotePlugin", pluginName), + installRemotePlugin: (pluginName: string) => + ipcRenderer.invoke("installRemotePlugin", pluginName), - downloadFile: (url: string, path: string) => ipcRenderer.invoke("downloadFile", url, path), + downloadFile: (url: string, path: string) => + ipcRenderer.invoke("downloadFile", url, path), - onFileDownloadUpdate: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), + onFileDownloadUpdate: (callback: any) => + ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback), - onFileDownloadError: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), + onFileDownloadError: (callback: any) => + ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback), - onFileDownloadSuccess: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), + onFileDownloadSuccess: (callback: any) => + ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback), - onAppUpdateDownloadUpdate: (callback: any) => ipcRenderer.on("APP_UPDATE_PROGRESS", callback), + onAppUpdateDownloadUpdate: (callback: any) => + ipcRenderer.on("APP_UPDATE_PROGRESS", callback), - onAppUpdateDownloadError: (callback: any) => ipcRenderer.on("APP_UPDATE_ERROR", callback), + onAppUpdateDownloadError: (callback: any) => + ipcRenderer.on("APP_UPDATE_ERROR", callback), - onAppUpdateDownloadSuccess: (callback: any) => ipcRenderer.on("APP_UPDATE_COMPLETE", callback), + onAppUpdateDownloadSuccess: (callback: any) => + ipcRenderer.on("APP_UPDATE_COMPLETE", callback), }); diff --git a/electron/tests/main.e2e.spec.ts b/electron/tests/main.e2e.spec.ts index 85c3d5c7a..d6df31ca4 100644 --- a/electron/tests/main.e2e.spec.ts +++ b/electron/tests/main.e2e.spec.ts @@ -48,10 +48,8 @@ test("renders the home page", async () => { // Welcome text is available const welcomeText = await page - .locator(".text-5xl", { - hasText: "Welcome,let’s download your first model", - }) + .getByTestId("testid-welcome-title") .first() - .isDisabled(); + .isVisible(); expect(welcomeText).toBe(false); }); diff --git a/electron/tests/my-models.e2e.spec.ts b/electron/tests/my-models.e2e.spec.ts index c52f1a0aa..627612ea8 100644 --- a/electron/tests/my-models.e2e.spec.ts +++ b/electron/tests/my-models.e2e.spec.ts @@ -40,7 +40,7 @@ test("shows my models", async () => { .getByRole("heading") .filter({ hasText: "My Models" }) .first() - .isDisabled(); + .isVisible(); expect(header).toBe(false); // More test cases here... }); diff --git a/electron/tests/navigation.e2e.spec.ts b/electron/tests/navigation.e2e.spec.ts index 554a7b4d7..90220d5db 100644 --- a/electron/tests/navigation.e2e.spec.ts +++ b/electron/tests/navigation.e2e.spec.ts @@ -35,37 +35,13 @@ test.afterAll(async () => { }); test("renders left navigation panel", async () => { - // Chat History section is available - const chatSection = await page - .getByRole("heading") - .filter({ hasText: "CHAT HISTORY" }) - .first() - .isDisabled(); + // Chat section should be there + const chatSection = await page.getByTestId("Chat").first().isVisible(); expect(chatSection).toBe(false); // Home actions - const createBotBtn = await page - .getByRole("button", { name: "Create bot" }) - .first() - .isEnabled(); - const exploreBtn = await page - .getByRole("button", { name: "Explore Models" }) - .first() - .isEnabled(); - const myModelsBtn = await page - .getByTestId("My Models") - .first() - .isEnabled(); - const settingsBtn = await page - .getByTestId("Settings") - .first() - .isEnabled(); - expect( - [ - createBotBtn, - exploreBtn, - myModelsBtn, - settingsBtn, - ].filter((e) => !e).length - ).toBe(0); + const botBtn = await page.getByTestId("Bot").first().isEnabled(); + const myModelsBtn = await page.getByTestId("My Models").first().isEnabled(); + const settingsBtn = await page.getByTestId("Settings").first().isEnabled(); + expect([botBtn, myModelsBtn, settingsBtn].filter((e) => !e).length).toBe(0); }); diff --git a/electron/tests/settings.e2e.spec.ts b/electron/tests/settings.e2e.spec.ts index dacefd380..2f8d6465b 100644 --- a/electron/tests/settings.e2e.spec.ts +++ b/electron/tests/settings.e2e.spec.ts @@ -36,7 +36,5 @@ test.afterAll(async () => { test("shows settings", async () => { await page.getByTestId("Settings").first().click(); - - const pluginList = await page.getByTestId("plugin-item").count(); - expect(pluginList).toBe(4); + await page.getByTestId("testid-setting-description").isVisible(); }); diff --git a/plugins/data-plugin/package.json b/plugins/data-plugin/package.json index df62a56ba..5ada2f7ed 100644 --- a/plugins/data-plugin/package.json +++ b/plugins/data-plugin/package.json @@ -43,9 +43,5 @@ "@janhq/core": "^0.1.6", "pouchdb-find": "^8.0.1", "pouchdb-node": "^8.0.1" - }, - "bundleDependencies": [ - "pouchdb-node", - "pouchdb-find" - ] + } } diff --git a/plugins/inference-plugin/index.ts b/plugins/inference-plugin/index.ts index d8f73a218..04ed4b0f0 100644 --- a/plugins/inference-plugin/index.ts +++ b/plugins/inference-plugin/index.ts @@ -9,13 +9,17 @@ import { } from "@janhq/core"; import { Observable } from "rxjs"; -const initModel = async (product) => invokePluginFunc(MODULE_PATH, "initModel", product); +const initModel = async (product) => + invokePluginFunc(MODULE_PATH, "initModel", product); const stopModel = () => { invokePluginFunc(MODULE_PATH, "killSubprocess"); }; -function requestInference(recentMessages: any[], bot?: any): Observable { +function requestInference( + recentMessages: any[], + bot?: any +): Observable { return new Observable((subscriber) => { const requestBody = JSON.stringify({ messages: recentMessages, @@ -69,10 +73,15 @@ function requestInference(recentMessages: any[], bot?: any): Observable async function retrieveLastTenMessages(conversationId: string, bot?: any) { // TODO: Common collections should be able to access via core functions instead of store - const messageHistory = (await store.findMany("messages", { conversationId }, [{ createdAt: "asc" }])) ?? []; + const messageHistory = + (await store.findMany("messages", { conversationId }, [ + { createdAt: "asc" }, + ])) ?? []; let recentMessages = messageHistory - .filter((e) => e.message !== "" && (e.user === "user" || e.user === "assistant")) + .filter( + (e) => e.message !== "" && (e.user === "user" || e.user === "assistant") + ) .slice(-9) .map((message) => ({ content: message.message.trim(), @@ -81,10 +90,13 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) { if (bot && bot.systemPrompt) { // append bot's system prompt - recentMessages = [{ - content: `[INST] ${bot.systemPrompt}`, - role: 'system' - },...recentMessages]; + recentMessages = [ + { + content: `[INST] ${bot.systemPrompt}`, + role: "system", + }, + ...recentMessages, + ]; } console.debug(`Last 10 messages: ${JSON.stringify(recentMessages, null, 2)}`); @@ -93,13 +105,19 @@ async function retrieveLastTenMessages(conversationId: string, bot?: any) { } async function handleMessageRequest(data: NewMessageRequest) { - const conversation = await store.findOne("conversations", data.conversationId); + const conversation = await store.findOne( + "conversations", + data.conversationId + ); let bot = undefined; if (conversation.botId != null) { bot = await store.findOne("bots", conversation.botId); } - - const recentMessages = await retrieveLastTenMessages(data.conversationId, bot); + + const recentMessages = await retrieveLastTenMessages( + data.conversationId, + bot + ); const message = { ...data, message: "", @@ -124,7 +142,8 @@ async function handleMessageRequest(data: NewMessageRequest) { await store.updateOne("messages", message._id, message); }, error: async (err) => { - message.message = message.message.trim() + "\n" + "Error occurred: " + err; + message.message = + message.message.trim() + "\n" + "Error occurred: " + err; // TODO: Common collections should be able to access via core functions instead of store await store.updateOne("messages", message._id, message); }, @@ -140,7 +159,10 @@ async function inferenceRequest(data: NewMessageRequest): Promise { }; return new Promise(async (resolve, reject) => { const recentMessages = await retrieveLastTenMessages(data.conversationId); - requestInference([...recentMessages, { role: "user", content: data.message }]).subscribe({ + requestInference([ + ...recentMessages, + { role: "user", content: data.message }, + ]).subscribe({ next: (content) => { message.message = content; }, @@ -166,5 +188,9 @@ export function init({ register }) { register(PluginService.OnStart, PLUGIN_NAME, onStart); register(InferenceService.InitModel, initModel.name, initModel); register(InferenceService.StopModel, stopModel.name, stopModel); - register(InferenceService.InferenceRequest, inferenceRequest.name, inferenceRequest); + register( + InferenceService.InferenceRequest, + inferenceRequest.name, + inferenceRequest + ); } diff --git a/web/app/_components/ActiveModelTable/index.tsx b/web/app/_components/ActiveModelTable/index.tsx index eae2ed565..3a477ba5c 100644 --- a/web/app/_components/ActiveModelTable/index.tsx +++ b/web/app/_components/ActiveModelTable/index.tsx @@ -1,7 +1,7 @@ import { useAtomValue } from 'jotai' import React from 'react' import ModelTable from '../ModelTable' -import { activeAssistantModelAtom } from '@/_helpers/atoms/Model.atom' +import { activeAssistantModelAtom } from '@helpers/atoms/Model.atom' const ActiveModelTable: React.FC = () => { const activeModel = useAtomValue(activeAssistantModelAtom) diff --git a/web/app/_components/AvailableModelCard/index.tsx b/web/app/_components/AvailableModelCard/index.tsx index 8d69b2880..9773dfb85 100644 --- a/web/app/_components/AvailableModelCard/index.tsx +++ b/web/app/_components/AvailableModelCard/index.tsx @@ -2,8 +2,7 @@ import DownloadModelContent from '../DownloadModelContent' import ModelDownloadButton from '../ModelDownloadButton' import ModelDownloadingButton from '../ModelDownloadingButton' import { useAtomValue } from 'jotai' -import { modelDownloadStateAtom } from '@/_helpers/atoms/DownloadState.atom' -import { AssistantModel } from '@/_models/AssistantModel' +import { modelDownloadStateAtom } from '@helpers/atoms/DownloadState.atom' type Props = { model: AssistantModel diff --git a/web/app/_components/Avatar/index.tsx b/web/app/_components/Avatar/index.tsx index 4b7259e3e..51e5e1d59 100644 --- a/web/app/_components/Avatar/index.tsx +++ b/web/app/_components/Avatar/index.tsx @@ -7,7 +7,7 @@ type Props = { const Avatar: React.FC = ({ allowEdit = false }) => (
- + { const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) @@ -11,7 +11,7 @@ const BasicPromptAccessories: React.FC = () => { const shouldShowAdvancedPrompt = false return ( -
+
{/* Add future accessories here, e.g upload a file */}
diff --git a/web/app/_components/BasicPromptButton/index.tsx b/web/app/_components/BasicPromptButton/index.tsx index 58bc6abc4..3e9bd0ec8 100644 --- a/web/app/_components/BasicPromptButton/index.tsx +++ b/web/app/_components/BasicPromptButton/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useSetAtom } from 'jotai' import { ChevronLeftIcon } from '@heroicons/react/24/outline' -import { showingAdvancedPromptAtom } from '@/_helpers/atoms/Modal.atom' +import { showingAdvancedPromptAtom } from '@helpers/atoms/Modal.atom' const BasicPromptButton: React.FC = () => { const setShowingAdvancedPrompt = useSetAtom(showingAdvancedPromptAtom) diff --git a/web/app/_components/BasicPromptInput/index.tsx b/web/app/_components/BasicPromptInput/index.tsx index d6e2f2d83..bb2eeb8e0 100644 --- a/web/app/_components/BasicPromptInput/index.tsx +++ b/web/app/_components/BasicPromptInput/index.tsx @@ -1,11 +1,11 @@ 'use client' -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 { 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 { useAtom, useAtomValue } from 'jotai' import { ChangeEvent, useEffect, useRef } from 'react' @@ -68,7 +68,7 @@ const BasicPromptInput: React.FC = () => { } return ( -
+