diff --git a/ai.menlo.jan.desktop b/ai.menlo.jan.desktop new file mode 100644 index 000000000..779bffb27 --- /dev/null +++ b/ai.menlo.jan.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Jan +Comment=Local AI Assistant that runs 100% offline +Exec=run.sh +Icon=ai.menlo.jan +Type=Application +Categories=Development; +Keywords=AI;Assistant;LLM;ChatGPT;Local;Offline; +StartupNotify=true \ No newline at end of file diff --git a/ai.menlo.jan.metainfo.xml b/ai.menlo.jan.metainfo.xml new file mode 100644 index 000000000..713471d26 --- /dev/null +++ b/ai.menlo.jan.metainfo.xml @@ -0,0 +1,42 @@ + + + ai.menlo.jan + FSFAP + AGPL-3.0-only + Jan + Local AI Assistant that runs 100% offline on your device + + +

+ Jan is a ChatGPT-alternative that runs 100% offline on your device. Our goal is to make it easy for anyone to download and run LLMs and use AI with full control and privacy. +

+

Features:

+ +
+ + ai.menlo.jan.desktop + + + + https://catalog.jan.ai/flatpak/demo.gif + + + + https://jan.ai/ + https://github.com/janhq/jan/issues + + + + + + +

Latest stable release of Jan AI

