diff --git a/.github/scripts/auto-sign.sh b/.github/scripts/auto-sign.sh index a2130e791..e7ea49d40 100755 --- a/.github/scripts/auto-sign.sh +++ b/.github/scripts/auto-sign.sh @@ -7,6 +7,6 @@ if [[ -z "$APP_PATH" ]] || [[ -z "$DEVELOPER_ID" ]]; then fi # If both variables are set, execute the following commands -find "$APP_PATH" \( -type f -perm +111 -o -name "*.node" \) -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \; +find "$APP_PATH" \( -type f -perm +111 -o -name "*.node" \) -exec codesign --force -s "$DEVELOPER_ID" --options=runtime {} \; -find "$APP_PATH" -type f -name "*.o" -exec codesign -s "$DEVELOPER_ID" --options=runtime {} \; +find "$APP_PATH" -type f -name "*.o" -exec codesign --force -s "$DEVELOPER_ID" --options=runtime {} \; diff --git a/README.md b/README.md index 8a4c03098..496bbd434 100644 --- a/README.md +++ b/README.md @@ -43,31 +43,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Stable (Recommended) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage @@ -76,31 +76,31 @@ Jan is an open-source ChatGPT alternative that runs 100% offline on your compute Experimental (Nightly Build) - + jan.exe - + Intel - + M1/M2 - + jan.deb - + jan.AppImage diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 79d675c7a..43334c988 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -1,6 +1,5 @@ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion - require("dotenv").config(); const darkCodeTheme = require("prism-react-renderer/themes/dracula"); @@ -105,6 +104,9 @@ const config = { { from: "/troubleshooting/undefined-issue/", to: "/guides/error-codes/undefined-issue/", + }, { + from: "/install/", + to: "/guides/install/", }, ], }, diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 4593f4f94..deab47c88 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -33,16 +33,24 @@ /* Dark mode styles based on Docusaurus dark theme */ [data-theme='dark'] .head_Menu div { + font-weight: bold; background-color: var(--ifm-background-color); color: var(--ifm-font-color-base); + margin-left: 0.7rem; + font-size: larger; } [data-theme='dark'] .head_Menu li { + font-weight: normal; background-color: var(--ifm-background-color); + margin-bottom: 5px; color: var(--ifm-font-color-base); } [data-theme='dark'] .head_SubMenu div { + font-weight: normal; background-color: var(--ifm-background-color); color: var(--ifm-font-color-base); + margin-left: 0rem; + font-size: medium; } diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts index 3f52c401e..5ea261e54 100644 --- a/electron/handlers/update.ts +++ b/electron/handlers/update.ts @@ -7,6 +7,7 @@ import { autoUpdater, } from 'electron-updater' import { AppEvent } from '@janhq/core' +import { trayManager } from '../managers/tray' export let waitingToInstallVersion: string | undefined = undefined @@ -22,6 +23,7 @@ export function handleAppUpdates() { message: 'Would you like to download and install it now?', buttons: ['Download', 'Later'], }) + trayManager.destroyCurrentTray() if (action.response === 0) await autoUpdater.downloadUpdate() }) diff --git a/electron/main.ts b/electron/main.ts index 21f95cd00..78577ac68 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, Menu, Tray } from 'electron' +import { app, BrowserWindow, Tray } from 'electron' import { join } from 'path' /** @@ -27,6 +27,7 @@ import { setupReactDevTool } from './utils/dev' import { cleanLogs } from './utils/log' import { registerShortcut } from './utils/selectedText' +import { trayManager } from './managers/tray' const preloadPath = join(__dirname, 'preload.js') const rendererPath = join(__dirname, '..', 'renderer') @@ -38,6 +39,8 @@ const quickAskUrl = `${mainUrl}/search` const quickAskHotKey = 'CommandOrControl+J' +const gotTheLock = app.requestSingleInstanceLock() + app .whenReady() .then(setupReactDevTool) @@ -48,37 +51,26 @@ app .then(setupMenu) .then(handleIPCs) .then(handleAppUpdates) - .then(createQuickAskWindow) + .then(() => process.env.CI !== 'e2e' && createQuickAskWindow()) .then(createMainWindow) .then(() => { if (!app.isPackaged) { windowManager.mainWindow?.webContents.openDevTools() } }) - .then(() => { - const iconPath = join(app.getAppPath(), 'icons', 'icon-tray.png') - const tray = new Tray(iconPath) - tray.setToolTip(app.getName()) - - const contextMenu = Menu.buildFromTemplate([ - { - label: 'Open Jan', - type: 'normal', - click: () => windowManager.showMainWindow(), - }, - { - label: 'Open Quick Ask', - type: 'normal', - click: () => windowManager.showQuickAskWindow(), - }, - { label: 'Quit', type: 'normal', click: () => app.quit() }, - ]) - tray.setContextMenu(contextMenu) - }) + .then(() => process.env.CI !== 'e2e' && trayManager.createSystemTray()) .then(() => { log(`Version: ${app.getVersion()}`) }) .then(() => { + if (!gotTheLock) { + app.quit() + } else { + app.on('second-instance', (_event, _commandLine, _workingDirectory) => { + // Someone tried to run a second instance, we should focus our window. + windowManager.showMainWindow() + }) + } app.on('activate', () => { if (!BrowserWindow.getAllWindows().length) { createMainWindow() @@ -91,6 +83,10 @@ app.on('ready', () => { registerGlobalShortcuts() }) +app.on('before-quit', function (evt) { + trayManager.destroyCurrentTray() +}) + app.once('quit', () => { cleanUpAndQuit() }) diff --git a/electron/managers/tray.ts b/electron/managers/tray.ts new file mode 100644 index 000000000..18661e58e --- /dev/null +++ b/electron/managers/tray.ts @@ -0,0 +1,38 @@ +import { join } from 'path' +import { Tray, app, Menu } from 'electron' +import { windowManager } from '../managers/window' + +class TrayManager { + currentTray: Tray | undefined + + createSystemTray = () => { + if (this.currentTray) { + return + } + const iconPath = join(app.getAppPath(), 'icons', 'icon-tray.png') + const tray = new Tray(iconPath) + tray.setToolTip(app.getName()) + + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Open Jan', + type: 'normal', + click: () => windowManager.showMainWindow(), + }, + { + label: 'Open Quick Ask', + type: 'normal', + click: () => windowManager.showQuickAskWindow(), + }, + { label: 'Quit', type: 'normal', click: () => app.quit() }, + ]) + tray.setContextMenu(contextMenu) + } + + destroyCurrentTray() { + this.currentTray?.destroy() + this.currentTray = undefined + } +} + +export const trayManager = new TrayManager() diff --git a/electron/managers/window.ts b/electron/managers/window.ts index 796a5d54a..eed80c37c 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -101,6 +101,7 @@ class WindowManager { expandQuickAskWindow(heightOffset: number): void { const width = quickAskWindowConfig.width! const height = quickAskWindowConfig.height! + heightOffset + this._quickAskWindow?.setMinimumSize(width, height) this._quickAskWindow?.setSize(width, height, true) } diff --git a/electron/package.json b/electron/package.json index 93c30682c..e09e0daf2 100644 --- a/electron/package.json +++ b/electron/package.json @@ -41,7 +41,6 @@ "notarize": { "teamId": "F8AH6NHVY5" }, - "icon": "icons/icon.png" }, "linux": { @@ -92,7 +91,7 @@ "request": "^2.88.2", "request-progress": "^3.0.0", "ulid": "^2.3.0", - "@hurdlegroup/robotjs": "^0.11.4" + "@nut-tree/nut-js": "^4.0.0" }, "devDependencies": { "@electron/notarize": "^2.1.0", diff --git a/electron/utils/selectedText.ts b/electron/utils/selectedText.ts index 6b2349725..a39e331a9 100644 --- a/electron/utils/selectedText.ts +++ b/electron/utils/selectedText.ts @@ -1,19 +1,24 @@ -import { clipboard, globalShortcut } from "electron"; -import { keyTap, keys } from "@hurdlegroup/robotjs"; +import { clipboard, globalShortcut } from 'electron' +import { keyboard, Key } from '@nut-tree/nut-js' /** * Gets selected text by synthesizing the keyboard shortcut * "CommandOrControl+c" then reading text from the clipboard */ export const getSelectedText = async () => { - const currentClipboardContent = clipboard.readText(); // preserve clipboard content - clipboard.clear(); - keyTap("c" as keys, process.platform === "darwin" ? "command" : "control"); - await new Promise((resolve) => setTimeout(resolve, 200)); // add a delay before checking clipboard - const selectedText = clipboard.readText(); - clipboard.writeText(currentClipboardContent); - return selectedText; -}; + const currentClipboardContent = clipboard.readText() // preserve clipboard content + clipboard.clear() + const hotkeys: Key[] = [ + process.platform === 'darwin' ? Key.LeftCmd : Key.LeftControl, + Key.C, + ] + await keyboard.pressKey(...hotkeys) + await keyboard.releaseKey(...hotkeys) + await new Promise((resolve) => setTimeout(resolve, 200)) // add a delay before checking clipboard + const selectedText = clipboard.readText() + clipboard.writeText(currentClipboardContent) + return selectedText +} /** * Registers a global shortcut of `accelerator`. The `callback` is called @@ -26,14 +31,14 @@ export const registerShortcut = ( callback: (selectedText: string) => void ) => { return globalShortcut.register(accelerator, async () => { - callback(await getSelectedText()); - }); -}; + callback(await getSelectedText()) + }) +} /** * Unregisters a global shortcut of `accelerator` and * is equivalent to electron.globalShortcut.unregister */ export const unregisterShortcut = (accelerator: Electron.Accelerator) => { - globalShortcut.unregister(accelerator); -}; \ No newline at end of file + globalShortcut.unregister(accelerator) +} diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index 9e88e763a..11a57a598 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -79,6 +79,8 @@ export default function useSendChatMessage() { const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) const activeThreadRef = useRef() + const selectedModelRef = useRef() + useEffect(() => { modelRef.current = activeModel }, [activeModel]) @@ -91,6 +93,10 @@ export default function useSendChatMessage() { activeThreadRef.current = activeThread }, [activeThread]) + useEffect(() => { + selectedModelRef.current = selectedModel + }, [selectedModel]) + const resendChatMessage = async (currentMessage: ThreadMessage) => { if (!activeThreadRef.current) { console.error('No active thread') @@ -128,11 +134,13 @@ export default function useSendChatMessage() { type: MessageRequestType.Thread, messages: messages, threadId: activeThreadRef.current.id, - model: activeThreadRef.current.assistants[0].model ?? selectedModel, + model: + activeThreadRef.current.assistants[0].model ?? selectedModelRef.current, } const modelId = - selectedModel?.id ?? activeThreadRef.current.assistants[0].model.id + selectedModelRef.current?.id ?? + activeThreadRef.current.assistants[0].model.id if (modelRef.current?.id !== modelId) { setQueuedMessage(true) @@ -213,7 +221,7 @@ export default function useSendChatMessage() { { role: ChatCompletionRole.User, content: - selectedModel && base64Blob + selectedModelRef.current && base64Blob ? [ { type: ChatCompletionMessageContentType.Text, @@ -242,7 +250,7 @@ export default function useSendChatMessage() { ) let modelRequest = - selectedModel ?? activeThreadRef.current.assistants[0].model + selectedModelRef?.current ?? activeThreadRef.current.assistants[0].model if (runtimeParams.stream == null) { runtimeParams.stream = true } @@ -344,7 +352,8 @@ export default function useSendChatMessage() { ?.addNewMessage(threadMessage) const modelId = - selectedModel?.id ?? activeThreadRef.current.assistants[0].model.id + selectedModelRef.current?.id ?? + activeThreadRef.current.assistants[0].model.id if (modelRef.current?.id !== modelId) { setQueuedMessage(true)