Merge branch 'dev' of https://github.com/janhq/jan into dev

This commit is contained in:
Ashley 2025-01-05 20:56:28 +07:00
commit 57b4efcf7d
7 changed files with 146 additions and 19 deletions

9
ai.menlo.jan.desktop Normal file
View File

@ -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

42
ai.menlo.jan.metainfo.xml Normal file
View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>ai.menlo.jan</id>
<metadata_license>FSFAP</metadata_license>
<project_license>AGPL-3.0-only</project_license>
<name>Jan</name>
<summary>Local AI Assistant that runs 100% offline on your device</summary>
<description>
<p>
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.
</p>
<p>Features:</p>
<ul>
<li>Model Library with popular LLMs like Llama, Gemma, Mistral, or Qwen</li>
<li>Connect to Remote AI APIs like Groq and OpenRouter</li>
<li>Local API Server with OpenAI-equivalent API</li>
<li>Extensions for customizing Jan</li>
</ul>
</description>
<launchable type="desktop-id">ai.menlo.jan.desktop</launchable>
<screenshots>
<screenshot type="default">
<image>https://catalog.jan.ai/flatpak/demo.gif</image>
</screenshot>
</screenshots>
<url type="homepage">https://jan.ai/</url>
<url type="bugtracker">https://github.com/janhq/jan/issues</url>
<content_rating type="oars-1.1" />
<releases>
<release version="0.5.12" date="2024-01-02">
<description>
<p>Latest stable release of Jan AI</p>
</description>
</release>
</releases>
</component>

View File

@ -2,7 +2,7 @@
set BIN_PATH=./bin set BIN_PATH=./bin
set SHARED_PATH=./../../electron/shared set SHARED_PATH=./../../electron/shared
set /p CORTEX_VERSION=<./bin/version.txt set /p CORTEX_VERSION=<./bin/version.txt
set ENGINE_VERSION=0.1.42 set ENGINE_VERSION=0.1.43
@REM Download cortex.llamacpp binaries @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 set DOWNLOAD_URL=https://github.com/janhq/cortex.llamacpp/releases/download/v%ENGINE_VERSION%/cortex.llamacpp-%ENGINE_VERSION%-windows-amd64

View File

@ -2,7 +2,7 @@
# Read CORTEX_VERSION # Read CORTEX_VERSION
CORTEX_VERSION=$(cat ./bin/version.txt) 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" 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}" 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}" CUDA_DOWNLOAD_URL="https://github.com/janhq/cortex.llamacpp/releases/download/v${ENGINE_VERSION}"

View File