+
+
+
+
\ No newline at end of file diff --git a/extensions/inference-cortex-extension/download.bat b/extensions/inference-cortex-extension/download.bat index 0e7eef20e..00e37cd19 100644 --- a/extensions/inference-cortex-extension/download.bat +++ b/extensions/inference-cortex-extension/download.bat @@ -2,7 +2,7 @@ set BIN_PATH=./bin set SHARED_PATH=./../../electron/shared set /p CORTEX_VERSION=<./bin/version.txt -set ENGINE_VERSION=0.1.42 +set ENGINE_VERSION=0.1.43 @REM Download cortex.llamacpp binaries set DOWNLOAD_URL=https://github.com/janhq/cortex.llamacpp/releases/download/v%ENGINE_VERSION%/cortex.llamacpp-%ENGINE_VERSION%-windows-amd64 diff --git a/extensions/inference-cortex-extension/download.sh b/extensions/inference-cortex-extension/download.sh index 0ccbdcb1a..9d35bd117 100755 --- a/extensions/inference-cortex-extension/download.sh +++ b/extensions/inference-cortex-extension/download.sh @@ -2,7 +2,7 @@ # Read CORTEX_VERSION CORTEX_VERSION=$(cat ./bin/version.txt) -ENGINE_VERSION=0.1.42 +ENGINE_VERSION=0.1.43 CORTEX_RELEASE_URL="https://github.com/janhq/cortex.cpp/releases/download" ENGINE_DOWNLOAD_URL="https://github.com/janhq/cortex.llamacpp/releases/download/v${ENGINE_VERSION}/cortex.llamacpp-${ENGINE_VERSION}" CUDA_DOWNLOAD_URL="https://github.com/janhq/cortex.llamacpp/releases/download/v${ENGINE_VERSION}" diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx index 2049fedf6..e6ee94c9a 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, ClipboardEvent } from 'react' import { MessageStatus } from '@janhq/core' import { useAtom, useAtomValue } from 'jotai' -import { BaseEditor, createEditor, Editor, Transforms } from 'slate' +import { BaseEditor, createEditor, Editor, Range, Transforms } from 'slate' import { withHistory } from 'slate-history' // Import withHistory import { Editable, @@ -186,10 +186,6 @@ const RichTextEditor = ({ : '40px' textareaRef.current.style.height = textareaRef.current.scrollHeight + 2 + 'px' - textareaRef.current?.scrollTo({ - top: textareaRef.current.scrollHeight, - behavior: 'instant', - }) textareaRef.current.style.overflow = textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' } @@ -302,6 +298,7 @@ const RichTextEditor = ({ return decorate(entry) }} renderLeaf={renderLeaf} // Pass the renderLeaf function + scrollSelectionIntoView={scrollSelectionIntoView} onKeyDown={handleKeyDown} onPaste={handlePaste} // Add the custom paste handler className={twMerge( @@ -317,6 +314,83 @@ const RichTextEditor = ({ /> ) + + function scrollSelectionIntoView( + editor: ReactEditor, + domRange: globalThis.Range + ) { + // This was affecting the selection of multiple blocks and dragging behavior, + // so enabled only if the selection has been collapsed. + if (editor.selection && Range.isExpanded(editor.selection)) return + + const minTop = 80 // sticky header height + + const leafEl = domRange.startContainer.parentElement + const scrollParent = getScrollParent(leafEl) + + // Check if browser supports getBoundingClientRect + if (typeof domRange.getBoundingClientRect !== 'function') return + + const { top: elementTop, height: elementHeight } = + domRange.getBoundingClientRect() + const { height: parentHeight } = scrollParent.getBoundingClientRect() + + const isChildAboveViewport = elementTop < minTop + const isChildBelowViewport = elementTop + elementHeight > parentHeight + + if (isChildAboveViewport && isChildBelowViewport) { + // Child spans through all visible area which means it's already in view. + return + } + + if (isChildAboveViewport) { + const y = scrollParent.scrollTop + elementTop - minTop + scrollParent.scroll({ left: scrollParent.scrollLeft, top: y }) + return + } + + if (isChildBelowViewport) { + const y = Math.min( + scrollParent.scrollTop + elementTop - minTop, + scrollParent.scrollTop + elementTop + elementHeight - parentHeight + ) + scrollParent.scroll({ left: scrollParent.scrollLeft, top: y }) + } + } + + function getScrollParent(element: any) { + const elementStyle = window.getComputedStyle(element) + const excludeStaticParent = elementStyle.position === 'absolute' + + if (elementStyle.position === 'fixed') { + return document.body + } + + let parent = element + + while (parent) { + const parentStyle = window.getComputedStyle(parent) + + if (parentStyle.position !== 'static' || !excludeStaticParent) { + const overflowAttributes = [ + parentStyle.overflow, + parentStyle.overflowY, + parentStyle.overflowX, + ] + + if ( + overflowAttributes.includes('auto') || + overflowAttributes.includes('hidden') + ) { + return parent + } + } + + parent = parent.parentElement + } + + return document.documentElement + } } export default RichTextEditor diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index 990d24c7a..06e647008 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -59,7 +59,7 @@ const ChatInput = () => { const activeThreadId = useAtomValue(getActiveThreadIdAtom) const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) - const [showAttacmentMenus, setShowAttacmentMenus] = useState(false) + const [showAttachmentMenus, setShowAttachmentMenus] = useState(false) const textareaRef = useRef(null) const fileInputRef = useRef(null) const imageInputRef = useRef(null) @@ -73,7 +73,9 @@ const ChatInput = () => { activeTabThreadRightPanelAtom ) - const refAttachmentMenus = useClickOutside(() => setShowAttacmentMenus(false)) + const refAttachmentMenus = useClickOutside(() => + setShowAttachmentMenus(false) + ) const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) useEffect(() => { @@ -169,7 +171,7 @@ const ChatInput = () => { ) { e.stopPropagation() } else { - setShowAttacmentMenus(!showAttacmentMenus) + setShowAttachmentMenus(!showAttachmentMenus) } }} > @@ -214,7 +216,7 @@ const ChatInput = () => { /> )} - {showAttacmentMenus && ( + {showAttachmentMenus && (
{ onClick={() => { if (activeAssistant?.model.settings?.vision_model) { imageInputRef.current?.click() - setShowAttacmentMenus(false) + setShowAttachmentMenus(false) } }} > @@ -254,7 +256,7 @@ const ChatInput = () => { onClick={() => { if (isModelSupportRagAndTools) { fileInputRef.current?.click() - setShowAttacmentMenus(false) + setShowAttachmentMenus(false) } }} > diff --git a/web/screens/Thread/index.test.tsx b/web/screens/Thread/index.test.tsx index f3227bfbe..5277e82b5 100644 --- a/web/screens/Thread/index.test.tsx +++ b/web/screens/Thread/index.test.tsx @@ -1,13 +1,13 @@ import React from 'react' -import { render, screen } from '@testing-library/react' +import { render } from '@testing-library/react' import ThreadScreen from './index' import { useStarterScreen } from '../../hooks/useStarterScreen' import '@testing-library/jest-dom' global.ResizeObserver = class { - observe() { } - unobserve() { } - disconnect() { } + observe() {} + unobserve() {} + disconnect() {} } // Mock the useStarterScreen hook jest.mock('@/hooks/useStarterScreen') @@ -17,7 +17,7 @@ global.API_BASE_URL = 'http://localhost:3000' describe('ThreadScreen', () => { it('renders OnDeviceStarterScreen when isShowStarterScreen is true', () => { - ; (useStarterScreen as jest.Mock).mockReturnValue({ + ;(useStarterScreen as jest.Mock).mockReturnValue({ isShowStarterScreen: true, extensionHasSettings: false, }) @@ -27,7 +27,7 @@ describe('ThreadScreen', () => { }) it('renders Thread panels when isShowStarterScreen is false', () => { - ; (useStarterScreen as jest.Mock).mockReturnValue({ + ;(useStarterScreen as jest.Mock).mockReturnValue({ isShowStarterScreen: false, extensionHasSettings: false, })