Compare commits

...

17 Commits
dev ... main

Author SHA1 Message Date
Louis
91a1fb5233
chore: sync Release/v0.5.16 into main (#4834)
* feat: Jan Hub Revamp (#4491)

* feat: model hub revamp UI

* chore: model description - consistent markdown css

* chore: add model versions dropdown

* chore: integrate APIs - model sources

* chore: update model display name

* chore: lint fix

* chore: page transition animation

* feat: model search dropdown - deeplink

* chore: bump cortex version

* chore: add remote model sources

* chore: model download state

* chore: fix model metadata label

* chore: polish model detail page markdown

* test: fix test cases

* chore: initialize default Hub model sources

* chore: fix model stats

* chore: clean up click outside and inside hooks

* feat: change hub banner

* chore: lint fix

* chore: fix css long model id

* chore: sync cortex engine version (#4536)

* chore: rotate model hub banner on app launch until set (#4542)

* blog: add DeepSeek R1 local installation guide (#4552)

* docs: add DeepSeek R1 local installation guide

- Add comprehensive guide for running DeepSeek R1 locally
- Include step-by-step instructions with screenshots
- Add VRAM requirements and model selection guide
- Include system prompt setup instructions

* docs: add comprehensive guide on running AI models locally

* docs: address PR feedback for DeepSeek R1 and local AI guides

- Improve language and terminology throughout
- Add Linux support information
- Enhance technical explanations
- Update introduction for better flow
- Fix parameters section in run-ai-models-locally.mdx

---------

Co-authored-by: Louis <louis@jan.ai>

* enhancement: code snippet color and bakground should depend on native theme (#4566)

* chore: correct conversational PATCH methods with latest cortex update (#4568)

* fix: exclude yml into totaldownload start next version (#4572)

* enhancement: adjust hub ui in different themes (#4574)

* enhancement: toggle change cover hub banner style (#4579)

* chore: update style codeblock (#4599)

* docs: improve local AI guides content and linking (#4600)

* docs: add DeepSeek R1 local installation guide

- Add comprehensive guide for running DeepSeek R1 locally
- Include step-by-step instructions with screenshots
- Add VRAM requirements and model selection guide
- Include system prompt setup instructions

* docs: add comprehensive guide on running AI models locally

* docs: address PR feedback for DeepSeek R1 and local AI guides

- Improve language and terminology throughout
- Add Linux support information
- Enhance technical explanations
- Update introduction for better flow
- Fix parameters section in run-ai-models-locally.mdx

* docs: improve local AI guides content and linking

- Update titles and introductions for better SEO
- Add opinionated guidance section for beginners
- Link DeepSeek guide with general local AI guide
- Fix typos and improve readability

* fix: remove git conflict markers from deepseek guide frontmatter

---------

Co-authored-by: Louis <louis@jan.ai>

* chore: open URL from model detail page should open in an external browser (#4611)

* chore: open URL from model detail page should open in an external browser

* chore: remove unused param

* Added guide for cloud model installation

* Added DeepSeek & Google guides, updated some images

* blog: improve local AI guide for beginners (#4610)

* docs: add DeepSeek R1 local installation guide

- Add comprehensive guide for running DeepSeek R1 locally
- Include step-by-step instructions with screenshots
- Add VRAM requirements and model selection guide
- Include system prompt setup instructions

* docs: add comprehensive guide on running AI models locally

* docs: address PR feedback for DeepSeek R1 and local AI guides

- Improve language and terminology throughout
- Add Linux support information
- Enhance technical explanations
- Update introduction for better flow
- Fix parameters section in run-ai-models-locally.mdx

* docs: improve local AI guides content and linking

- Update titles and introductions for better SEO
- Add opinionated guidance section for beginners
- Link DeepSeek guide with general local AI guide
- Fix typos and improve readability

* fix: remove git conflict markers from deepseek guide frontmatter

* docs: improve local AI guide for beginners

Key improvements:
- Add detailed explanation of GGUF and why it's needed
- Improve content structure and readability
- Add visual guides with SEO-friendly images
- Enhance llama.cpp explanation with GitHub link
- Fix heading hierarchy for better navigation
- Add practical examples and common questions
- Update image paths and captions for better SEO

Technical details:
- Add proper image alt text and captions
- Link to llama.cpp GitHub repository
- Clarify model size requirements
- Simplify hardware requirements section
- Improve heading structure (h1-h5)
- Add step-by-step model installation guide

* docs: add offline ChatGPT alternative guide with Jan

- Add comprehensive guide on using Jan as offline ChatGPT alternative
- Include step-by-step instructions for setup
- Add images for document chat feature
- Optimize content for SEO with relevant keywords

* docs: update description to emphasize computer-local aspect

---------

Co-authored-by: Louis <louis@jan.ai>

* docs: add new changelog posts and images (#4601)

* docs: add DeepSeek R1 local installation guide

- Add comprehensive guide for running DeepSeek R1 locally
- Include step-by-step instructions with screenshots
- Add VRAM requirements and model selection guide
- Include system prompt setup instructions

* docs: add comprehensive guide on running AI models locally

* docs: address PR feedback for DeepSeek R1 and local AI guides

- Improve language and terminology throughout
- Add Linux support information
- Enhance technical explanations
- Update introduction for better flow
- Fix parameters section in run-ai-models-locally.mdx

* docs: improve local AI guides content and linking

- Update titles and introductions for better SEO
- Add opinionated guidance section for beginners
- Link DeepSeek guide with general local AI guide
- Fix typos and improve readability

* docs: add new changelog posts and images

- Add DeepSeek R1 changelog (v0.5.14)
- Add key issues resolved changelog (v0.5.13)
- Add corresponding changelog images

---------

Co-authored-by: Louis <louis@jan.ai>

* Fix typo for Red Hat company name (#4637)

"Red Hat" is the correct company name, not "Redhat".

* feat: app updater with changelog (#4631)

* feat: ui modal app updater with changelog

* chore: update action when click update now

* chore: update handler actions

* chore: fix linter

* Update all images & some wrong parts

* chore: typo

* chore: lint fix

* fix: format compact number utils (#4695)

* chore: Hub UI and markdown CSS

* feat: allow users to refresh cloud model list (#4698)

* feat: allow users to refresh cloud model list

* chore: reusable model list refresh

* chore: clean up

* refactor: different Jan builds should have different Cortex server port (#4699)

* refactor: different Jan instances should have different Cortex server port configurations

* chore: update workflow to use env input

* chore: update env for cortex port setting

* chore: change app logo for jan beta and nightly version

* feat: Jan Model Hub should stay updated. (#4707)

* feat: Jan Model Hub should stay updated.

* chore: polish provider description

* feat: preserve token speed in the thread (#4711)

* feat: preserve token speed in the thread

* chore: lint fix

* chore: streaming should be turned on by default (#4712)

* fix: should disable start model button when there is a model is loading (#4713)

* chore: update pipeline change app logo for build nightly (#4709)

* chore: change app logo for build nightly

* chore: add nightly option to rename icons

* chore: add rename icons for nightly and beta build

* chore: remove rename icons job

* feat: Jan Model Hub filter options and responsiveness (#4714)

* feat: Jan Model Hub filter options and responsiveness

* chore: fix display unit

* chore: fix optional wrapping

* chore: correct joi component's test

* chore: sticky model hub filter panel (#4715)

* chore: migrate engine settings on update (#4719)

* chore: migrate engine settings on update

* chore: queue engine migration to ensure it only execute when server is on

* chore: ensure queue is empty instead of running in the queue

* enhancement: update pexelated icon on windows platform (#4721)

* chore: fix model hub sorting (#4722)

* chore: fix model hub sorting

* chore: linter fix

* fix: should not select vulkan by default when there are Nvidia GPUs detected (#4720)

* chore: enhance onboarding screen's models (#4723)

* chore: enhance onboarding screen's models

* chore: lint fix

* chore: correct lint fix command

* chore: fix tests

* enhancement: scrollbar setting options (#4726)

* enhancement: scrollbar setting options

* chore: fix linter

* refactor: clean up legacy predefined models (#4730)

* refactor: clean up legacy predefined models

* chore: fix onboarding filtered models

* chore: correct channel name to ask for help

* fix: flow app updater manual check from native menu (#4731)

* chore: update hub UI  (#4734)

* chore: update hub ui based feedback

* chore: update hub ui

* chore: code block ui

* chore: update bg color

* chore: decrease margin codeblock

* enhancement: improve chat thread (#4736)

* enhancement: improve chat thread

* chore: fix linter

* fix: linter

* chore: fix linter

* fix: chore failed test

* fix: chat body scrollbar (#4737)

* fix: chat body scrollbar

* chore: update lock file

* fix: remove PluggableList

* fix: onboarding screen should show persisted cloud providers (#4738)

* chore: remove hard coded recommendation models and use cortexso featured tags (#4741)

* chore: remove hard coded recommendation models and use cortexso featured tags

* chore: polish model detail page

* chore: fix test

* fix: hub ui no result search found (#4739)

* fix: hub button download  (#4742)

* fix: hub button download and use

* chore: fix linter

* fix: remove the button download on top of model page

* enhancement: receommended label engine variant (#4740)

* fix: correct default engine variant setup on first launch (#4747)

* fix: there is a case where app selects incorrect engine variant first launch

* refactor: clean up legacy settings hook

* fix: cannot click dropdown appearance (#4750)

* fix: app check available update (#4751)

* fix: some endpoints are invisible (#4752)

* chore: correct model author with new cortexso update (#4754)

* chore: hub UI tooltip filter, max model size and search result (#4753)

* chore: fix hub ui tooltip, max-filter, and search result

* chore: fix linter

* fix: deeplink does not work sometime (#4755)

* fix: deep link does not work sometime and reduce redundant request

* chore: bump cortex fix

* typo fix (#4748)

* chore: handle list number and disc readme hgf (#4756)

* chore: handle list number and disc readme hgf

* chore: fix space hover message toolbar

* chore: handle reset state filter hub revamp

* fix: Jan Quick Ask window capture input issues (#4758)

* fix: first message padding is off

* fix: correct jan discord channel

* feat: add openai 4.5 preview and anthropic claude 3.7 sonnet models

* fix: image upload button does not work - refresh models list persist current selected engine (#4768)

* fix: list space styled and hidden message toolbar when editmode (#4773)

* refactor: clean up deprecated components and events (#4769)

* fix: quickask ui color and alignment on mymodel list (#4774)

* fix: quickask ui color and alignment on mymodel list

* fix: color scheme quickask

* chore: bump cortex version to fix model sources issue (#4775)

* chore: sync initial hub models (#4778)

* chore: sync initial hub models

* fix: openai request template

* fix: naming nightly and beta build (#4779)

* fix: naming nightly and beta build

* chore: enhance replace icons for beta and nightly build logic

* chore: update icon linux build

* chore: add debug step

* chore: remove specific icon linux build

* refactor: clean up legacy vision model settings (#4777)

* analytics: integrate posthog into Jan web (#4788)

* fix: engine version update - cortex version bump - update tests (#4787)

* fix: anthropic response template correction

* fix: image preview overlap toolbar message (#4790)

* fix: cohere response template correction for proper markdown parsing

* fix: hub UI issue render readme

* chore: fix linter

* fix: result search scrollable

* fix: app image - could not load model

* chore: bump cortex version (#4793)

* enhancement: add checkmark selected model

* fix: render desc hub model list

* chore linter

* fix: table markdown

* chore: do not symlink engine on linux

* fix: update career url

* chore: correct cohere response template

* chore: bump llama.cpp to support gemma3

* fix: minor ui issue

* fix: scroll setting preferences

* chore: fix padding

* fix: clear logs should not delete the folder (#4806)

* chore: bump cortex 1.0.11-rc10

* chore: bump to latest cortex release

* feat: Cortex API Authorization

* chore: correct CI CD repo name

* chore: correct new menloresearch repo name

* feat: rotate api token for each run (#4820)

* feat: rotate api token for each run

* chore: correct github repo url

* chore: correct github api url

* chore: should not filter out models first launch

* chore: bump cortex release

* chore: should get hardware information on launch (#4821)

* chore: should have an option to not revalidate hardware information

* chore: cortex.cpp gpu activation could cause a race condition (#4825)

* fix: jan beta logo displayed in jan release (#4828)

---------

Co-authored-by: Emre Can Kartal <159995642+eckartal@users.noreply.github.com>
Co-authored-by: Faisal Amir <urmauur@gmail.com>
Co-authored-by: Ashley <tuyethantt@gmail.com>
Co-authored-by: Ashley <89722390+imtuyethan@users.noreply.github.com>
Co-authored-by: ddri <davedri@gmail.com>
Co-authored-by: Minh141120 <minh.itptit@gmail.com>
Co-authored-by: Nguyen Ngoc Minh <91668012+Minh141120@users.noreply.github.com>
Co-authored-by: Minh <david@menlo.ai>
Co-authored-by: David <davidpt.janai@gmail.com>
Co-authored-by: Matt <matthewbcool@gmail.com>
2025-03-24 12:14:31 +07:00
Nguyen Ngoc Minh
ef172dc6c8
fix: jan beta logo displayed in jan release (#4828) 2025-03-21 22:45:07 +07:00
Louis
7e46295af1
chore: cortex.cpp gpu activation could cause a race condition (#4825) 2025-03-21 14:55:39 +07:00
David
2271c8d3d6
Merge pull request #4822 from menloresearch/chore/add-option-to-not-revalidate-hardware-infor
chore: should have an option to not revalidate hardware information
2025-03-21 10:46:05 +07:00
Louis
1d4567082b
chore: should have an option to not revalidate hardware information 2025-03-21 10:33:16 +07:00
Louis
8c0f88fb4e
chore: should get hardware information on launch (#4821) 2025-03-21 09:19:41 +07:00
Louis
296891ee39
chore: bump cortex release 2025-03-21 08:19:33 +07:00
Louis
3568053084
chore: should not filter out models first launch 2025-03-20 23:05:18 +07:00
Louis
fd2d23869c
feat: rotate api token for each run (#4820)
* feat: rotate api token for each run

* chore: correct github repo url

* chore: correct github api url
2025-03-20 21:41:41 +07:00
David
431e4b00dc
Merge pull request #4819 from menloresearch/feat/cortex-api-authorization
feat: Cortex API Authorization
2025-03-20 19:58:36 +07:00
Louis
03367f4387
chore: correct new menloresearch repo name 2025-03-20 19:26:09 +07:00
Louis
30f9a34ede
chore: correct CI CD repo name 2025-03-20 19:23:08 +07:00
Louis
821036945f
feat: Cortex API Authorization 2025-03-20 19:03:36 +07:00
Louis
09877c94a2
chore: bump to latest cortex release 2025-03-18 17:57:48 +07:00
Louis
24051f493f
chore: bump cortex 1.0.11-rc10 2025-03-17 15:42:15 +07:00
Louis
07428b4cdc
Merge pull request #4684 from janhq/release/v0.5.15
chore: merge release 0.5.15 branch into main branch
2025-02-18 18:13:27 +07:00
Louis
3037206108
Merge pull request #4509 from janhq/release/v0.5.14
chore: sync 0.5.14 release into main
2025-01-23 11:42:21 +07:00
40 changed files with 574 additions and 596 deletions

View File

@ -40,6 +40,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
build-windows-x64:
@ -49,6 +51,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
build-linux-x64:
@ -58,6 +62,8 @@ jobs:
with:
ref: ${{ github.ref }}
public_provider: github
beta: false
nightly: false
new_version: ${{ needs.get-update-version.outputs.new_version }}
update_release_draft:
@ -82,4 +88,4 @@ jobs:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -36,7 +36,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.PAT_SERVICE_ACCOUNT }}
run: |
curl -s https://api.github.com/repos/janhq/cortex/releases > /tmp/github_api_releases.json
curl -s https://api.github.com/repos/menloresearch/cortex/releases > /tmp/github_api_releases.json
latest_prerelease_name=$(cat /tmp/github_api_releases.json | jq -r '.[] | select(.prerelease) | .name' | head -n 1)
get_asset_count() {

View File

@ -55,55 +55,26 @@ jobs:
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
echo "Icons before replacement:"
ls -la electron/icons/
echo "Setting up beta icons"
# Replace the key icon files with move operations (no need for rm first)
mv electron/icons/jan-beta-512x512.png electron/icons/512x512.png
mv electron/icons/jan-beta.ico electron/icons/icon.ico
mv electron/icons/jan-beta.png electron/icons/icon.png
mv electron/icons/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-beta-tray.png electron/icons/icon-tray.png
# Remove any remaining nightly icon files
rm -f electron/icons/jan-nightly-512x512.png
rm -f electron/icons/jan-nightly.ico
rm -f electron/icons/jan-nightly.png
rm -f electron/icons/jan-nightly-tray@2x.png
rm -f electron/icons/jan-nightly-tray.png
# Verify the replacement
echo "Icons after replacement:"
ls -la electron/icons/
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
echo "Icons before replacement:"
ls -la electron/icons/
echo "Setting up nightly icons"
# Replace the key icon files with move operations (no need for rm first)
mv electron/icons/jan-nightly-512x512.png electron/icons/512x512.png
mv electron/icons/jan-nightly.ico electron/icons/icon.ico
mv electron/icons/jan-nightly.png electron/icons/icon.png
mv electron/icons/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-nightly-tray.png electron/icons/icon-tray.png
# Remove any remaining beta icon files
rm -f electron/icons/jan-beta-512x512.png
rm -f electron/icons/jan-beta.ico
rm -f electron/icons/jan-beta.png
rm -f electron/icons/jan-beta-tray@2x.png
rm -f electron/icons/jan-beta-tray.png
# Verify the replacement
echo "Icons after replacement:"
ls -la electron/icons/
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
@ -213,4 +184,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-linux-amd64-${{ inputs.new_version }}-AppImage
path: ./electron/dist/*.AppImage
path: ./electron/dist/*.AppImage

View File

@ -65,31 +65,25 @@ jobs:
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
rm -f electron/icons/512x512.png
rm -f electron/icons/icon.ico
rm -f electron/icons/icon.png
rm -f electron/icons/icon-tray@2x.png
rm -f electron/icons/icon-tray.png
mv electron/icons/jan-beta-512x512.png electron/icons/512x512.png
mv electron/icons/jan-beta.ico electron/icons/icon.ico
mv electron/icons/jan-beta.png electron/icons/icon.png
mv electron/icons/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-beta-tray.png electron/icons/icon-tray.png
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
rm -f electron/icons/512x512.png
rm -f electron/icons/icon.ico
rm -f electron/icons/icon.png
rm -f electron/icons/icon-tray@2x.png
rm -f electron/icons/icon-tray.png
mv electron/icons/jan-nightly-512x512.png electron/icons/512x512.png
mv electron/icons/jan-nightly.ico electron/icons/icon.ico
mv electron/icons/jan-nightly.png electron/icons/icon.png
mv electron/icons/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-nightly-tray.png electron/icons/icon-tray.png
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
@ -236,4 +230,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-mac-universal-${{ inputs.new_version }}
path: ./electron/dist/*.dmg
path: ./electron/dist/*.dmg

View File

@ -64,31 +64,25 @@ jobs:
if: inputs.beta == true && inputs.nightly != true
shell: bash
run: |
rm -f electron/icons/512x512.png
rm -f electron/icons/icon.ico
rm -f electron/icons/icon.png
rm -f electron/icons/icon-tray@2x.png
rm -f electron/icons/icon-tray.png
mv electron/icons/jan-beta-512x512.png electron/icons/512x512.png
mv electron/icons/jan-beta.ico electron/icons/icon.ico
mv electron/icons/jan-beta.png electron/icons/icon.png
mv electron/icons/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-beta-tray.png electron/icons/icon-tray.png
rm -rf electron/icons/*
cp electron/icons_dev/jan-beta-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-beta.ico electron/icons/icon.ico
cp electron/icons_dev/jan-beta.png electron/icons/icon.png
cp electron/icons_dev/jan-beta-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-beta-tray.png electron/icons/icon-tray.png
- name: Replace Icons for Nightly Build
if: inputs.nightly == true && inputs.beta != true
shell: bash
run: |
rm -f electron/icons/512x512.png
rm -f electron/icons/icon.ico
rm -f electron/icons/icon.png
rm -f electron/icons/icon-tray@2x.png
rm -f electron/icons/icon-tray.png
mv electron/icons/jan-nightly-512x512.png electron/icons/512x512.png
mv electron/icons/jan-nightly.ico electron/icons/icon.ico
mv electron/icons/jan-nightly.png electron/icons/icon.png
mv electron/icons/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
mv electron/icons/jan-nightly-tray.png electron/icons/icon-tray.png
rm -rf electron/icons/*
cp electron/icons_dev/jan-nightly-512x512.png electron/icons/512x512.png
cp electron/icons_dev/jan-nightly.ico electron/icons/icon.ico
cp electron/icons_dev/jan-nightly.png electron/icons/icon.png
cp electron/icons_dev/jan-nightly-tray@2x.png electron/icons/icon-tray@2x.png
cp electron/icons_dev/jan-nightly-tray.png electron/icons/icon-tray.png
- name: Installing node
uses: actions/setup-node@v1
@ -232,4 +226,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: jan-win-x64-${{ inputs.new_version }}
path: ./electron/dist/*.exe
path: ./electron/dist/*.exe

View File

@ -30,7 +30,7 @@ jobs:
local max_retries=3
local tag
while [ $retries -lt $max_retries ]; do
tag=$(curl -s https://api.github.com/repos/janhq/jan/releases/latest | jq -r .tag_name)
tag=$(curl -s https://api.github.com/repos/menloresearch/jan/releases/latest | jq -r .tag_name)
if [ -n "$tag" ] && [ "$tag" != "null" ]; then
echo $tag
return

View File

@ -33,6 +33,8 @@ export enum NativeRoute {
stopServer = 'stopServer',
appUpdateDownload = 'appUpdateDownload',
appToken = 'appToken',
}
/**

View File

@ -317,4 +317,11 @@ export function handleAppIPCs() {
const { stopServer } = require('@janhq/server')
return stopServer()
})
/**
* Handles the "appToken" IPC message to generate a random app ID.
*/
ipcMain.handle(NativeRoute.appToken, async (_event): Promise<string> => {
return process.env.appToken ?? 'cortex.cpp'
})
}

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 806 B

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 835 B

After

Width:  |  Height:  |  Size: 835 B

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -29,6 +29,7 @@ import { trayManager } from './managers/tray'
import { logSystemInfo } from './utils/system'
import { registerGlobalShortcuts } from './utils/shortcut'
import { registerLogger } from './utils/logger'
import { randomBytes } from 'crypto'
const preloadPath = join(__dirname, 'preload.js')
const preloadQuickAskPath = join(__dirname, 'preload.quickask.js')
@ -56,6 +57,10 @@ const createMainWindow = () => {
windowManager.createMainWindow(preloadPath, startUrl)
}
// Generate a random token for the app
// This token is used for authentication when making request to cortex.cpp server
process.env.appToken = randomBytes(16).toString('hex')
app
.whenReady()
.then(() => {

View File

@ -23,11 +23,16 @@ export class Retrieval {
constructor(chunkSize: number = 4000, chunkOverlap: number = 200) {
this.updateTextSplitter(chunkSize, chunkOverlap)
this.initialize()
}
private async initialize() {
const apiKey = await window.core?.api.appToken() ?? 'cortex.cpp'
// declare time-weighted retriever and storage
this.timeWeightedVectorStore = new MemoryVectorStore(
new OpenAIEmbeddings(
{ openAIApiKey: 'cortex-embedding' },
{ openAIApiKey: apiKey },
{ basePath: `${CORTEX_API_URL}/v1` }
)
)
@ -47,9 +52,10 @@ export class Retrieval {
})
}
public updateEmbeddingEngine(model: string, engine: string): void {
public async updateEmbeddingEngine(model: string, engine: string) {
const apiKey = await window.core?.api.appToken() ?? 'cortex.cpp'
this.embeddingModel = new OpenAIEmbeddings(
{ openAIApiKey: 'cortex-embedding', model },
{ openAIApiKey: apiKey, model },
// TODO: Raw settings
{ basePath: `${CORTEX_API_URL}/v1` }
)

View File

@ -4,7 +4,7 @@ import {
ThreadAssistantInfo,
ThreadMessage,
} from '@janhq/core'
import ky from 'ky'
import ky, { KyInstance } from 'ky'
import PQueue from 'p-queue'
type ThreadList = {
@ -22,6 +22,22 @@ type MessageList = {
export default class CortexConversationalExtension extends ConversationalExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
return this.api
}
/**
* Called when the extension is loaded.
*/
@ -39,10 +55,12 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async listThreads(): Promise<Thread[]> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/threads?limit=-1`)
.json<ThreadList>()
.then((e) => e.data)
this.apiInstance().then((api) =>
api
.get('v1/threads?limit=-1')
.json<ThreadList>()
.then((e) => e.data)
)
) as Promise<Thread[]>
}
@ -52,7 +70,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async createThread(thread: Thread): Promise<Thread> {
return this.queue.add(() =>
ky.post(`${API_URL}/v1/threads`, { json: thread }).json<Thread>()
this.apiInstance().then((api) =>
api.post('v1/threads', { json: thread }).json<Thread>()
)
) as Promise<Thread>
}
@ -63,7 +83,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
async modifyThread(thread: Thread): Promise<void> {
return this.queue
.add(() =>
ky.patch(`${API_URL}/v1/threads/${thread.id}`, { json: thread })
this.apiInstance().then((api) =>
api.patch(`v1/threads/${thread.id}`, { json: thread })
)
)
.then()
}
@ -74,7 +96,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async deleteThread(threadId: string): Promise<void> {
return this.queue
.add(() => ky.delete(`${API_URL}/v1/threads/${threadId}`))
.add(() =>
this.apiInstance().then((api) => api.delete(`v1/threads/${threadId}`))
)
.then()
}
@ -85,11 +109,13 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async createMessage(message: ThreadMessage): Promise<ThreadMessage> {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/threads/${message.thread_id}/messages`, {
json: message,
})
.json<ThreadMessage>()
this.apiInstance().then((api) =>
api
.post(`v1/threads/${message.thread_id}/messages`, {
json: message,
})
.json<ThreadMessage>()
)
) as Promise<ThreadMessage>
}
@ -100,14 +126,13 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async modifyMessage(message: ThreadMessage): Promise<ThreadMessage> {
return this.queue.add(() =>
ky
.patch(
`${API_URL}/v1/threads/${message.thread_id}/messages/${message.id}`,
{
this.apiInstance().then((api) =>
api
.patch(`v1/threads/${message.thread_id}/messages/${message.id}`, {
json: message,
}
)
.json<ThreadMessage>()
})
.json<ThreadMessage>()
)
) as Promise<ThreadMessage>
}
@ -120,7 +145,9 @@ export default class CortexConversationalExtension extends ConversationalExtensi
async deleteMessage(threadId: string, messageId: string): Promise<void> {
return this.queue
.add(() =>
ky.delete(`${API_URL}/v1/threads/${threadId}/messages/${messageId}`)
this.apiInstance().then((api) =>
api.delete(`v1/threads/${threadId}/messages/${messageId}`)
)
)
.then()
}
@ -132,10 +159,12 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async listMessages(threadId: string): Promise<ThreadMessage[]> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/threads/${threadId}/messages?order=asc&limit=-1`)
.json<MessageList>()
.then((e) => e.data)
this.apiInstance().then((api) =>
api
.get(`v1/threads/${threadId}/messages?order=asc&limit=-1`)
.json<MessageList>()
.then((e) => e.data)
)
) as Promise<ThreadMessage[]>
}
@ -147,9 +176,11 @@ export default class CortexConversationalExtension extends ConversationalExtensi
*/
async getThreadAssistant(threadId: string): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/assistants/${threadId}?limit=-1`)
.json<ThreadAssistantInfo>()
this.apiInstance().then((api) =>
api
.get(`v1/assistants/${threadId}?limit=-1`)
.json<ThreadAssistantInfo>()
)
) as Promise<ThreadAssistantInfo>
}
/**
@ -163,9 +194,11 @@ export default class CortexConversationalExtension extends ConversationalExtensi
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
this.apiInstance().then((api) =>
api
.post(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
)
) as Promise<ThreadAssistantInfo>
}
@ -180,9 +213,11 @@ export default class CortexConversationalExtension extends ConversationalExtensi
assistant: ThreadAssistantInfo
): Promise<ThreadAssistantInfo> {
return this.queue.add(() =>
ky
.patch(`${API_URL}/v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
this.apiInstance().then((api) =>
api
.patch(`v1/assistants/${threadId}`, { json: assistant })
.json<ThreadAssistantInfo>()
)
) as Promise<ThreadAssistantInfo>
}
@ -191,10 +226,12 @@ export default class CortexConversationalExtension extends ConversationalExtensi
* @returns
*/
async healthz(): Promise<void> {
return ky
.get(`${API_URL}/healthz`, {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
)
.then(() => {})
}
}

View File

@ -15,7 +15,7 @@ import {
ModelEvent,
EngineEvent,
} from '@janhq/core'
import ky, { HTTPError } from 'ky'
import ky, { HTTPError, KyInstance } from 'ky'
import PQueue from 'p-queue'
import { EngineError } from './error'
import { getJanDataFolderPath } from '@janhq/core'
@ -31,6 +31,22 @@ interface ModelList {
export default class JanEngineManagementExtension extends EngineManagementExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
return this.api
}
/**
* Called when the extension is loaded.
*/
@ -59,10 +75,12 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async getEngines(): Promise<Engines> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines`)
.json<Engines>()
.then((e) => e)
this.apiInstance().then((api) =>
api
.get('v1/engines')
.json<Engines>()
.then((e) => e)
)
) as Promise<Engines>
}
@ -70,12 +88,15 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns A Promise that resolves to an object of list engines.
*/
async getRemoteModels(name: string): Promise<any> {
return ky
.get(`${API_URL}/v1/models/remote/${name}`)
.json<ModelList>()
.catch(() => ({
data: [],
})) as Promise<ModelList>
return this.apiInstance().then(
(api) =>
api
.get(`v1/models/remote/${name}`)
.json<ModelList>()
.catch(() => ({
data: [],
})) as Promise<ModelList>
)
}
/**
@ -84,10 +105,12 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async getInstalledEngines(name: InferenceEngine): Promise<EngineVariant[]> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}`)
.json<EngineVariant[]>()
.then((e) => e)
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}`)
.json<EngineVariant[]>()
.then((e) => e)
)
) as Promise<EngineVariant[]>
}
@ -103,12 +126,14 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
platform?: string
) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/releases/${version}`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/${version}`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
)
) as Promise<EngineReleased[]>
}
@ -119,12 +144,14 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async getLatestReleasedEngine(name: InferenceEngine, platform?: string) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/releases/latest`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/releases/latest`)
.json<EngineReleased[]>()
.then((e) =>
platform ? e.filter((r) => r.name.includes(platform)) : e
)
)
) as Promise<EngineReleased[]>
}
@ -134,9 +161,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async installEngine(name: string, engineConfig: EngineConfig) {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
)
) as Promise<{ messages: string }>
}
@ -167,15 +196,17 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
engineConfig.metadata.header_template = DEFAULT_REQUEST_HEADERS_TRANSFORM
return this.queue.add(() =>
ky.post(`${API_URL}/v1/engines`, { json: engineConfig }).then((e) => {
if (persistModels && engineConfig.metadata?.get_models_url) {
// Pull /models from remote models endpoint
return this.populateRemoteModels(engineConfig)
.then(() => e)
.catch(() => e)
}
return e
})
this.apiInstance().then((api) =>
api.post('v1/engines', { json: engineConfig }).then((e) => {
if (persistModels && engineConfig.metadata?.get_models_url) {
// Pull /models from remote models endpoint
return this.populateRemoteModels(engineConfig)
.then(() => e)
.catch(() => e)
}
return e
})
)
) as Promise<{ messages: string }>
}
@ -185,9 +216,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async uninstallEngine(name: InferenceEngine, engineConfig: EngineConfig) {
return this.queue.add(() =>
ky
.delete(`${API_URL}/v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
this.apiInstance().then((api) =>
api
.delete(`v1/engines/${name}/install`, { json: engineConfig })
.then((e) => e)
)
) as Promise<{ messages: string }>
}
@ -196,25 +229,27 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @param model - Remote model object.
*/
async addRemoteModel(model: Model) {
return this.queue
.add(() =>
ky
.post(`${API_URL}/v1/models/add`, {
json: {
inference_params: {
max_tokens: 4096,
temperature: 0.7,
top_p: 0.95,
stream: true,
frequency_penalty: 0,
presence_penalty: 0,
return this.queue.add(() =>
this.apiInstance()
.then((api) =>
api
.post('v1/models/add', {
json: {
inference_params: {
max_tokens: 4096,
temperature: 0.7,
top_p: 0.95,
stream: true,
frequency_penalty: 0,
presence_penalty: 0,
},
...model,
},
...model,
},
})
.then((e) => e)
)
.then(() => {})
})
.then((e) => e)
)
.then(() => {})
)
}
/**
@ -223,10 +258,12 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async getDefaultEngineVariant(name: InferenceEngine) {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/engines/${name}/default`)
.json<{ messages: string }>()
.then((e) => e)
this.apiInstance().then((api) =>
api
.get(`v1/engines/${name}/default`)
.json<{ messages: string }>()
.then((e) => e)
)
) as Promise<DefaultEngineVariant>
}
@ -240,9 +277,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
engineConfig: EngineConfig
) {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/engines/${name}/default`, { json: engineConfig })
.then((e) => e)
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/default`, { json: engineConfig })
.then((e) => e)
)
) as Promise<{ messages: string }>
}
@ -251,9 +290,11 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
*/
async updateEngine(name: InferenceEngine, engineConfig?: EngineConfig) {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/engines/${name}/update`, { json: engineConfig })
.then((e) => e)
this.apiInstance().then((api) =>
api
.post(`v1/engines/${name}/update`, { json: engineConfig })
.then((e) => e)
)
) as Promise<{ messages: string }>
}
@ -262,10 +303,12 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
* @returns
*/
async healthz(): Promise<void> {
return ky
.get(`${API_URL}/healthz`, {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
)
.then(() => {
this.queue.concurrency = Infinity
})
@ -390,7 +433,6 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
const version = await this.getSetting<string>('version', '0.0.0')
const engines = await this.getEngines()
if (version < VERSION) {
console.log('Migrating engine settings...')
// Migrate engine settings
await Promise.all(
@ -398,7 +440,7 @@ export default class JanEngineManagementExtension extends EngineManagementExtens
const { id, ...data } = engine
data.api_key = engines[id]?.api_key
return this.updateEngine(id,{
return this.updateEngine(id, {
...data,
}).catch(console.error)
})

View File

@ -29,12 +29,10 @@
},
"dependencies": {
"@janhq/core": "../../core/package.tgz",
"cpu-instructions": "^0.0.13",
"ky": "^1.7.2",
"p-queue": "^8.0.1"
},
"bundledDependencies": [
"cpu-instructions",
"@janhq/core"
],
"hardwares": {

View File

@ -1,5 +1,5 @@
import { HardwareManagementExtension, HardwareInformation } from '@janhq/core'
import ky from 'ky'
import ky, { KyInstance } from 'ky'
import PQueue from 'p-queue'
/**
@ -17,6 +17,23 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
this.queue.add(() => this.healthz())
}
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
this.api = ky.extend({
prefixUrl: API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
return this.api
}
/**
* Called when the extension is unloaded.
*/
@ -27,11 +44,13 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
* @returns
*/
async healthz(): Promise<void> {
return ky
.get(`${API_URL}/healthz`, {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
.then(() => {})
return this.apiInstance().then((api) =>
api
.get('healthz', {
retry: { limit: 20, delay: () => 500, methods: ['get'] },
})
.then(() => {})
)
}
/**
@ -39,10 +58,12 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
*/
async getHardware(): Promise<HardwareInformation> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/hardware`)
.json<HardwareInformation>()
.then((e) => e)
this.apiInstance().then((api) =>
api
.get('v1/hardware')
.json<HardwareInformation>()
.then((e) => e)
)
) as Promise<HardwareInformation>
}
@ -54,7 +75,9 @@ export default class JSONHardwareManagementExtension extends HardwareManagementE
activated_gpus: number[]
}> {
return this.queue.add(() =>
ky.post(`${API_URL}/v1/hardware/activate`, { json: data }).then((e) => e)
this.apiInstance().then((api) =>
api.post('v1/hardware/activate', { json: data }).then((e) => e)
)
) as Promise<{
message: string
activated_gpus: number[]

View File

@ -1 +1 @@
1.0.11-rc9
1.0.12

View File

@ -5,11 +5,11 @@ set /p CORTEX_VERSION=<./bin/version.txt
set ENGINE_VERSION=0.1.55
@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 CUDA_DOWNLOAD_URL=https://github.com/janhq/cortex.llamacpp/releases/download/v%ENGINE_VERSION%
set DOWNLOAD_URL=https://github.com/menloresearch/cortex.llamacpp/releases/download/v%ENGINE_VERSION%/cortex.llamacpp-%ENGINE_VERSION%-windows-amd64
set CUDA_DOWNLOAD_URL=https://github.com/menloresearch/cortex.llamacpp/releases/download/v%ENGINE_VERSION%
set SUBFOLDERS=windows-amd64-noavx-cuda-12-0 windows-amd64-noavx-cuda-11-7 windows-amd64-avx2-cuda-12-0 windows-amd64-avx2-cuda-11-7 windows-amd64-noavx windows-amd64-avx windows-amd64-avx2 windows-amd64-avx512 windows-amd64-vulkan
call .\node_modules\.bin\download -e --strip 1 -o %BIN_PATH% https://github.com/janhq/cortex.cpp/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-windows-amd64.tar.gz
call .\node_modules\.bin\download -e --strip 1 -o %BIN_PATH% https://github.com/menloresearch/cortex.cpp/releases/download/v%CORTEX_VERSION%/cortex-%CORTEX_VERSION%-windows-amd64.tar.gz
call .\node_modules\.bin\download %DOWNLOAD_URL%-avx2-cuda-12-0.tar.gz -e --strip 1 -o %SHARED_PATH%/engines/cortex.llamacpp/windows-amd64-avx2-cuda-12-0/v%ENGINE_VERSION%
call .\node_modules\.bin\download %DOWNLOAD_URL%-avx2-cuda-11-7.tar.gz -e --strip 1 -o %SHARED_PATH%/engines/cortex.llamacpp/windows-amd64-avx2-cuda-11-7/v%ENGINE_VERSION%
call .\node_modules\.bin\download %DOWNLOAD_URL%-noavx-cuda-12-0.tar.gz -e --strip 1 -o %SHARED_PATH%/engines/cortex.llamacpp/windows-amd64-noavx-cuda-12-0/v%ENGINE_VERSION%

View File

@ -3,9 +3,9 @@
# Read CORTEX_VERSION
CORTEX_VERSION=$(cat ./bin/version.txt)
ENGINE_VERSION=0.1.55
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}"
CORTEX_RELEASE_URL="https://github.com/menloresearch/cortex.cpp/releases/download"
ENGINE_DOWNLOAD_URL="https://github.com/menloresearch/cortex.llamacpp/releases/download/v${ENGINE_VERSION}/cortex.llamacpp-${ENGINE_VERSION}"
CUDA_DOWNLOAD_URL="https://github.com/menloresearch/cortex.llamacpp/releases/download/v${ENGINE_VERSION}"
BIN_PATH=./bin
SHARED_PATH="../../electron/shared"
# Detect platform

View File

@ -24,7 +24,7 @@ export default defineConfig([
},
{
input: 'src/node/index.ts',
external: ['@janhq/core/node', 'cpu-instructions'],
external: ['@janhq/core/node'],
output: {
format: 'cjs',
file: 'dist/node/index.cjs.js',

View File

@ -17,7 +17,7 @@ import {
ModelEvent,
} from '@janhq/core'
import PQueue from 'p-queue'
import ky from 'ky'
import ky, { KyInstance } from 'ky'
/**
* Event subscription types of Downloader
@ -75,8 +75,35 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
abortControllers = new Map<string, AbortController>()
api?: KyInstance
/**
* Subscribes to events emitted by the @janhq/core package.
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
this.api = ky.extend({
prefixUrl: CORTEX_API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
return this.api
}
/**
* Authorization headers for the API requests.
* @returns
*/
headers(): Promise<HeadersInit> {
return window.core?.api.appToken().then((token: string) => ({
Authorization: `Bearer ${token}`,
}))
}
/**
* Called when the extension is loaded.
*/
async onLoad() {
super.onLoad()
@ -153,45 +180,49 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
this.abortControllers.set(model.id, controller)
return await this.queue.add(() =>
ky
.post(`${CORTEX_API_URL}/v1/models/start`, {
json: {
...extractModelLoadParams(model.settings),
model: model.id,
engine:
model.engine === InferenceEngine.nitro // Legacy model cache
? InferenceEngine.cortex_llamacpp
: model.engine,
cont_batching: this.cont_batching,
n_parallel: this.n_parallel,
caching_enabled: this.caching_enabled,
flash_attn: this.flash_attn,
cache_type: this.cache_type,
use_mmap: this.use_mmap,
...(this.cpu_threads ? { cpu_threads: this.cpu_threads } : {}),
},
timeout: false,
signal,
})
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.finally(() => this.abortControllers.delete(model.id))
.then()
this.apiInstance().then((api) =>
api
.post('v1/models/start', {
json: {
...extractModelLoadParams(model.settings),
model: model.id,
engine:
model.engine === InferenceEngine.nitro // Legacy model cache
? InferenceEngine.cortex_llamacpp
: model.engine,
cont_batching: this.cont_batching,
n_parallel: this.n_parallel,
caching_enabled: this.caching_enabled,
flash_attn: this.flash_attn,
cache_type: this.cache_type,
use_mmap: this.use_mmap,
...(this.cpu_threads ? { cpu_threads: this.cpu_threads } : {}),
},
timeout: false,
signal,
})
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.finally(() => this.abortControllers.delete(model.id))
.then()
)
)
}
override async unloadModel(model: Model): Promise<void> {
return ky
.post(`${CORTEX_API_URL}/v1/models/stop`, {
json: { model: model.id },
})
.json()
.finally(() => {
this.abortControllers.get(model.id)?.abort()
})
.then()
return this.apiInstance().then((api) =>
api
.post('v1/models/stop', {
json: { model: model.id },
})
.json()
.finally(() => {
this.abortControllers.get(model.id)?.abort()
})
.then()
)
}
/**
@ -199,15 +230,17 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
* @returns
*/
private async healthz(): Promise<void> {
return ky
.get(`${CORTEX_API_URL}/healthz`, {
retry: {
limit: 20,
delay: () => 500,
methods: ['get'],
},
})
.then(() => {})
return this.apiInstance().then((api) =>
api
.get('healthz', {
retry: {
limit: 20,
delay: () => 500,
methods: ['get'],
},
})
.then(() => {})
)
}
/**
@ -215,13 +248,15 @@ export default class JanInferenceCortexExtension extends LocalOAIEngine {
* @returns
*/
private async clean(): Promise<any> {
return ky
.delete(`${CORTEX_API_URL}/processmanager/destroy`, {
timeout: 2000, // maximum 2 seconds
retry: {
limit: 0,
},
})
return this.apiInstance()
.then((api) =>
api.delete('processmanager/destroy', {
timeout: 2000, // maximum 2 seconds
retry: {
limit: 0,
},
})
)
.catch(() => {
// Do nothing
})

View File

@ -44,8 +44,9 @@ function run(): Promise<any> {
`${path.join(dataFolderPath, '.janrc')}`,
'--data_folder_path',
dataFolderPath,
'--loglevel',
'INFO',
'config',
'--api_keys',
process.env.appToken ?? 'cortex.cpp',
],
{
env: {

View File

@ -11,7 +11,7 @@ export default defineConfig({
platform: 'browser',
define: {
SETTINGS: JSON.stringify(settingJson),
API_URL: JSON.stringify(`http://127.0.0.1:${process.env.CORTEX_API_PORT ?? "39291"}`),
CORTEX_API_URL: JSON.stringify(`http://127.0.0.1:${process.env.CORTEX_API_PORT ?? "39291"}`),
DEFAULT_MODEL_SOURCES: JSON.stringify(modelSources),
},
})

View File

@ -1,5 +1,5 @@
declare const NODE: string
declare const API_URL: string
declare const CORTEX_API_URL: string
declare const SETTINGS: SettingComponentProps[]
declare const DEFAULT_MODEL_SOURCES: any

View File

@ -13,7 +13,7 @@ import {
import { scanModelsFolder } from './legacy/model-json'
import { deleteModelFiles } from './legacy/delete'
import PQueue from 'p-queue'
import ky from 'ky'
import ky, { KyInstance } from 'ky'
/**
* cortex.cpp setting keys
@ -32,9 +32,25 @@ type Data<T> = {
*/
export default class JanModelExtension extends ModelExtension {
queue = new PQueue({ concurrency: 1 })
api?: KyInstance
/**
* Get the API instance
* @returns
*/
async apiInstance(): Promise<KyInstance> {
if(this.api) return this.api
const apiKey = (await window.core?.api.appToken()) ?? 'cortex.cpp'
this.api = ky.extend({
prefixUrl: CORTEX_API_URL,
headers: {
Authorization: `Bearer ${apiKey}`,
},
})
return this.api
}
/**
* Called when the extension is loaded.
* @override
*/
async onLoad() {
this.queue.add(() => this.healthz())
@ -82,13 +98,15 @@ export default class JanModelExtension extends ModelExtension {
* Sending POST to /models/pull/{id} endpoint to pull the model
*/
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/models/pull`, { json: { model, id, name } })
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.then()
this.apiInstance().then((api) =>
api
.post('v1/models/pull', { json: { model, id, name }, timeout: false })
.json()
.catch(async (e) => {
throw (await e.response?.json()) ?? e
})
.then()
)
)
}
@ -103,10 +121,12 @@ export default class JanModelExtension extends ModelExtension {
* Sending DELETE to /models/pull/{id} endpoint to cancel a model pull
*/
return this.queue.add(() =>
ky
.delete(`${API_URL}/v1/models/pull`, { json: { taskId: model } })
.json()
.then()
this.apiInstance().then((api) =>
api
.delete('v1/models/pull', { json: { taskId: model } })
.json()
.then()
)
)
}
@ -117,7 +137,11 @@ export default class JanModelExtension extends ModelExtension {
*/
async deleteModel(model: string): Promise<void> {
return this.queue
.add(() => ky.delete(`${API_URL}/v1/models/${model}`).json().then())
.add(() =>
this.apiInstance().then((api) =>
api.delete(`v1/models/${model}`).json().then()
)
)
.catch((e) => console.debug(e))
.finally(async () => {
// Delete legacy model files
@ -219,10 +243,15 @@ export default class JanModelExtension extends ModelExtension {
async updateModel(model: Partial<Model>): Promise<Model> {
return this.queue
.add(() =>
ky
.patch(`${API_URL}/v1/models/${model.id}`, { json: { ...model } })
.json()
.then()
this.apiInstance().then((api) =>
api
.patch(`v1/models/${model.id}`, {
json: { ...model },
timeout: false,
})
.json()
.then()
)
)
.then(() => this.getModel(model.id))
}
@ -233,10 +262,12 @@ export default class JanModelExtension extends ModelExtension {
*/
async getModel(model: string): Promise<Model> {
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/models/${model}`)
.json()
.then((e) => this.transformModel(e))
this.apiInstance().then((api) =>
api
.get(`v1/models/${model}`)
.json()
.then((e) => this.transformModel(e))
)
) as Promise<Model>
}
@ -252,13 +283,16 @@ export default class JanModelExtension extends ModelExtension {
option?: OptionType
): Promise<void> {
return this.queue.add(() =>
ky
.post(`${API_URL}/v1/models/import`, {
json: { model, modelPath, name, option },
})
.json()
.catch((e) => console.debug(e)) // Ignore error
.then()
this.apiInstance().then((api) =>
api
.post('v1/models/import', {
json: { model, modelPath, name, option },
timeout: false,
})
.json()
.catch((e) => console.debug(e)) // Ignore error
.then()
)
)
}
@ -269,7 +303,11 @@ export default class JanModelExtension extends ModelExtension {
*/
async getSources(): Promise<ModelSource[]> {
const sources = await this.queue
.add(() => ky.get(`${API_URL}/v1/models/sources`).json<Data<ModelSource>>())
.add(() =>
this.apiInstance().then((api) =>
api.get('v1/models/sources').json<Data<ModelSource>>()
)
)
.then((e) => (typeof e === 'object' ? (e.data as ModelSource[]) : []))
.catch(() => [])
return sources.concat(
@ -283,11 +321,13 @@ export default class JanModelExtension extends ModelExtension {
*/
async addSource(source: string): Promise<any> {
return this.queue.add(() =>
ky.post(`${API_URL}/v1/models/sources`, {
json: {
source,
},
})
this.apiInstance().then((api) =>
api.post('v1/models/sources', {
json: {
source,
},
})
)
)
}
@ -297,11 +337,14 @@ export default class JanModelExtension extends ModelExtension {
*/
async deleteSource(source: string): Promise<any> {
return this.queue.add(() =>
ky.delete(`${API_URL}/v1/models/sources`, {
json: {
source,
},
})
this.apiInstance().then((api) =>
api.delete('v1/models/sources', {
json: {
source,
},
timeout: false,
})
)
)
}
// END - Model Sources
@ -312,7 +355,9 @@ export default class JanModelExtension extends ModelExtension {
*/
async isModelLoaded(model: string): Promise<boolean> {
return this.queue
.add(() => ky.get(`${API_URL}/v1/models/status/${model}`))
.add(() =>
this.apiInstance().then((api) => api.get(`v1/models/status/${model}`))
)
.then((e) => true)
.catch(() => false)
}
@ -324,14 +369,18 @@ export default class JanModelExtension extends ModelExtension {
return this.updateCortexConfig(options).catch((e) => console.debug(e))
}
/**
/**
* Fetches models list from cortex.cpp
* @param model
* @returns
*/
async fetchModels(): Promise<Model[]> {
async fetchModels(): Promise<Model[]> {
return this.queue
.add(() => ky.get(`${API_URL}/v1/models?limit=-1`).json<Data<Model>>())
.add(() =>
this.apiInstance().then((api) =>
api.get('v1/models?limit=-1').json<Data<Model>>()
)
)
.then((e) =>
typeof e === 'object' ? e.data.map((e) => this.transformModel(e)) : []
)
@ -371,7 +420,9 @@ export default class JanModelExtension extends ModelExtension {
}): Promise<void> {
return this.queue
.add(() =>
ky.patch(`${API_URL}/v1/configs`, { json: body }).then(() => {})
this.apiInstance().then((api) =>
api.patch('v1/configs', { json: body }).then(() => {})
)
)
.catch((e) => console.debug(e))
}
@ -381,14 +432,16 @@ export default class JanModelExtension extends ModelExtension {
* @returns
*/
private healthz(): Promise<void> {
return ky
.get(`${API_URL}/healthz`, {
retry: {
limit: 20,
delay: () => 500,
methods: ['get'],
},
})
return this.apiInstance()
.then((api) =>
api.get('healthz', {
retry: {
limit: 20,
delay: () => 500,
methods: ['get'],
},
})
)
.then(() => {
this.queue.concurrency = Infinity
})
@ -401,17 +454,22 @@ export default class JanModelExtension extends ModelExtension {
const models = await this.fetchModels()
return this.queue.add(() =>
ky
.get(`${API_URL}/v1/models/hub?author=cortexso&tag=cortex.cpp`)
.json<Data<string>>()
.then((e) => {
e.data?.forEach((model) => {
if (
!models.some((e) => 'modelSource' in e && e.modelSource === model)
)
this.addSource(model).catch((e) => console.debug(e))
})
})
this.apiInstance()
.then((api) =>
api
.get('v1/models/hub?author=cortexso&tag=cortex.cpp')
.json<Data<string>>()
.then((e) => {
e.data?.forEach((model) => {
if (
!models.some(
(e) => 'modelSource' in e && e.modelSource === model
)
)
this.addSource(model).catch((e) => console.debug(e))
})
})
)
.catch((e) => console.debug(e))
)
}

View File

@ -2668,7 +2668,7 @@
},
"url": {
"type": "string",
"example": "https://api.github.com/repos/janhq/cortex.llamacpp/releases/186479804"
"example": "https://api.github.com/repos/menloresearch/cortex.llamacpp/releases/186479804"
}
}
}
@ -3633,238 +3633,6 @@
},
"tags": ["Files"]
}
},
"/configs": {
"get": {
"summary": "Get Configurations",
"description": "Retrieves the current configuration settings of the Cortex server.",
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"allowed_origins": {
"type": "array",
"items": {
"type": "string"
},
"example": ["http://127.0.0.1:39281", "https://cortex.so"]
},
"cors": {
"type": "boolean",
"example": false
},
"proxy_username": {
"type": "string",
"example": "username"
},
"proxy_password": {
"type": "string",
"example": "password"
},
"proxy_url": {
"type": "string",
"example": "http://proxy.example.com:8080"
},
"verify_proxy_ssl": {
"type": "boolean",
"description": "test",
"example": false
},
"verify_proxy_host_ssl": {
"type": "boolean",
"example": false
},
"verify_peer_ssl": {
"type": "boolean",
"example": false
},
"verify_host_ssl": {
"type": "boolean",
"example": false
},
"no_proxy": {
"type": "string",
"example": "localhost"
},
"huggingface_token": {
"type": "string",
"example": "your_token"
}
}
},
"example": {
"allowed_origins": [
"http://127.0.0.1:39281",
"https://cortex.so"
],
"cors": false,
"proxy_username": "username",
"proxy_password": "password",
"proxy_url": "http://proxy.example.com:8080",
"verify_proxy_ssl": false,
"verify_proxy_host_ssl": false,
"verify_peer_ssl": false,
"verify_host_ssl": false,
"no_proxy": "localhost",
"huggingface_token": "your_token"
}
}
}
}
},
"tags": ["Configurations"]
},
"patch": {
"tags": ["Configurations"],
"summary": "Update configuration settings",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"cors": {
"type": "boolean",
"description": "Indicates whether CORS is enabled.",
"example": false
},
"allowed_origins": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of allowed origins.",
"example": ["http://127.0.0.1:39281", "https://cortex.so"]
},
"proxy_username": {
"type": "string",
"description": "Username for the proxy server.",
"example": "username"
},
"proxy_password": {
"type": "string",
"description": "Password for the proxy server.",
"example": "password"
},
"proxy_url": {
"type": "string",
"description": "URL for the proxy server.",
"example": "http://proxy.example.com:8080"
},
"verify_proxy_ssl": {
"type": "boolean",
"description": "Indicates whether to verify the SSL certificate of the proxy server.",
"example": false
},
"verify_proxy_host_ssl": {
"type": "boolean",
"description": "Indicates whether to verify the SSL certificate of the proxy server host.",
"example": false
},
"verify_peer_ssl": {
"type": "boolean",
"description": "Indicates whether to verify the SSL certificate of the peer.",
"example": false
},
"verify_host_ssl": {
"type": "boolean",
"description": "Indicates whether to verify the SSL certificate of the host.",
"example": false
},
"no_proxy": {
"type": "string",
"description": "List of hosts that should not be proxied.",
"example": "localhost"
},
"huggingface_token": {
"type": "string",
"description": "HuggingFace token to pull models.",
"example": "your_token"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Configuration updated successfully",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"config": {
"type": "object",
"properties": {
"allowed_origins": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"http://127.0.0.1:39281",
"https://cortex.so"
]
},
"cors": {
"type": "boolean",
"example": false
},
"proxy_username": {
"type": "string",
"example": "username"
},
"proxy_password": {
"type": "string",
"example": "password"
},
"proxy_url": {
"type": "string",
"example": "http://proxy.example.com:8080"
},
"verify_proxy_ssl": {
"type": "boolean",
"example": false
},
"verify_proxy_host_ssl": {
"type": "boolean",
"example": false
},
"verify_peer_ssl": {
"type": "boolean",
"example": false
},
"verify_host_ssl": {
"type": "boolean",
"example": false
},
"no_proxy": {
"type": "string",
"example": "localhost"
},
"huggingface_token": {
"type": "string",
"example": "your_token"
}
}
},
"message": {
"type": "string",
"example": "Configuration updated successfully"
}
}
}
}
}
}
}
}
}
},
"info": {

View File

@ -86,6 +86,14 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
},
})
const rewriteRequestHeaders = (req: any, headers: any) => {
if (req.url.includes('/configs')) return headers
return {
...headers,
authorization: `Bearer ${process.env.appToken}`, // Add or modify Authorization header
}
}
// Register Swagger UI
await server.register(require('@fastify/swagger-ui'), {
routePrefix: '/',
@ -102,24 +110,36 @@ export const startServer = async (configs?: ServerConfig): Promise<boolean> => {
upstream: `${CORTEX_API_URL}/v1`,
prefix: configs?.prefix ?? '/v1',
http2: false,
})
server.register(proxy, {
upstream: `${CORTEX_API_URL}/system`,
prefix:'/system',
http2: false,
replyOptions: {
rewriteRequestHeaders,
},
})
server.register(proxy, {
upstream: `${CORTEX_API_URL}/processManager`,
prefix:'/processManager',
prefix: '/processManager',
http2: false,
replyOptions: {
rewriteRequestHeaders,
},
})
server.register(proxy, {
upstream: `${CORTEX_API_URL}/system`,
prefix: '/system',
http2: false,
replyOptions: {
rewriteRequestHeaders,
},
})
server.register(proxy, {
upstream: `${CORTEX_API_URL}/healthz`,
prefix:'/healthz',
prefix: '/healthz',
http2: false,
replyOptions: {
rewriteRequestHeaders,
},
})
// Start listening for requests

View File

@ -15,6 +15,7 @@ import { useDebouncedCallback } from 'use-debounce'
import useAssistants from '@/hooks/useAssistants'
import { useGetEngines } from '@/hooks/useEngineManagement'
import useGetSystemResources from '@/hooks/useGetSystemResources'
import { useGetHardwareInfo } from '@/hooks/useHardwareManagement'
import useModels from '@/hooks/useModels'
import useThreads from '@/hooks/useThreads'
@ -34,6 +35,7 @@ const DataLoader: React.FC = () => {
const setJanSettingScreen = useSetAtom(janSettingScreenAtom)
const { getData: loadModels } = useModels()
const { mutate } = useGetEngines()
const { mutate: getHardwareInfo } = useGetHardwareInfo(false)
useThreads()
useAssistants()
@ -42,6 +44,7 @@ const DataLoader: React.FC = () => {
useEffect(() => {
// Load data once
loadModels()
getHardwareInfo()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const reloadData = useDebouncedCallback(() => {

View File

@ -2,7 +2,9 @@
import useSWR from 'swr'
const fetchLatestRelease = async (includeBeta: boolean) => {
const res = await fetch('https://api.github.com/repos/janhq/jan/releases')
const res = await fetch(
'https://api.github.com/repos/menloresearch/jan/releases'
)
if (!res.ok) throw new Error('Failed to fetch releases')
const releases = await res.json()

View File

@ -32,7 +32,7 @@ const getExtension = () =>
/**
* @returns A Promise that resolves to an object of list engines.
*/
export function useGetHardwareInfo() {
export function useGetHardwareInfo(updatePeriodically: boolean = true) {
const setCpuUsage = useSetAtom(cpuUsageAtom)
const setUsedRam = useSetAtom(usedRamAtom)
const setTotalRam = useSetAtom(totalRamAtom)
@ -56,7 +56,7 @@ export function useGetHardwareInfo() {
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshInterval: 2000,
refreshInterval: updatePeriodically ? 2000 : undefined,
}
)

View File

@ -80,7 +80,7 @@ const filterOptions = [
},
]
const hubCompatibleAtom = atom(true)
const hubCompatibleAtom = atom(false)
const HubScreen = () => {
const { sources } = useGetModelSources()

View File

@ -6,13 +6,14 @@ import { useState } from 'react'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { Progress, ScrollArea, Switch } from '@janhq/joi'
import { useAtom, useAtomValue } from 'jotai'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { ChevronDownIcon, GripVerticalIcon } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import { activeModelAtom } from '@/hooks/useActiveModel'
import {
useGetHardwareInfo,
setActiveGpus,
@ -47,6 +48,7 @@ const Hardware = () => {
const ramUtilitized = useAtomValue(ramUtilitizedAtom)
const showScrollBar = useAtomValue(showScrollBarAtom)
const [gpus, setGpus] = useAtom(gpusAtom)
const setActiveModel = useSetAtom(activeModelAtom)
const [orderGpus, setOrderGpus] = useAtom(orderGpusAtom)
@ -70,11 +72,15 @@ const Hardware = () => {
.filter((gpu: any) => gpu.activated)
.map((gpu: any) => Number(gpu.id))
await setActiveGpus({ gpus: activeGpuIds })
setActiveModel(undefined)
mutate()
window.location.reload()
} catch (error) {
console.error('Failed to update active GPUs:', error)
}
setIsActivatingGpu((prev) => {
prev.delete(id)
return new Set(prev)
})
}
const handleDragEnd = (result: any) => {