@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, ClipboardEvent } from 'react'
import { MessageStatus } from '@janhq/core' import { MessageStatus } from '@janhq/core'
import { useAtom, useAtomValue } from 'jotai' 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 { withHistory } from 'slate-history' // Import withHistory
import { import {
Editable, Editable,
@ -186,10 +186,6 @@ const RichTextEditor = ({
: '40px' : '40px'
textareaRef.current.style.height = textareaRef.current.style.height =
textareaRef.current.scrollHeight + 2 + 'px' textareaRef.current.scrollHeight + 2 + 'px'
textareaRef.current?.scrollTo({
top: textareaRef.current.scrollHeight,
behavior: 'instant',
})
textareaRef.current.style.overflow = textareaRef.current.style.overflow =
textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden' textareaRef.current.clientHeight >= 390 ? 'auto' : 'hidden'
} }
@ -302,6 +298,7 @@ const RichTextEditor = ({
return decorate(entry) return decorate(entry)
}} }}
renderLeaf={renderLeaf} // Pass the renderLeaf function renderLeaf={renderLeaf} // Pass the renderLeaf function
scrollSelectionIntoView={scrollSelectionIntoView}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste} // Add the custom paste handler onPaste={handlePaste} // Add the custom paste handler
className={twMerge( className={twMerge(
@ -317,6 +314,83 @@ const RichTextEditor = ({
/> />
</Slate> </Slate>
) )
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 export default RichTextEditor

View File

@ -59,7 +59,7 @@ const ChatInput = () => {
const activeThreadId = useAtomValue(getActiveThreadIdAtom) const activeThreadId = useAtomValue(getActiveThreadIdAtom)
const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) const [fileUpload, setFileUpload] = useAtom(fileUploadAtom)
const [showAttacmentMenus, setShowAttacmentMenus] = useState(false) const [showAttachmentMenus, setShowAttachmentMenus] = useState(false)
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const imageInputRef = useRef<HTMLInputElement>(null) const imageInputRef = useRef<HTMLInputElement>(null)
@ -73,7 +73,9 @@ const ChatInput = () => {
activeTabThreadRightPanelAtom activeTabThreadRightPanelAtom
) )
const refAttachmentMenus = useClickOutside(() => setShowAttacmentMenus(false)) const refAttachmentMenus = useClickOutside(() =>
setShowAttachmentMenus(false)
)
const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom)
useEffect(() => { useEffect(() => {
@ -169,7 +171,7 @@ const ChatInput = () => {
) { ) {
e.stopPropagation() e.stopPropagation()
} else { } else {
setShowAttacmentMenus(!showAttacmentMenus) setShowAttachmentMenus(!showAttachmentMenus)
} }
}} }}
> >
@ -214,7 +216,7 @@ const ChatInput = () => {
/> />
)} )}
{showAttacmentMenus && ( {showAttachmentMenus && (
<div <div
ref={refAttachmentMenus} ref={refAttachmentMenus}
className={twMerge( className={twMerge(
@ -234,7 +236,7 @@ const ChatInput = () => {
onClick={() => { onClick={() => {
if (activeAssistant?.model.settings?.vision_model) { if (activeAssistant?.model.settings?.vision_model) {
imageInputRef.current?.click() imageInputRef.current?.click()
setShowAttacmentMenus(false) setShowAttachmentMenus(false)
} }
}} }}
> >
@ -254,7 +256,7 @@ const ChatInput = () => {
onClick={() => { onClick={() => {
if (isModelSupportRagAndTools) { if (isModelSupportRagAndTools) {
fileInputRef.current?.click() fileInputRef.current?.click()
setShowAttacmentMenus(false) setShowAttachmentMenus(false)
} }
}} }}
> >

View File

@ -1,13 +1,13 @@
import React from 'react' import React from 'react'
import { render, screen } from '@testing-library/react' import { render } from '@testing-library/react'
import ThreadScreen from './index' import ThreadScreen from './index'
import { useStarterScreen } from '../../hooks/useStarterScreen' import { useStarterScreen } from '../../hooks/useStarterScreen'
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
global.ResizeObserver = class { global.ResizeObserver = class {
observe() { } observe() {}
unobserve() { } unobserve() {}
disconnect() { } disconnect() {}
} }
// Mock the useStarterScreen hook // Mock the useStarterScreen hook
jest.mock('@/hooks/useStarterScreen') jest.mock('@/hooks/useStarterScreen')
@ -17,7 +17,7 @@ global.API_BASE_URL = 'http://localhost:3000'
describe('ThreadScreen', () => { describe('ThreadScreen', () => {
it('renders OnDeviceStarterScreen when isShowStarterScreen is true', () => { it('renders OnDeviceStarterScreen when isShowStarterScreen is true', () => {
; (useStarterScreen as jest.Mock).mockReturnValue({ ;(useStarterScreen as jest.Mock).mockReturnValue({
isShowStarterScreen: true, isShowStarterScreen: true,
extensionHasSettings: false, extensionHasSettings: false,
}) })
@ -27,7 +27,7 @@ describe('ThreadScreen', () => {
}) })
it('renders Thread panels when isShowStarterScreen is false', () => { it('renders Thread panels when isShowStarterScreen is false', () => {
; (useStarterScreen as jest.Mock).mockReturnValue({ ;(useStarterScreen as jest.Mock).mockReturnValue({
isShowStarterScreen: false, isShowStarterScreen: false,
extensionHasSettings: false, extensionHasSettings: false,
}) })