Merge branch 'dev' of https://github.com/janhq/jan into dev
This commit is contained in:
commit
57b4efcf7d
9
ai.menlo.jan.desktop
Normal file
9
ai.menlo.jan.desktop
Normal 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
42
ai.menlo.jan.metainfo.xml
Normal 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>
|
||||||
@ -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
|
||||||
|
|||||||
@ -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}"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user