From 2df843fe5bd586c871c33013a52bd51437576215 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 5 Dec 2024 09:48:09 +0700 Subject: [PATCH 01/61] chore: bump web version 0.5.11 --- web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index e0f8558826..c113c3d493 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@janhq/web", - "version": "0.5.10", + "version": "0.5.11", "private": true, "homepage": "./", "scripts": { From 7e28939a0cd25f6b4cfc8b204ab48c1a6db235c1 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 5 Dec 2024 10:12:17 +0700 Subject: [PATCH 02/61] fix: hide Vulkan option for users who don't have GPU --- web/screens/Settings/Advanced/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 8d791694ca..3570633667 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -436,7 +436,7 @@ const Advanced = () => { )} {/* Vulkan for AMD GPU/ APU and Intel Arc GPU */} - {!isMac && experimentalEnabled && ( + {!isMac && gpuList.length && experimentalEnabled && (
From 2b27624f424092ad604d458ff4a40e810ec1227f Mon Sep 17 00:00:00 2001 From: Gabrielle Ong Date: Thu, 5 Dec 2024 11:53:58 +0800 Subject: [PATCH 03/61] chore: roadmap template fix --- .github/ISSUE_TEMPLATE/roadmap.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/roadmap.md b/.github/ISSUE_TEMPLATE/roadmap.md index dbb0dfdd50..7947f31bf9 100644 --- a/.github/ISSUE_TEMPLATE/roadmap.md +++ b/.github/ISSUE_TEMPLATE/roadmap.md @@ -1,3 +1,12 @@ +--- +name: Roadmap +about: Plan Roadmap items with subtasks +title: 'roadmap: ' +labels: 'type: planning' +assignees: '' + +--- + ## Goal ## Tasklist From 7585bb00ea0ef24db2bd8c81c1ce37785087e56b Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 5 Dec 2024 11:27:58 +0700 Subject: [PATCH 04/61] chore: refactor types out of atoms --- web/containers/Providers/Jotai.tsx | 9 ++------- web/types/file.d.ts | 6 ++++++ web/utils/messageRequestBuilder.ts | 4 ++-- web/utils/threadMessageBuilder.ts | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 web/types/file.d.ts diff --git a/web/containers/Providers/Jotai.tsx b/web/containers/Providers/Jotai.tsx index c68226fefc..8f1433ea0c 100644 --- a/web/containers/Providers/Jotai.tsx +++ b/web/containers/Providers/Jotai.tsx @@ -4,6 +4,8 @@ import { PropsWithChildren } from 'react' import { Provider, atom } from 'jotai' +import { FileInfo } from '@/types/file' + export const editPromptAtom = atom('') export const currentPromptAtom = atom('') export const fileUploadAtom = atom([]) @@ -15,10 +17,3 @@ export const selectedTextAtom = atom('') export default function JotaiWrapper({ children }: PropsWithChildren) { return {children} } - -export type FileType = 'image' | 'pdf' - -export type FileInfo = { - file: File - type: FileType -} diff --git a/web/types/file.d.ts b/web/types/file.d.ts new file mode 100644 index 0000000000..737c5e380a --- /dev/null +++ b/web/types/file.d.ts @@ -0,0 +1,6 @@ +export type FileType = 'image' | 'pdf' + +export type FileInfo = { + file: File + type: FileType +} diff --git a/web/utils/messageRequestBuilder.ts b/web/utils/messageRequestBuilder.ts index 3153a7e3eb..63b14d769e 100644 --- a/web/utils/messageRequestBuilder.ts +++ b/web/utils/messageRequestBuilder.ts @@ -13,10 +13,10 @@ import { } from '@janhq/core' import { ulid } from 'ulidx' -import { FileType } from '@/containers/Providers/Jotai' - import { Stack } from '@/utils/Stack' +import { FileType } from '@/types/file' + export class MessageRequestBuilder { msgId: string type: MessageRequestType diff --git a/web/utils/threadMessageBuilder.ts b/web/utils/threadMessageBuilder.ts index 92e51e5742..1f55e4d2d1 100644 --- a/web/utils/threadMessageBuilder.ts +++ b/web/utils/threadMessageBuilder.ts @@ -6,10 +6,10 @@ import { ThreadMessage, } from '@janhq/core' -import { FileInfo } from '@/containers/Providers/Jotai' - import { MessageRequestBuilder } from './messageRequestBuilder' +import { FileInfo } from '@/types/file' + export class ThreadMessageBuilder { messageRequest: MessageRequestBuilder From 85ec8c69049b1f8484787bebb8cb7209655b558d Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 5 Dec 2024 15:10:51 +0800 Subject: [PATCH 05/61] feat: update app download universal for macos (#4230) --- docs/src/components/Download/CardDownload.tsx | 13 ++---- .../src/components/DropdownDownload/index.tsx | 44 +++++++------------ 2 files changed, 20 insertions(+), 37 deletions(-) diff --git a/docs/src/components/Download/CardDownload.tsx b/docs/src/components/Download/CardDownload.tsx index f75543d621..f61f9f462c 100644 --- a/docs/src/components/Download/CardDownload.tsx +++ b/docs/src/components/Download/CardDownload.tsx @@ -18,17 +18,12 @@ type SystemType = { const systemsTemplate: SystemType[] = [ { - name: 'Mac M1, M2, M3', - label: 'Apple Silicon', + name: 'Mac ', + label: 'Universal', logo: FaApple, - fileFormat: '{appname}-mac-arm64-{tag}.dmg', - }, - { - name: 'Mac (Intel)', - label: 'Apple Intel', - logo: FaApple, - fileFormat: '{appname}-mac-x64-{tag}.dmg', + fileFormat: '{appname}-mac-universal-{tag}.dmg', }, + { name: 'Windows', label: 'Standard (64-bit)', diff --git a/docs/src/components/DropdownDownload/index.tsx b/docs/src/components/DropdownDownload/index.tsx index 87461122eb..26e0f49d6b 100644 --- a/docs/src/components/DropdownDownload/index.tsx +++ b/docs/src/components/DropdownDownload/index.tsx @@ -24,14 +24,9 @@ type GpuInfo = { const systemsTemplate: SystemType[] = [ { - name: 'Download for Mac (M1/M2/M3)', + name: 'Download for Mac', logo: FaApple, - fileFormat: '{appname}-mac-arm64-{tag}.dmg', - }, - { - name: 'Download for Mac (Intel)', - logo: FaApple, - fileFormat: '{appname}-mac-x64-{tag}.dmg', + fileFormat: '{appname}-mac-universal-{tag}.dmg', }, { name: 'Download for Windows', @@ -66,27 +61,20 @@ const DropdownDownload = ({ lastRelease }: Props) => { type: '', }) - const changeDefaultSystem = useCallback( - async (systems: SystemType[]) => { - const userAgent = navigator.userAgent - if (userAgent.includes('Windows')) { - // windows user - setDefaultSystem(systems[2]) - } else if (userAgent.includes('Linux')) { - // linux user - setDefaultSystem(systems[3]) - } else if (userAgent.includes('Mac OS')) { - if (gpuInfo.type === 'Apple Silicon') { - setDefaultSystem(systems[0]) - } else { - setDefaultSystem(systems[1]) - } - } else { - setDefaultSystem(systems[1]) - } - }, - [gpuInfo.type] - ) + const changeDefaultSystem = useCallback(async (systems: SystemType[]) => { + const userAgent = navigator.userAgent + if (userAgent.includes('Windows')) { + // windows user + setDefaultSystem(systems[2]) + } else if (userAgent.includes('Linux')) { + // linux user + setDefaultSystem(systems[3]) + } else if (userAgent.includes('Mac OS')) { + setDefaultSystem(systems[0]) + } else { + setDefaultSystem(systems[1]) + } + }, []) function getUnmaskedInfo(gl: WebGLRenderingContext): { renderer: string From 863b0ac2f1cbe8d786510bebfc036dcd891460da Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:56:12 +0700 Subject: [PATCH 06/61] chore: update download url for macos universal (#4232) Co-authored-by: Hien To --- README.md | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0439605378..8052a34dc1 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures: Version Type Windows - MacOS + MacOS Universal Linux @@ -59,15 +59,9 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures: - + - Intel - - - - - - M1/M2/M3/M4 + jan.dmg @@ -92,15 +86,9 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures: - - - Intel - - - - + - M1/M2/M3/M4 + jan.dmg @@ -125,15 +113,9 @@ From PCs to multi-GPU clusters, Jan & Cortex supports universal architectures: - - - Intel - - - - + - M1/M2/M3/M4 + jan.dmg From c7f637348afabc25cf102410172134f24da6bb2e Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:16:43 +0700 Subject: [PATCH 07/61] chore: fix beta ci (#4233) Co-authored-by: Hien To --- .github/workflows/jan-electron-build-beta.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/jan-electron-build-beta.yml b/.github/workflows/jan-electron-build-beta.yml index b29038b558..9cae31d670 100644 --- a/.github/workflows/jan-electron-build-beta.yml +++ b/.github/workflows/jan-electron-build-beta.yml @@ -70,6 +70,8 @@ jobs: permissions: contents: write steps: + - name: Getting the repo + uses: actions/checkout@v3 - name: Sync temp to latest run: | # sync temp-beta to beta by copy files that are different or new From fbdb8e2bc83ed97e9a01331de9e73188fb90d0fe Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 5 Dec 2024 17:04:36 +0800 Subject: [PATCH 08/61] fix: list style off screen (#4231) --- web/styles/components/message.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/styles/components/message.scss b/web/styles/components/message.scss index d73a39f654..d96ab8b6a4 100644 --- a/web/styles/components/message.scss +++ b/web/styles/components/message.scss @@ -5,8 +5,9 @@ ul, ol { list-style: auto; - padding-left: 24px; + padding-left: 16px; white-space: normal; + list-style-position: inside; } ul { From b71dfb8047b15cecc3c102b403efbd4463ede77e Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 5 Dec 2024 16:15:21 +0700 Subject: [PATCH 09/61] fix: unexpected content --- web/screens/Settings/Advanced/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 3570633667..0ca7ebc643 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -436,7 +436,7 @@ const Advanced = () => { )} {/* Vulkan for AMD GPU/ APU and Intel Arc GPU */} - {!isMac && gpuList.length && experimentalEnabled && ( + {!isMac && gpuList.length > 0 && experimentalEnabled && (
From 1c80cb2cd54f9f3781dd4815ea0445f3dfa79474 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 5 Dec 2024 19:00:50 +0800 Subject: [PATCH 10/61] enhancement: default open Jan Input Box Settings and Right panel (#4234) * enhancement: default open Jan Input Box Settings and Right panel * chore: added option getOnInit atomstorage --- web/containers/Providers/Responsive.tsx | 5 ++--- web/helpers/atoms/App.atom.ts | 13 ++++++++++++- web/helpers/atoms/Thread.atom.ts | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/web/containers/Providers/Responsive.tsx b/web/containers/Providers/Responsive.tsx index cb7bd4c1cf..f73fdc970e 100644 --- a/web/containers/Providers/Responsive.tsx +++ b/web/containers/Providers/Responsive.tsx @@ -11,15 +11,14 @@ const Responsive = () => { const [showRightPanel, setShowRightPanel] = useAtom(showRightPanelAtom) // Refs to store the last known state of the panels - const lastLeftPanelState = useRef(true) - const lastRightPanelState = useRef(true) + const lastLeftPanelState = useRef(showLeftPanel) + const lastRightPanelState = useRef(showRightPanel) useEffect(() => { if (matches) { // Store the last known state before closing the panels lastLeftPanelState.current = showLeftPanel lastRightPanelState.current = showRightPanel - setShowLeftPanel(false) setShowRightPanel(false) } else { diff --git a/web/helpers/atoms/App.atom.ts b/web/helpers/atoms/App.atom.ts index 8770b4bcd8..bd1e4f7aae 100644 --- a/web/helpers/atoms/App.atom.ts +++ b/web/helpers/atoms/App.atom.ts @@ -1,14 +1,25 @@ import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + import { MainViewState } from '@/constants/screens' export const mainViewStateAtom = atom(MainViewState.Thread) export const defaultJanDataFolderAtom = atom('') +const SHOW_RIGHT_PANEL = 'showRightPanel' + // Store panel atom export const showLeftPanelAtom = atom(true) -export const showRightPanelAtom = atom(true) + +export const showRightPanelAtom = atomWithStorage( + SHOW_RIGHT_PANEL, + false, + undefined, + { getOnInit: true } +) + export const showSystemMonitorPanelAtom = atom(false) export const appDownloadProgressAtom = atom(-1) export const updateVersionErrorAtom = atom(undefined) diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index e0ea433ce7..7fb6f3c600 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -207,7 +207,7 @@ export const setThreadModelParamsAtom = atom( */ export const activeSettingInputBoxAtom = atomWithStorage( ACTIVE_SETTING_INPUT_BOX, - false, + true, undefined, { getOnInit: true } ) From 3341a3be158d55052cf5d80e1639fdf9da7ae8af Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:47:02 +0700 Subject: [PATCH 11/61] feat: add auto build when PR is approved (#4241) Co-authored-by: Hien To --- .../workflows/jan-electron-build-nightly.yml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/jan-electron-build-nightly.yml b/.github/workflows/jan-electron-build-nightly.yml index 60720052c8..e08a351699 100644 --- a/.github/workflows/jan-electron-build-nightly.yml +++ b/.github/workflows/jan-electron-build-nightly.yml @@ -12,6 +12,8 @@ on: - none - aws-s3 default: none + pull_request_review: + types: [submitted] jobs: set-public-provider: @@ -33,6 +35,9 @@ jobs: elif [ "${{ github.event_name }}" == "push" ]; then echo "::set-output name=public_provider::aws-s3" echo "::set-output name=ref::${{ github.ref }}" + elif [ "${{ github.event_name }}" == "pull_request_review" ]; then + echo "::set-output name=public_provider::none" + echo "::set-output name=ref::${{ github.ref }}" else echo "::set-output name=public_provider::none" echo "::set-output name=ref::${{ github.ref }}" @@ -116,3 +121,24 @@ jobs: build_reason: Manual push_to_branch: dev new_version: ${{ needs.get-update-version.outputs.new_version }} + + + comment-pr-build-url: + needs: [build-macos, build-windows-x64, build-linux-x64, get-update-version, set-public-provider, sync-temp-to-latest] + runs-on: ubuntu-latest + if: github.event_name == 'pull_request_review' + steps: + - name: Set up GitHub CLI + run: | + curl -sSL https://github.com/cli/cli/releases/download/v2.33.0/gh_2.33.0_linux_amd64.tar.gz | tar xz + sudo cp gh_2.33.0_linux_amd64/bin/gh /usr/local/bin/ + + - name: Comment build URL on PR + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_URL=${{ github.event.pull_request.html_url }} + RUN_ID=${{ github.run_id }} + COMMENT="This is the build for this pull request. You can download it from the Artifacts section here: [Build URL](https://github.com/${{ github.repository }}/actions/runs/${RUN_ID})." + gh pr comment $PR_URL --body "$COMMENT" + \ No newline at end of file From 21389070fb362a67e76538eca32ac45528ad8658 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Fri, 6 Dec 2024 16:48:01 +0800 Subject: [PATCH 12/61] fix: performance issue when pasting long content into the chat input box (#4240) --- web/containers/CenterPanelContainer/index.tsx | 27 ++++++++++++++++++- web/containers/Layout/index.tsx | 2 +- web/containers/LeftPanelContainer/index.tsx | 2 +- web/containers/MainViewContainer/index.tsx | 2 +- web/containers/RightPanelContainer/index.tsx | 14 +++++----- .../ChatInput/RichTextEditor.tsx | 27 ++++++++++++++++--- .../ThreadCenterPanel/ChatInput/index.tsx | 2 +- 7 files changed, 61 insertions(+), 15 deletions(-) diff --git a/web/containers/CenterPanelContainer/index.tsx b/web/containers/CenterPanelContainer/index.tsx index 9ce81f184c..fb2518dc77 100644 --- a/web/containers/CenterPanelContainer/index.tsx +++ b/web/containers/CenterPanelContainer/index.tsx @@ -1,15 +1,40 @@ import { PropsWithChildren } from 'react' +import { useMediaQuery } from '@janhq/joi' import { useAtomValue } from 'jotai' import { twMerge } from 'tailwind-merge' +import { MainViewState } from '@/constants/screens' + +import { LEFT_PANEL_WIDTH } from '../LeftPanelContainer' + +import { RIGHT_PANEL_WIDTH } from '../RightPanelContainer' + +import { + mainViewStateAtom, + showLeftPanelAtom, + showRightPanelAtom, +} from '@/helpers/atoms/App.atom' import { reduceTransparentAtom } from '@/helpers/atoms/Setting.atom' const CenterPanelContainer = ({ children }: PropsWithChildren) => { const reduceTransparent = useAtomValue(reduceTransparentAtom) + const matches = useMediaQuery('(max-width: 880px)') + const showLeftPanel = useAtomValue(showLeftPanelAtom) + const showRightPanel = useAtomValue(showRightPanelAtom) + const mainViewState = useAtomValue(mainViewStateAtom) return ( -
+
{ const [leftPanelRef, setLeftPanelRef] = useState(null) diff --git a/web/containers/MainViewContainer/index.tsx b/web/containers/MainViewContainer/index.tsx index ba7f87fd2b..811f19c6e9 100644 --- a/web/containers/MainViewContainer/index.tsx +++ b/web/containers/MainViewContainer/index.tsx @@ -37,7 +37,7 @@ const MainViewContainer = () => { } return ( -
+
{ const [isResizing, setIsResizing] = useState(false) const [threadRightPanelWidth, setRightPanelWidth] = useState( - Number(localStorage.getItem(RIGHT_PANEL_WIDTH)) || DEFAULT_RIGTH_PANEL_WIDTH + Number(localStorage.getItem(RIGHT_PANEL_WIDTH)) || DEFAULT_RIGHT_PANEL_WIDTH ) const [rightPanelRef, setRightPanelRef] = useState( null @@ -55,11 +55,11 @@ const RightPanelContainer = ({ children }: Props) => { mouseMoveEvent.clientX < 200 ) { - setRightPanelWidth(DEFAULT_RIGTH_PANEL_WIDTH) + setRightPanelWidth(DEFAULT_RIGHT_PANEL_WIDTH) setIsResizing(false) localStorage.setItem( RIGHT_PANEL_WIDTH, - String(DEFAULT_RIGTH_PANEL_WIDTH) + String(DEFAULT_RIGHT_PANEL_WIDTH) ) setShowRightPanel(false) } else { @@ -77,8 +77,8 @@ const RightPanelContainer = ({ children }: Props) => { useEffect(() => { if (localStorage.getItem(RIGHT_PANEL_WIDTH) === null) { - setRightPanelWidth(DEFAULT_RIGTH_PANEL_WIDTH) - localStorage.setItem(RIGHT_PANEL_WIDTH, String(DEFAULT_RIGTH_PANEL_WIDTH)) + setRightPanelWidth(DEFAULT_RIGHT_PANEL_WIDTH) + localStorage.setItem(RIGHT_PANEL_WIDTH, String(DEFAULT_RIGHT_PANEL_WIDTH)) } window.addEventListener('mousemove', resize) window.addEventListener('mouseup', stopResizing) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx index 0d477d78dc..7166e39753 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, ClipboardEvent } from 'react' import { MessageStatus } from '@janhq/core' import hljs from 'highlight.js' @@ -67,7 +67,7 @@ const RichTextEditor = ({ placeholder, spellCheck, }: RichTextEditorProps) => { - const [editor] = useState(() => withHistory(withReact(createEditor()))) + const editor = useMemo(() => withHistory(withReact(createEditor())), []) const currentLanguage = useRef('plaintext') const hasStartBackticks = useRef(false) const hasEndBackticks = useRef(false) @@ -80,6 +80,8 @@ const RichTextEditor = ({ const { sendChatMessage } = useSendChatMessage() const { stopInference } = useActiveModel() + const largeContentThreshold = 1000 + // The decorate function identifies code blocks and marks the ranges const decorate = useCallback( (entry: [any, any]) => { @@ -324,6 +326,16 @@ const RichTextEditor = ({ [currentPrompt, editor, messages] ) + const handlePaste = (event: ClipboardEvent) => { + const clipboardData = event.clipboardData || (window as any).clipboardData + const pastedData = clipboardData.getData('text') + + if (pastedData.length > largeContentThreshold) { + event.preventDefault() // Prevent the default paste behavior + Transforms.insertText(editor, pastedData) // Insert the content directly into the editor + } + } + return ( { + // Skip decorate if content exceeds threshold + if ( + currentPrompt.length > largeContentThreshold || + !currentPrompt.length + ) + return [] + return decorate(entry) + }} renderLeaf={renderLeaf} // Pass the renderLeaf function onKeyDown={handleKeyDown} + onPaste={handlePaste} // Add the custom paste handler className={twMerge( className, disabled && diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index 5662cd0c01..fbca6d2904 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -337,7 +337,7 @@ const ChatInput = () => { {activeSettingInputBox && (
Date: Sat, 7 Dec 2024 14:35:03 +0700 Subject: [PATCH 13/61] chore: fix openai vision models --- extensions/inference-openai-extension/package.json | 2 +- extensions/inference-openai-extension/resources/models.json | 4 +++- extensions/inference-openai-extension/src/index.ts | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/extensions/inference-openai-extension/package.json b/extensions/inference-openai-extension/package.json index 9700383d63..d5b2a1d7a6 100644 --- a/extensions/inference-openai-extension/package.json +++ b/extensions/inference-openai-extension/package.json @@ -1,7 +1,7 @@ { "name": "@janhq/inference-openai-extension", "productName": "OpenAI Inference Engine", - "version": "1.0.4", + "version": "1.0.5", "description": "This extension enables OpenAI chat completion API calls", "main": "dist/index.js", "module": "dist/module.js", diff --git a/extensions/inference-openai-extension/resources/models.json b/extensions/inference-openai-extension/resources/models.json index a34bc54604..4d19654bc5 100644 --- a/extensions/inference-openai-extension/resources/models.json +++ b/extensions/inference-openai-extension/resources/models.json @@ -67,7 +67,9 @@ "version": "1.1", "description": "OpenAI GPT 4o is a new flagship model with fast speed and high quality", "format": "api", - "settings": {}, + "settings": { + "vision_model": true + }, "parameters": { "max_tokens": 4096, "temperature": 0.7, diff --git a/extensions/inference-openai-extension/src/index.ts b/extensions/inference-openai-extension/src/index.ts index 18bc4e0aae..2612ed8153 100644 --- a/extensions/inference-openai-extension/src/index.ts +++ b/extensions/inference-openai-extension/src/index.ts @@ -74,6 +74,11 @@ export default class JanInferenceOpenAIExtension extends RemoteOAIEngine { * @returns */ transformPayload = (payload: OpenAIPayloadType): OpenAIPayloadType => { + // Remove empty stop words + if (payload.stop?.length === 0) { + const { stop, ...params } = payload + payload = params + } // Transform the payload for preview models if (this.previewModels.includes(payload.model)) { const { max_tokens, stop, ...params } = payload From 893d6ff40e59b04b8631319cfaa0cbb689bd117e Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 9 Dec 2024 17:18:34 +0800 Subject: [PATCH 14/61] fix: crash markdown render code block without triple backtick (#4248) --- web/package.json | 1 - .../ThreadCenterPanel/TextMessage/MarkdownTextMessage.tsx | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/web/package.json b/web/package.json index c113c3d493..13f646a6fe 100644 --- a/web/package.json +++ b/web/package.json @@ -41,7 +41,6 @@ "rehype-highlight": "^7.0.1", "rehype-highlight-code-lines": "^1.0.4", "rehype-katex": "^7.0.1", - "rehype-raw": "^7.0.0", "remark-math": "^6.0.0", "sass": "^1.69.4", "slate": "latest", diff --git a/web/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage.tsx b/web/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage.tsx index 6b416f1526..76e75f2a28 100644 --- a/web/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage.tsx +++ b/web/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage.tsx @@ -7,8 +7,9 @@ import Markdown from 'react-markdown' import rehypeHighlight from 'rehype-highlight' import rehypeHighlightCodeLines from 'rehype-highlight-code-lines' + import rehypeKatex from 'rehype-katex' -import rehypeRaw from 'rehype-raw' + import remarkMath from 'remark-math' import 'katex/dist/katex.min.css' @@ -198,12 +199,10 @@ export const MarkdownTextMessage = memo( remarkPlugins={[remarkMath]} rehypePlugins={[ [rehypeKatex, { throwOnError: false }], - rehypeRaw, rehypeHighlight, [rehypeHighlightCodeLines, { showLineNumbers: true }], wrapCodeBlocksWithoutVisit, ]} - skipHtml={true} > {text} From c15bb9e9b4e030f3f13036a9301fbdf44bff1d92 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 10 Dec 2024 13:34:47 +0800 Subject: [PATCH 15/61] enhancement: better error handing for remote models when there's no internet connection (#4252) * enhance: better handling failed to fetch * chore: remove console * chore: checking engine showing error failed to fetch * chore: fix linter * chore: fix linter error missing assistant --- web/containers/ErrorMessage/index.tsx | 87 +++++++++++++++++---------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/web/containers/ErrorMessage/index.tsx b/web/containers/ErrorMessage/index.tsx index b2f6bc23af..96ced0ac53 100644 --- a/web/containers/ErrorMessage/index.tsx +++ b/web/containers/ErrorMessage/index.tsx @@ -14,6 +14,8 @@ import ModalTroubleShooting, { import { MainViewState } from '@/constants/screens' +import { isLocalEngine } from '@/utils/modelEngine' + import { mainViewStateAtom } from '@/helpers/atoms/App.atom' import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' @@ -25,30 +27,52 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => { const setSelectedSettingScreen = useSetAtom(selectedSettingAtom) const activeThread = useAtomValue(activeThreadAtom) + const defaultDesc = () => { + return ( + <> +

+ {`Something's wrong.`} Access  + setModalTroubleShooting(true)} + > + troubleshooting assistance + +  now. +

+ + + ) + } + + const getEngine = () => { + const engineName = activeThread?.assistants?.[0]?.model?.engine + return engineName ? EngineManager.instance().get(engineName) : null + } + const getErrorTitle = () => { + const engine = getEngine() + switch (message.error_code) { case ErrorCode.InvalidApiKey: case ErrorCode.AuthenticationError: return ( - - Invalid API key. Please check your API key from{' '} - {' '} - and try again. - + }} + > + Settings + {' '} + and try again. + + {defaultDesc()} + ) default: return ( @@ -56,8 +80,18 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => { data-testid="passthrough-error-message" className="first-letter:uppercase" > - {message.content[0]?.text?.value && ( - + {message.content[0]?.text?.value === 'Failed to fetch' && + engine && + !isLocalEngine(String(engine?.name)) ? ( + + No internet connection.
Switch to an on-device model or + check connection. +
+ ) : ( + <> + + {defaultDesc()} + )}

) @@ -65,24 +99,13 @@ const ErrorMessage = ({ message }: { message: ThreadMessage }) => { } return ( -
+
{message.status === MessageStatus.Error && (
{getErrorTitle()} -

- {`Something's wrong.`} Access  - setModalTroubleShooting(true)} - > - troubleshooting assistance - -  now. -

-
)}
From 09bfc0549ec14f4de2b2bb87cf1381108841e312 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 10 Dec 2024 13:34:59 +0800 Subject: [PATCH 16/61] fix: auto scrolling to bottom (#4256) --- .../ThreadCenterPanel/ChatBody/index.tsx | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx index 9077a351ac..de99ee4138 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatBody/index.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { ThreadMessage } from '@janhq/core' import { useVirtualizer } from '@tanstack/react-virtual' @@ -58,6 +58,8 @@ const ChatBody = memo( }) => { // The scrollable element for your list const parentRef = useRef(null) + const prevScrollTop = useRef(0) + const isUserManuallyScrollingUp = useRef(false) const count = useMemo( () => (messages?.length ?? 0) + (loadModelError ? 1 : 0), @@ -72,27 +74,61 @@ const ChatBody = memo( overscan: 5, }) useEffect(() => { + if (isUserManuallyScrollingUp.current === true || !parentRef.current) + return + if (count > 0 && messages && virtualizer) { virtualizer.scrollToIndex(count - 1) } - }, [count, virtualizer, messages, loadModelError]) + }, [ + count, + virtualizer, + messages, + loadModelError, + isUserManuallyScrollingUp, + ]) const items = virtualizer.getVirtualItems() + virtualizer.shouldAdjustScrollPositionOnItemSizeChange = ( item, _, instance ) => { + if (isUserManuallyScrollingUp.current === true) return false return ( // item.start < (instance.scrollOffset ?? 0) && instance.scrollDirection !== 'backward' ) } + const handleScroll = useCallback((event: React.UIEvent) => { + const currentScrollTop = event.currentTarget.scrollTop + + if (prevScrollTop.current > currentScrollTop) { + isUserManuallyScrollingUp.current = true + } else { + const currentScrollTop = event.currentTarget.scrollTop + const scrollHeight = event.currentTarget.scrollHeight + const clientHeight = event.currentTarget.clientHeight + + if (currentScrollTop + clientHeight >= scrollHeight) { + isUserManuallyScrollingUp.current = false + } + } + + if (isUserManuallyScrollingUp.current === true) { + event.preventDefault() + event.stopPropagation() + } + prevScrollTop.current = currentScrollTop + }, []) + return (
Date: Thu, 12 Dec 2024 10:05:35 +0700 Subject: [PATCH 17/61] chore: 4244 - deprecate groq llama 3.1 70B Versatile --- .../resources/models.json | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/extensions/inference-groq-extension/resources/models.json b/extensions/inference-groq-extension/resources/models.json index 04b60bfdd2..b4b013dad6 100644 --- a/extensions/inference-groq-extension/resources/models.json +++ b/extensions/inference-groq-extension/resources/models.json @@ -61,37 +61,6 @@ }, "engine": "groq" }, - { - "sources": [ - { - "url": "https://groq.com" - } - ], - "id": "llama-3.1-70b-versatile", - "object": "model", - "name": "Groq Llama 3.1 70b Versatile", - "version": "1.1", - "description": "Groq Llama 3.1 70b Versatile with supercharged speed!", - "format": "api", - "settings": {}, - "parameters": { - "max_tokens": 8000, - "temperature": 0.7, - "top_p": 0.95, - "stream": true, - "stop": [], - "frequency_penalty": 0, - "presence_penalty": 0 - }, - "metadata": { - "author": "Meta", - "tags": [ - "General", - "Big Context Length" - ] - }, - "engine": "groq" - }, { "sources": [ { From aac2216aef158f3d3a827d483e31a46feb651e80 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 12 Dec 2024 13:20:39 +0700 Subject: [PATCH 18/61] fix: 4238 - fix default max_tokens set on remote models --- .../inference-openai-extension/resources/models.json | 4 ++-- web/containers/ModelDropdown/index.tsx | 8 ++++++-- web/hooks/useCreateNewThread.ts | 7 +++++-- web/utils/modelEngine.ts | 4 +++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/extensions/inference-openai-extension/resources/models.json b/extensions/inference-openai-extension/resources/models.json index 4d19654bc5..fc68968821 100644 --- a/extensions/inference-openai-extension/resources/models.json +++ b/extensions/inference-openai-extension/resources/models.json @@ -99,10 +99,10 @@ "format": "api", "settings": {}, "parameters": { + "max_tokens": 32768, "temperature": 1, "top_p": 1, "stream": true, - "max_tokens": 32768, "frequency_penalty": 0, "presence_penalty": 0 }, @@ -126,9 +126,9 @@ "format": "api", "settings": {}, "parameters": { + "max_tokens": 65536, "temperature": 1, "top_p": 1, - "max_tokens": 65536, "stream": true, "frequency_penalty": 0, "presence_penalty": 0 diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index dd6caa795c..f6adf090bc 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -192,8 +192,12 @@ const ModelDropdown = ({ model?.settings.ctx_len ?? 8192 ) const overriddenParameters = { - ctx_len: Math.min(8192, model?.settings.ctx_len ?? 8192), - max_tokens: defaultContextLength, + ctx_len: !isLocalEngine(model?.engine) + ? undefined + : defaultContextLength, + max_tokens: !isLocalEngine(model?.engine) + ? (model?.parameters.max_tokens ?? 8192) + : defaultContextLength, } const modelParams = { diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 999c887cbc..63de2d3abb 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -17,6 +17,7 @@ import { fileUploadAtom } from '@/containers/Providers/Jotai' import { toaster } from '@/containers/Toast' +import { isLocalEngine } from '@/utils/modelEngine' import { generateThreadId } from '@/utils/thread' import { useActiveModel } from './useActiveModel' @@ -113,12 +114,14 @@ export const useCreateNewThread = () => { ) const overriddenSettings = { - ctx_len: defaultContextLength, + ctx_len: !isLocalEngine(model?.engine) ? undefined : defaultContextLength, } // Use ctx length by default const overriddenParameters = { - max_tokens: defaultContextLength, + max_tokens: !isLocalEngine(model?.engine) + ? (model?.parameters.token_limit ?? 8192) + : defaultContextLength, } const createdAt = Date.now() diff --git a/web/utils/modelEngine.ts b/web/utils/modelEngine.ts index 2ac4a1acdc..d87d8d3826 100644 --- a/web/utils/modelEngine.ts +++ b/web/utils/modelEngine.ts @@ -38,7 +38,9 @@ export const getLogoEngine = (engine: InferenceEngine) => { * @param engine * @returns */ -export const isLocalEngine = (engine: string) => { +export const isLocalEngine = (engine?: string) => { + if (!engine) return false + const engineObj = EngineManager.instance().get(engine) if (!engineObj) return false return ( From 137ba0789bd38ee812676e53e13ae959e024daf9 Mon Sep 17 00:00:00 2001 From: cuhong Date: Thu, 12 Dec 2024 15:30:14 +0900 Subject: [PATCH 19/61] Add check for isComposing to ensure events are not triggered prematurely. --- .../Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx index 7166e39753..f784e7ac03 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -314,7 +314,7 @@ const RichTextEditor = ({ const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { - if (event.key === 'Enter' && !event.shiftKey) { + if (event.key === 'Enter' && !event.shiftKey && event.nativeEvent.isComposing === false) { event.preventDefault() if (messages[messages.length - 1]?.status !== MessageStatus.Pending) { sendChatMessage(currentPrompt) From d8689e2bf83a29319cd89e2f9c9f57eab49d8300 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:50:34 +0700 Subject: [PATCH 20/61] chore: add ci janhq/core publish npm (#4259) Co-authored-by: Hien To --- .github/workflows/publish-npm-core.yml | 53 ++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/publish-npm-core.yml diff --git a/.github/workflows/publish-npm-core.yml b/.github/workflows/publish-npm-core.yml new file mode 100644 index 0000000000..b6d4009579 --- /dev/null +++ b/.github/workflows/publish-npm-core.yml @@ -0,0 +1,53 @@ +name: Publish plugin models Package to npmjs +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+-core"] + paths: ["core/**"] + pull_request: + paths: ["core/**"] +jobs: + build-and-publish-plugins: + environment: production + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: "0" + token: ${{ secrets.PAT_SERVICE_ACCOUNT }} + + - name: Install jq + uses: dcarbone/install-jq-action@v2.0.1 + + - name: Extract tag name without v prefix + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV && echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" + env: + GITHUB_REF: ${{ github.ref }} + + - name: "Get Semantic Version from tag" + if: github.event_name == 'push' + run: | + # Get the tag from the event + tag=${GITHUB_REF#refs/tags/v} + # remove the -core suffix + new_version=$(echo $tag | sed -n 's/-core//p') + echo $new_version + # Replace the old version with the new version in package.json + jq --arg version "$new_version" '.version = $version' core/package.json > /tmp/package.json && mv /tmp/package.json core/package.json + + # Print the new version + echo "Updated package.json version to: $new_version" + cat core/package.json + + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v3 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + + - run: cd core && yarn install && yarn build + + - run: cd core && yarn publish --access public + if: github.event_name == 'push' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From 14737b7e31cb54ab6f1e82404e0200012a50dd8b Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 12 Dec 2024 15:44:54 +0800 Subject: [PATCH 21/61] fix: markdown inline inputbox (#4269) --- .../ChatInput/RichTextEditor.tsx | 91 +------------------ 1 file changed, 1 insertion(+), 90 deletions(-) diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx index 7166e39753..7f96fc5b5c 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/RichTextEditor.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, ClipboardEvent } from 'react' import { MessageStatus } from '@janhq/core' -import hljs from 'highlight.js' import { useAtom, useAtomValue } from 'jotai' import { BaseEditor, createEditor, Editor, Transforms } from 'slate' @@ -134,97 +133,9 @@ const RichTextEditor = ({ }) } - if (Editor.isBlock(editor, node) && node.type === 'paragraph') { - node.children.forEach((child: { text: any }, childIndex: number) => { - const text = child.text - - const codeBlockStartRegex = /```(\w*)/g - const matches = [...currentPrompt.matchAll(codeBlockStartRegex)] - - if (matches.length % 2 !== 0) { - hasEndBackticks.current = false - } - - // Match code block start and end - const lang = text.match(/^```(\w*)$/) - const endMatch = text.match(/^```$/) - - if (lang) { - // If it's the start of a code block, store the language - currentLanguage.current = lang[1] || 'plaintext' - } else if (endMatch) { - // Reset language when code block ends - currentLanguage.current = 'plaintext' - } else if ( - hasStartBackticks.current && - hasEndBackticks.current && - currentLanguage.current !== 'plaintext' - ) { - // Highlight entire code line if in a code block - - const codeContent = text.trim() // Remove leading spaces for highlighting - - let highlighted = '' - highlighted = hljs.highlightAuto(codeContent).value - try { - highlighted = hljs.highlight(codeContent, { - language: - currentLanguage.current.length > 1 - ? currentLanguage.current - : 'plaintext', - }).value - } catch (err) { - highlighted = hljs.highlight(codeContent, { - language: 'javascript', - }).value - } - - const parser = new DOMParser() - const doc = parser.parseFromString(highlighted, 'text/html') - - let slateTextIndex = 0 - - doc.body.childNodes.forEach((childNode) => { - const childText = childNode.textContent || '' - - const length = childText.length - const className = - childNode.nodeType === Node.ELEMENT_NODE - ? (childNode as HTMLElement).className - : '' - - ranges.push({ - anchor: { - path: [...path, childIndex], - offset: slateTextIndex, - }, - focus: { - path: [...path, childIndex], - offset: slateTextIndex + length, - }, - type: 'code', - code: true, - language: currentLanguage.current, - className, - }) - - slateTextIndex += length - }) - } else { - currentLanguage.current = 'plaintext' - ranges.push({ - anchor: { path: [...path, childIndex], offset: 0 }, - focus: { path: [...path, childIndex], offset: text.length }, - type: 'paragraph', // Treat as a paragraph - code: false, - }) - } - }) - } - return ranges }, - [currentPrompt, editor] + [editor] ) // RenderLeaf applies the decoration styles From 174f1c7dcb01efa0532f9f5526013621a6adccac Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 5 Dec 2024 17:33:43 +0700 Subject: [PATCH 22/61] feat: reroute threads and messages requests to the backend --- core/src/browser/extensions/conversational.ts | 29 +- .../browser/extensions/engines/AIEngine.ts | 1 - core/src/node/api/restful/helper/builder.ts | 5 +- core/src/types/message/messageInterface.ts | 20 +- core/src/types/thread/threadInterface.ts | 14 +- .../conversational-extension/package.json | 8 +- .../src/@types/global.d.ts | 14 + .../src/Conversational.test.ts | 408 ------------------ .../conversational-extension/src/index.ts | 358 ++++++--------- .../conversational-extension/src/jsonUtil.ts | 14 - .../webpack.config.js | 7 +- web/containers/ErrorMessage/index.tsx | 4 +- web/containers/ModelDropdown/index.tsx | 24 +- web/containers/Providers/ModelHandler.tsx | 42 +- web/helpers/atoms/Assistant.atom.ts | 10 +- web/helpers/atoms/ChatMessage.atom.ts | 15 +- web/hooks/useActiveModel.ts | 6 +- web/hooks/useCreateNewThread.ts | 105 +++-- web/hooks/useDeleteThread.ts | 144 +++---- web/hooks/usePath.ts | 4 +- web/hooks/useRecommendedModel.ts | 6 +- web/hooks/useSendChatMessage.ts | 102 +++-- web/hooks/useSetActiveThread.ts | 41 +- web/hooks/useThreads.ts | 2 +- web/hooks/useUpdateModelParameters.ts | 67 ++- .../AssistantSetting/index.tsx | 29 +- .../ThreadCenterPanel/ChatInput/index.tsx | 38 +- .../ThreadCenterPanel/EditChatInput/index.tsx | 24 +- .../LoadModelError/index.tsx | 8 +- .../MessageToolbar/index.tsx | 14 +- .../ThreadCenterPanel/TextMessage/index.tsx | 8 +- .../Thread/ThreadCenterPanel/index.tsx | 30 +- .../ModalEditTitleThread/index.tsx | 14 +- web/screens/Thread/ThreadLeftPanel/index.tsx | 18 +- .../Thread/ThreadRightPanel/Tools/index.tsx | 235 +++++----- web/screens/Thread/ThreadRightPanel/index.tsx | 32 +- 36 files changed, 777 insertions(+), 1123 deletions(-) create mode 100644 extensions/conversational-extension/src/@types/global.d.ts delete mode 100644 extensions/conversational-extension/src/Conversational.test.ts delete mode 100644 extensions/conversational-extension/src/jsonUtil.ts diff --git a/core/src/browser/extensions/conversational.ts b/core/src/browser/extensions/conversational.ts index ec53fbbbf9..49fedd5448 100644 --- a/core/src/browser/extensions/conversational.ts +++ b/core/src/browser/extensions/conversational.ts @@ -1,4 +1,10 @@ -import { Thread, ThreadInterface, ThreadMessage, MessageInterface } from '../../types' +import { + Thread, + ThreadInterface, + ThreadMessage, + MessageInterface, + ThreadAssistantInfo, +} from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' /** @@ -17,10 +23,21 @@ export abstract class ConversationalExtension return ExtensionTypeEnum.Conversational } - abstract getThreads(): Promise - abstract saveThread(thread: Thread): Promise + abstract listThreads(): Promise + abstract createThread(thread: Partial): Promise + abstract modifyThread(thread: Thread): Promise abstract deleteThread(threadId: string): Promise - abstract addNewMessage(message: ThreadMessage): Promise - abstract writeMessages(threadId: string, messages: ThreadMessage[]): Promise - abstract getAllMessages(threadId: string): Promise + abstract createMessage(message: Partial): Promise + abstract deleteMessage(threadId: string, messageId: string): Promise + abstract listMessages(threadId: string): Promise + abstract getThreadAssistant(threadId: string): Promise + abstract createThreadAssistant( + threadId: string, + assistant: ThreadAssistantInfo + ): Promise + abstract modifyThreadAssistant( + threadId: string, + assistant: ThreadAssistantInfo + ): Promise + abstract modifyMessage(message: ThreadMessage): Promise } diff --git a/core/src/browser/extensions/engines/AIEngine.ts b/core/src/browser/extensions/engines/AIEngine.ts index d0528b0abf..2d1bdb3c2f 100644 --- a/core/src/browser/extensions/engines/AIEngine.ts +++ b/core/src/browser/extensions/engines/AIEngine.ts @@ -2,7 +2,6 @@ import { events } from '../../events' import { BaseExtension } from '../../extension' import { MessageRequest, Model, ModelEvent } from '../../../types' import { EngineManager } from './EngineManager' -import { ModelManager } from '../../models/manager' /** * Base AIEngine diff --git a/core/src/node/api/restful/helper/builder.ts b/core/src/node/api/restful/helper/builder.ts index e081708cfe..230eb64ab0 100644 --- a/core/src/node/api/restful/helper/builder.ts +++ b/core/src/node/api/restful/helper/builder.ts @@ -6,7 +6,6 @@ import { mkdirSync, appendFileSync, createWriteStream, - rmdirSync, } from 'fs' import { JanApiRouteConfiguration, RouteConfiguration } from './configuration' import { join } from 'path' @@ -126,7 +125,7 @@ export const createThread = async (thread: any) => { } } - const threadId = generateThreadId(thread.assistants[0].assistant_id) + const threadId = generateThreadId(thread.assistants[0]?.assistant_id) try { const updatedThread = { ...thread, @@ -280,7 +279,7 @@ export const models = async (request: any, reply: any) => { 'Content-Type': 'application/json', } - const response = await fetch(`${CORTEX_API_URL}/models${request.url.split('/models')[1] ?? ""}`, { + const response = await fetch(`${CORTEX_API_URL}/models${request.url.split('/models')[1] ?? ''}`, { method: request.method, headers: headers, body: JSON.stringify(request.body), diff --git a/core/src/types/message/messageInterface.ts b/core/src/types/message/messageInterface.ts index f6579da88b..1ea04298a0 100644 --- a/core/src/types/message/messageInterface.ts +++ b/core/src/types/message/messageInterface.ts @@ -11,20 +11,20 @@ export interface MessageInterface { * @param {ThreadMessage} message - The message to be added. * @returns {Promise} A promise that resolves when the message has been added. */ - addNewMessage(message: ThreadMessage): Promise - - /** - * Writes an array of messages to a specific thread. - * @param {string} threadId - The ID of the thread to write the messages to. - * @param {ThreadMessage[]} messages - The array of messages to be written. - * @returns {Promise} A promise that resolves when the messages have been written. - */ - writeMessages(threadId: string, messages: ThreadMessage[]): Promise + createMessage(message: ThreadMessage): Promise /** * Retrieves all messages from a specific thread. * @param {string} threadId - The ID of the thread to retrieve the messages from. * @returns {Promise} A promise that resolves to an array of messages from the thread. */ - getAllMessages(threadId: string): Promise + listMessages(threadId: string): Promise + + /** + * Deletes a specific message from a thread. + * @param {string} threadId - The ID of the thread from which the message will be deleted. + * @param {string} messageId - The ID of the message to be deleted. + * @returns {Promise} A promise that resolves when the message has been successfully deleted. + */ + deleteMessage(threadId: string, messageId: string): Promise } diff --git a/core/src/types/thread/threadInterface.ts b/core/src/types/thread/threadInterface.ts index 792c8c8a5f..4a78812c6a 100644 --- a/core/src/types/thread/threadInterface.ts +++ b/core/src/types/thread/threadInterface.ts @@ -11,15 +11,23 @@ export interface ThreadInterface { * @abstract * @returns {Promise} A promise that resolves to an array of threads. */ - getThreads(): Promise + listThreads(): Promise /** - * Saves a thread. + * Create a thread. * @abstract * @param {Thread} thread - The thread to save. * @returns {Promise} A promise that resolves when the thread is saved. */ - saveThread(thread: Thread): Promise + createThread(thread: Thread): Promise + + /** + * modify a thread. + * @abstract + * @param {Thread} thread - The thread to save. + * @returns {Promise} A promise that resolves when the thread is saved. + */ + modifyThread(thread: Thread): Promise /** * Deletes a thread. diff --git a/extensions/conversational-extension/package.json b/extensions/conversational-extension/package.json index 036fcfab25..ea30064490 100644 --- a/extensions/conversational-extension/package.json +++ b/extensions/conversational-extension/package.json @@ -18,12 +18,14 @@ "devDependencies": { "cpx": "^1.5.0", "rimraf": "^3.0.2", + "ts-loader": "^9.5.0", "webpack": "^5.88.2", - "webpack-cli": "^5.1.4", - "ts-loader": "^9.5.0" + "webpack-cli": "^5.1.4" }, "dependencies": { - "@janhq/core": "file:../../core" + "@janhq/core": "file:../../core", + "ky": "^1.7.2", + "p-queue": "^8.0.1" }, "engines": { "node": ">=18.0.0" diff --git a/extensions/conversational-extension/src/@types/global.d.ts b/extensions/conversational-extension/src/@types/global.d.ts new file mode 100644 index 0000000000..757b5eebf3 --- /dev/null +++ b/extensions/conversational-extension/src/@types/global.d.ts @@ -0,0 +1,14 @@ +export {} +declare global { + declare const API_URL: string + declare const SOCKET_URL: string + + interface Core { + api: APIFunctions + events: EventEmitter + } + interface Window { + core?: Core | undefined + electronAPI?: any | undefined + } +} diff --git a/extensions/conversational-extension/src/Conversational.test.ts b/extensions/conversational-extension/src/Conversational.test.ts deleted file mode 100644 index 3d1d6fc607..0000000000 --- a/extensions/conversational-extension/src/Conversational.test.ts +++ /dev/null @@ -1,408 +0,0 @@ -/** - * @jest-environment jsdom - */ -jest.mock('@janhq/core', () => ({ - ...jest.requireActual('@janhq/core/node'), - fs: { - existsSync: jest.fn(), - mkdir: jest.fn(), - writeFileSync: jest.fn(), - readdirSync: jest.fn(), - readFileSync: jest.fn(), - appendFileSync: jest.fn(), - rm: jest.fn(), - writeBlob: jest.fn(), - joinPath: jest.fn(), - fileStat: jest.fn(), - }, - joinPath: jest.fn(), - ConversationalExtension: jest.fn(), -})) - -import { fs } from '@janhq/core' - -import JSONConversationalExtension from '.' - -describe('JSONConversationalExtension Tests', () => { - let extension: JSONConversationalExtension - - beforeEach(() => { - // @ts-ignore - extension = new JSONConversationalExtension() - }) - - it('should create thread folder on load if it does not exist', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - - await extension.onLoad() - - expect(mkdirSpy).toHaveBeenCalledWith('file://threads') - }) - - it('should log message on unload', () => { - const consoleSpy = jest.spyOn(console, 'debug').mockImplementation() - - extension.onUnload() - - expect(consoleSpy).toHaveBeenCalledWith( - 'JSONConversationalExtension unloaded' - ) - }) - - it('should return sorted threads', async () => { - jest - .spyOn(extension, 'getValidThreadDirs') - .mockResolvedValue(['dir1', 'dir2']) - jest - .spyOn(extension, 'readThread') - .mockResolvedValueOnce({ updated: '2023-01-01' }) - .mockResolvedValueOnce({ updated: '2023-01-02' }) - - const threads = await extension.getThreads() - - expect(threads).toEqual([ - { updated: '2023-01-02' }, - { updated: '2023-01-01' }, - ]) - }) - - it('should ignore broken threads', async () => { - jest - .spyOn(extension, 'getValidThreadDirs') - .mockResolvedValue(['dir1', 'dir2']) - jest - .spyOn(extension, 'readThread') - .mockResolvedValueOnce(JSON.stringify({ updated: '2023-01-01' })) - .mockResolvedValueOnce('this_is_an_invalid_json_content') - - const threads = await extension.getThreads() - - expect(threads).toEqual([{ updated: '2023-01-01' }]) - }) - - it('should save thread', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const writeFileSyncSpy = jest - .spyOn(fs, 'writeFileSync') - .mockResolvedValue({}) - - const thread = { id: '1', updated: '2023-01-01' } as any - await extension.saveThread(thread) - - expect(mkdirSpy).toHaveBeenCalled() - expect(writeFileSyncSpy).toHaveBeenCalled() - }) - - it('should delete thread', async () => { - const rmSpy = jest.spyOn(fs, 'rm').mockResolvedValue({}) - - await extension.deleteThread('1') - - expect(rmSpy).toHaveBeenCalled() - }) - - it('should add new message', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const appendFileSyncSpy = jest - .spyOn(fs, 'appendFileSync') - .mockResolvedValue({}) - - const message = { - thread_id: '1', - content: [{ type: 'text', text: { annotations: [] } }], - } as any - await extension.addNewMessage(message) - - expect(mkdirSpy).toHaveBeenCalled() - expect(appendFileSyncSpy).toHaveBeenCalled() - }) - - it('should store image', async () => { - const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - await extension.storeImage( - 'data:image/png;base64,abcd', - 'path/to/image.png' - ) - - expect(writeBlobSpy).toHaveBeenCalled() - }) - - it('should store file', async () => { - const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - await extension.storeFile( - 'data:application/pdf;base64,abcd', - 'path/to/file.pdf' - ) - - expect(writeBlobSpy).toHaveBeenCalled() - }) - - it('should write messages', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const writeFileSyncSpy = jest - .spyOn(fs, 'writeFileSync') - .mockResolvedValue({}) - - const messages = [{ id: '1', thread_id: '1', content: [] }] as any - await extension.writeMessages('1', messages) - - expect(mkdirSpy).toHaveBeenCalled() - expect(writeFileSyncSpy).toHaveBeenCalled() - }) - - it('should get all messages on string response', async () => { - jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl']) - jest.spyOn(fs, 'readFileSync').mockResolvedValue('{"id":"1"}\n{"id":"2"}\n') - - const messages = await extension.getAllMessages('1') - - expect(messages).toEqual([{ id: '1' }, { id: '2' }]) - }) - - it('should get all messages on object response', async () => { - jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl']) - jest.spyOn(fs, 'readFileSync').mockResolvedValue({ id: 1 }) - - const messages = await extension.getAllMessages('1') - - expect(messages).toEqual([{ id: 1 }]) - }) - - it('get all messages return empty on error', async () => { - jest.spyOn(fs, 'readdirSync').mockRejectedValue(['messages.jsonl']) - - const messages = await extension.getAllMessages('1') - - expect(messages).toEqual([]) - }) - - it('return empty messages on no messages file', async () => { - jest.spyOn(fs, 'readdirSync').mockResolvedValue([]) - - const messages = await extension.getAllMessages('1') - - expect(messages).toEqual([]) - }) - - it('should ignore error message', async () => { - jest.spyOn(fs, 'readdirSync').mockResolvedValue(['messages.jsonl']) - jest - .spyOn(fs, 'readFileSync') - .mockResolvedValue('{"id":"1"}\nyolo\n{"id":"2"}\n') - - const messages = await extension.getAllMessages('1') - - expect(messages).toEqual([{ id: '1' }, { id: '2' }]) - }) - - it('should create thread folder on load if it does not exist', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - - await extension.onLoad() - - expect(mkdirSpy).toHaveBeenCalledWith('file://threads') - }) - - it('should log message on unload', () => { - const consoleSpy = jest.spyOn(console, 'debug').mockImplementation() - - extension.onUnload() - - expect(consoleSpy).toHaveBeenCalledWith( - 'JSONConversationalExtension unloaded' - ) - }) - - it('should return sorted threads', async () => { - jest - .spyOn(extension, 'getValidThreadDirs') - .mockResolvedValue(['dir1', 'dir2']) - jest - .spyOn(extension, 'readThread') - .mockResolvedValueOnce({ updated: '2023-01-01' }) - .mockResolvedValueOnce({ updated: '2023-01-02' }) - - const threads = await extension.getThreads() - - expect(threads).toEqual([ - { updated: '2023-01-02' }, - { updated: '2023-01-01' }, - ]) - }) - - it('should ignore broken threads', async () => { - jest - .spyOn(extension, 'getValidThreadDirs') - .mockResolvedValue(['dir1', 'dir2']) - jest - .spyOn(extension, 'readThread') - .mockResolvedValueOnce(JSON.stringify({ updated: '2023-01-01' })) - .mockResolvedValueOnce('this_is_an_invalid_json_content') - - const threads = await extension.getThreads() - - expect(threads).toEqual([{ updated: '2023-01-01' }]) - }) - - it('should save thread', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const writeFileSyncSpy = jest - .spyOn(fs, 'writeFileSync') - .mockResolvedValue({}) - - const thread = { id: '1', updated: '2023-01-01' } as any - await extension.saveThread(thread) - - expect(mkdirSpy).toHaveBeenCalled() - expect(writeFileSyncSpy).toHaveBeenCalled() - }) - - it('should delete thread', async () => { - const rmSpy = jest.spyOn(fs, 'rm').mockResolvedValue({}) - - await extension.deleteThread('1') - - expect(rmSpy).toHaveBeenCalled() - }) - - it('should add new message', async () => { - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(false) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const appendFileSyncSpy = jest - .spyOn(fs, 'appendFileSync') - .mockResolvedValue({}) - - const message = { - thread_id: '1', - content: [{ type: 'text', text: { annotations: [] } }], - } as any - await extension.addNewMessage(message) - - expect(mkdirSpy).toHaveBeenCalled() - expect(appendFileSyncSpy).toHaveBeenCalled() - }) - - it('should add new image message', async () => { - jest - .spyOn(fs, 'existsSync') - // @ts-ignore - .mockResolvedValueOnce(false) - // @ts-ignore - .mockResolvedValueOnce(false) - // @ts-ignore - .mockResolvedValueOnce(true) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const appendFileSyncSpy = jest - .spyOn(fs, 'appendFileSync') - .mockResolvedValue({}) - jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - const message = { - thread_id: '1', - content: [ - { type: 'image', text: { annotations: ['data:image;base64,hehe'] } }, - ], - } as any - await extension.addNewMessage(message) - - expect(mkdirSpy).toHaveBeenCalled() - expect(appendFileSyncSpy).toHaveBeenCalled() - }) - - it('should add new pdf message', async () => { - jest - .spyOn(fs, 'existsSync') - // @ts-ignore - .mockResolvedValueOnce(false) - // @ts-ignore - .mockResolvedValueOnce(false) - // @ts-ignore - .mockResolvedValueOnce(true) - const mkdirSpy = jest.spyOn(fs, 'mkdir').mockResolvedValue({}) - const appendFileSyncSpy = jest - .spyOn(fs, 'appendFileSync') - .mockResolvedValue({}) - jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - const message = { - thread_id: '1', - content: [ - { type: 'pdf', text: { annotations: ['data:pdf;base64,hehe'] } }, - ], - } as any - await extension.addNewMessage(message) - - expect(mkdirSpy).toHaveBeenCalled() - expect(appendFileSyncSpy).toHaveBeenCalled() - }) - - it('should store image', async () => { - const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - await extension.storeImage( - 'data:image/png;base64,abcd', - 'path/to/image.png' - ) - - expect(writeBlobSpy).toHaveBeenCalled() - }) - - it('should store file', async () => { - const writeBlobSpy = jest.spyOn(fs, 'writeBlob').mockResolvedValue({}) - - await extension.storeFile( - 'data:application/pdf;base64,abcd', - 'path/to/file.pdf' - ) - - expect(writeBlobSpy).toHaveBeenCalled() - }) -}) - -describe('test readThread', () => { - let extension: JSONConversationalExtension - - beforeEach(() => { - // @ts-ignore - extension = new JSONConversationalExtension() - }) - - it('should read thread', async () => { - jest - .spyOn(fs, 'readFileSync') - .mockResolvedValue(JSON.stringify({ id: '1' })) - const thread = await extension.readThread('1') - expect(thread).toEqual(`{"id":"1"}`) - }) - - it('getValidThreadDirs should return valid thread directories', async () => { - jest - .spyOn(fs, 'readdirSync') - .mockResolvedValueOnce(['1', '2', '3']) - .mockResolvedValueOnce(['thread.json']) - .mockResolvedValueOnce(['thread.json']) - .mockResolvedValueOnce([]) - // @ts-ignore - jest.spyOn(fs, 'existsSync').mockResolvedValue(true) - jest.spyOn(fs, 'fileStat').mockResolvedValue({ - isDirectory: true, - } as any) - const validThreadDirs = await extension.getValidThreadDirs() - expect(validThreadDirs).toEqual(['1', '2']) - }) -}) diff --git a/extensions/conversational-extension/src/index.ts b/extensions/conversational-extension/src/index.ts index b34f09181d..81d9d2023a 100644 --- a/extensions/conversational-extension/src/index.ts +++ b/extensions/conversational-extension/src/index.ts @@ -1,90 +1,71 @@ import { - fs, - joinPath, ConversationalExtension, Thread, + ThreadAssistantInfo, ThreadMessage, } from '@janhq/core' -import { safelyParseJSON } from './jsonUtil' +import ky from 'ky' +import PQueue from 'p-queue' + +type ThreadList = { + data: Thread[] +} + +type MessageList = { + data: ThreadMessage[] +} /** * JSONConversationalExtension is a ConversationalExtension implementation that provides * functionality for managing threads. */ export default class JSONConversationalExtension extends ConversationalExtension { - private static readonly _threadFolder = 'file://threads' - private static readonly _threadInfoFileName = 'thread.json' - private static readonly _threadMessagesFileName = 'messages.jsonl' + queue = new PQueue({ concurrency: 1 }) /** * Called when the extension is loaded. */ async onLoad() { - if (!(await fs.existsSync(JSONConversationalExtension._threadFolder))) { - await fs.mkdir(JSONConversationalExtension._threadFolder) - } + this.queue.add(() => this.healthz()) } /** * Called when the extension is unloaded. */ - onUnload() { - console.debug('JSONConversationalExtension unloaded') - } + onUnload() {} /** * Returns a Promise that resolves to an array of Conversation objects. */ - async getThreads(): Promise { - try { - const threadDirs = await this.getValidThreadDirs() - - const promises = threadDirs.map((dirName) => this.readThread(dirName)) - const promiseResults = await Promise.allSettled(promises) - const convos = promiseResults - .map((result) => { - if (result.status === 'fulfilled') { - return typeof result.value === 'object' - ? result.value - : safelyParseJSON(result.value) - } - return undefined - }) - .filter((convo) => !!convo) - convos.sort( - (a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime() - ) - - return convos - } catch (error) { - console.error(error) - return [] - } + async listThreads(): Promise { + return this.queue.add(() => + ky + .get(`${API_URL}/v1/threads`) + .json() + .then((e) => e.data) + ) as Promise } /** * Saves a Thread object to a json file. * @param thread The Thread object to save. */ - async saveThread(thread: Thread): Promise { - try { - const threadDirPath = await joinPath([ - JSONConversationalExtension._threadFolder, - thread.id, - ]) - const threadJsonPath = await joinPath([ - threadDirPath, - JSONConversationalExtension._threadInfoFileName, - ]) - if (!(await fs.existsSync(threadDirPath))) { - await fs.mkdir(threadDirPath) - } + async createThread(thread: Thread): Promise { + return this.queue.add(() => + ky.post(`${API_URL}/v1/threads`, { json: thread }).json() + ) as Promise + } - await fs.writeFileSync(threadJsonPath, JSON.stringify(thread, null, 2)) - } catch (err) { - console.error(err) - Promise.reject(err) - } + /** + * Saves a Thread object to a json file. + * @param thread The Thread object to save. + */ + async modifyThread(thread: Thread): Promise { + return this.queue + .add(() => + ky.post(`${API_URL}/v1/threads/${thread.id}`, { json: thread }) + ) + .then() } /** @@ -92,189 +73,126 @@ export default class JSONConversationalExtension extends ConversationalExtension * @param threadId The ID of the thread to delete. */ async deleteThread(threadId: string): Promise { - const path = await joinPath([ - JSONConversationalExtension._threadFolder, - `${threadId}`, - ]) - try { - await fs.rm(path) - } catch (err) { - console.error(err) - } + return this.queue + .add(() => ky.delete(`${API_URL}/v1/threads/${threadId}`)) + .then() } - async addNewMessage(message: ThreadMessage): Promise { - try { - const threadDirPath = await joinPath([ - JSONConversationalExtension._threadFolder, - message.thread_id, - ]) - const threadMessagePath = await joinPath([ - threadDirPath, - JSONConversationalExtension._threadMessagesFileName, - ]) - if (!(await fs.existsSync(threadDirPath))) await fs.mkdir(threadDirPath) - - if (message.content[0]?.type === 'image') { - const filesPath = await joinPath([threadDirPath, 'files']) - if (!(await fs.existsSync(filesPath))) await fs.mkdir(filesPath) - - const imagePath = await joinPath([filesPath, `${message.id}.png`]) - const base64 = message.content[0].text.annotations[0] - await this.storeImage(base64, imagePath) - if ((await fs.existsSync(imagePath)) && message.content?.length) { - // Use file path instead of blob - message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.png` - } - } - - if (message.content[0]?.type === 'pdf') { - const filesPath = await joinPath([threadDirPath, 'files']) - if (!(await fs.existsSync(filesPath))) await fs.mkdir(filesPath) - - const filePath = await joinPath([filesPath, `${message.id}.pdf`]) - const blob = message.content[0].text.annotations[0] - await this.storeFile(blob, filePath) - - if ((await fs.existsSync(filePath)) && message.content?.length) { - // Use file path instead of blob - message.content[0].text.annotations[0] = `threads/${message.thread_id}/files/${message.id}.pdf` - } - } - await fs.appendFileSync(threadMessagePath, JSON.stringify(message) + '\n') - Promise.resolve() - } catch (err) { - Promise.reject(err) - } - } - - async storeImage(base64: string, filePath: string): Promise { - const base64Data = base64.replace(/^data:image\/\w+;base64,/, '') - - try { - await fs.writeBlob(filePath, base64Data) - } catch (err) { - console.error(err) - } + /** + * Adds a new message to a specified thread. + * @param message The ThreadMessage object to be added. + * @returns A Promise that resolves when the message has been added. + */ + async createMessage(message: ThreadMessage): Promise { + return this.queue.add(() => + ky + .post(`${API_URL}/v1/threads/${message.thread_id}/messages`, { + json: message, + }) + .json() + ) as Promise } - async storeFile(base64: string, filePath: string): Promise { - const base64Data = base64.replace(/^data:application\/pdf;base64,/, '') - try { - await fs.writeBlob(filePath, base64Data) - } catch (err) { - console.error(err) - } + /** + * Modifies a message in a thread. + * @param message + * @returns + */ + async modifyMessage(message: ThreadMessage): Promise { + return this.queue.add(() => + ky + .post( + `${API_URL}/v1/threads/${message.thread_id}/messages/${message.id}`, + { + json: message, + } + ) + .json() + ) as Promise } - async writeMessages( - threadId: string, - messages: ThreadMessage[] - ): Promise { - try { - const threadDirPath = await joinPath([ - JSONConversationalExtension._threadFolder, - threadId, - ]) - const threadMessagePath = await joinPath([ - threadDirPath, - JSONConversationalExtension._threadMessagesFileName, - ]) - if (!(await fs.existsSync(threadDirPath))) await fs.mkdir(threadDirPath) - await fs.writeFileSync( - threadMessagePath, - messages.map((msg) => JSON.stringify(msg)).join('\n') + - (messages.length ? '\n' : '') + /** + * Deletes a specific message from a thread. + * @param threadId The ID of the thread containing the message. + * @param messageId The ID of the message to be deleted. + * @returns A Promise that resolves when the message has been successfully deleted. + */ + async deleteMessage(threadId: string, messageId: string): Promise { + return this.queue + .add(() => + ky.delete(`${API_URL}/v1/threads/${threadId}/messages/${messageId}`) ) - Promise.resolve() - } catch (err) { - Promise.reject(err) - } + .then() } /** - * A promise builder for reading a thread from a file. - * @param threadDirName the thread dir we are reading from. - * @returns data of the thread + * Retrieves all messages for a specified thread. + * @param threadId The ID of the thread to get messages from. + * @returns A Promise that resolves to an array of ThreadMessage objects. */ - async readThread(threadDirName: string): Promise { - return fs.readFileSync( - await joinPath([ - JSONConversationalExtension._threadFolder, - threadDirName, - JSONConversationalExtension._threadInfoFileName, - ]), - 'utf-8' - ) + async listMessages(threadId: string): Promise { + return this.queue.add(() => + ky + .get(`${API_URL}/v1/threads/${threadId}/messages?order=asc`) + .json() + .then((e) => e.data) + ) as Promise } /** - * Returns a Promise that resolves to an array of thread directories. - * @private + * Retrieves the assistant information for a specified thread. + * @param threadId The ID of the thread for which to retrieve assistant information. + * @returns A Promise that resolves to a ThreadAssistantInfo object containing + * the details of the assistant associated with the specified thread. */ - async getValidThreadDirs(): Promise { - const fileInsideThread: string[] = await fs.readdirSync( - JSONConversationalExtension._threadFolder - ) - - const threadDirs: string[] = [] - for (let i = 0; i < fileInsideThread.length; i++) { - const path = await joinPath([ - JSONConversationalExtension._threadFolder, - fileInsideThread[i], - ]) - if (!(await fs.fileStat(path))?.isDirectory) continue - - const isHavingThreadInfo = (await fs.readdirSync(path)).includes( - JSONConversationalExtension._threadInfoFileName - ) - if (!isHavingThreadInfo) { - console.debug(`Ignore ${path} because it does not have thread info`) - continue - } - - threadDirs.push(fileInsideThread[i]) - } - return threadDirs + async getThreadAssistant(threadId: string): Promise { + return this.queue.add(() => + ky.get(`${API_URL}/v1/assistants/${threadId}`).json() + ) as Promise + } + /** + * Creates a new assistant for the specified thread. + * @param threadId The ID of the thread for which the assistant is being created. + * @param assistant The information about the assistant to be created. + * @returns A Promise that resolves to the newly created ThreadAssistantInfo object. + */ + async createThreadAssistant( + threadId: string, + assistant: ThreadAssistantInfo + ): Promise { + return this.queue.add(() => + ky + .post(`${API_URL}/v1/assistants/${threadId}`, { json: assistant }) + .json() + ) as Promise } - async getAllMessages(threadId: string): Promise { - try { - const threadDirPath = await joinPath([ - JSONConversationalExtension._threadFolder, - threadId, - ]) - - const files: string[] = await fs.readdirSync(threadDirPath) - if ( - !files.includes(JSONConversationalExtension._threadMessagesFileName) - ) { - console.debug(`${threadDirPath} not contains message file`) - return [] - } - - const messageFilePath = await joinPath([ - threadDirPath, - JSONConversationalExtension._threadMessagesFileName, - ]) - - let readResult = await fs.readFileSync(messageFilePath, 'utf-8') - - if (typeof readResult === 'object') { - readResult = JSON.stringify(readResult) - } - - const result = readResult.split('\n').filter((line) => line !== '') + /** + * Modifies an existing assistant for the specified thread. + * @param threadId The ID of the thread for which the assistant is being modified. + * @param assistant The updated information for the assistant. + * @returns A Promise that resolves to the updated ThreadAssistantInfo object. + */ + async modifyThreadAssistant( + threadId: string, + assistant: ThreadAssistantInfo + ): Promise { + return this.queue.add(() => + ky + .patch(`${API_URL}/v1/assistants/${threadId}`, { json: assistant }) + .json() + ) as Promise + } - const messages: ThreadMessage[] = [] - result.forEach((line: string) => { - const message = safelyParseJSON(line) - if (message) messages.push(safelyParseJSON(line)) + /** + * Do health check on cortex.cpp + * @returns + */ + healthz(): Promise { + return ky + .get(`${API_URL}/healthz`, { + retry: { limit: 20, delay: () => 500, methods: ['get'] }, }) - return messages - } catch (err) { - console.error(err) - return [] - } + .then(() => {}) } } diff --git a/extensions/conversational-extension/src/jsonUtil.ts b/extensions/conversational-extension/src/jsonUtil.ts deleted file mode 100644 index 7f83cadce5..0000000000 --- a/extensions/conversational-extension/src/jsonUtil.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Note about performance -// The v8 JavaScript engine used by Node.js cannot optimise functions which contain a try/catch block. -// v8 4.5 and above can optimise try/catch -export function safelyParseJSON(json) { - // This function cannot be optimised, it's best to - // keep it small! - var parsed - try { - parsed = JSON.parse(json) - } catch (e) { - return undefined - } - return parsed // Could be undefined! -} diff --git a/extensions/conversational-extension/webpack.config.js b/extensions/conversational-extension/webpack.config.js index e4a0b2179e..0448af4212 100644 --- a/extensions/conversational-extension/webpack.config.js +++ b/extensions/conversational-extension/webpack.config.js @@ -17,7 +17,12 @@ module.exports = { filename: 'index.js', // Adjust the output file name as needed library: { type: 'module' }, // Specify ESM output format }, - plugins: [new webpack.DefinePlugin({})], + plugins: [ + new webpack.DefinePlugin({ + API_URL: JSON.stringify('http://127.0.0.1:39291'), + SOCKET_URL: JSON.stringify('ws://127.0.0.1:39291'), + }), + ], resolve: { extensions: ['.ts', '.js'], }, diff --git a/web/containers/ErrorMessage/index.tsx b/web/containers/ErrorMessage/index.tsx index 96ced0ac53..b572da6364 100644 --- a/web/containers/ErrorMessage/index.tsx +++ b/web/containers/ErrorMessage/index.tsx @@ -18,14 +18,14 @@ import { isLocalEngine } from '@/utils/modelEngine' import { mainViewStateAtom } from '@/helpers/atoms/App.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' -import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const ErrorMessage = ({ message }: { message: ThreadMessage }) => { const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom) const setMainState = useSetAtom(mainViewStateAtom) const setSelectedSettingScreen = useSetAtom(selectedSettingAtom) - const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const defaultDesc = () => { return ( diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index dd6caa795c..1f0364dd10 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -46,6 +46,7 @@ import { import { extensionManager } from '@/extension' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { inActiveEngineProviderAtom } from '@/helpers/atoms/Extension.atom' import { configuredModelsAtom, @@ -75,6 +76,7 @@ const ModelDropdown = ({ const [searchText, setSearchText] = useState('') const [open, setOpen] = useState(false) const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const downloadingModels = useAtomValue(getDownloadingModelAtom) const [toggle, setToggle] = useState(null) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) @@ -151,17 +153,24 @@ const ModelDropdown = ({ useEffect(() => { if (!activeThread) return - const modelId = activeThread?.assistants?.[0]?.model?.id + const modelId = activeAssistant?.model?.id let model = downloadedModels.find((model) => model.id === modelId) if (!model) { model = recommendedModel } setSelectedModel(model) - }, [recommendedModel, activeThread, downloadedModels, setSelectedModel]) + }, [ + recommendedModel, + activeThread, + downloadedModels, + setSelectedModel, + activeAssistant?.model?.id, + ]) const onClickModelItem = useCallback( async (modelId: string) => { + if (!activeAssistant) return const model = downloadedModels.find((m) => m.id === modelId) setSelectedModel(model) setOpen(false) @@ -172,14 +181,14 @@ const ModelDropdown = ({ ...activeThread, assistants: [ { - ...activeThread.assistants[0], + ...activeAssistant, tools: [ { type: 'retrieval', enabled: isModelSupportRagAndTools(model as Model), settings: { - ...(activeThread.assistants[0].tools && - activeThread.assistants[0].tools[0]?.settings), + ...(activeAssistant.tools && + activeAssistant.tools[0]?.settings), }, }, ], @@ -215,13 +224,14 @@ const ModelDropdown = ({ } }, [ + activeAssistant, downloadedModels, - activeThread, setSelectedModel, + activeThread, + updateThreadMetadata, isModelSupportRagAndTools, setThreadModelParams, updateModelParameter, - updateThreadMetadata, ] ) diff --git a/web/containers/Providers/ModelHandler.tsx b/web/containers/Providers/ModelHandler.tsx index 373c0aebd6..9066a2d1f7 100644 --- a/web/containers/Providers/ModelHandler.tsx +++ b/web/containers/Providers/ModelHandler.tsx @@ -1,4 +1,4 @@ -import { Fragment, useCallback, useEffect, useRef } from 'react' +import { Fragment, use, useCallback, useEffect, useRef } from 'react' import { ChatCompletionMessage, @@ -31,6 +31,7 @@ import { addNewMessageAtom, updateMessageAtom, tokenSpeedAtom, + deleteMessageAtom, } from '@/helpers/atoms/ChatMessage.atom' import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { @@ -49,6 +50,7 @@ export default function ModelHandler() { const addNewMessage = useSetAtom(addNewMessageAtom) const updateMessage = useSetAtom(updateMessageAtom) const downloadedModels = useAtomValue(downloadedModelsAtom) + const deleteMessage = useSetAtom(deleteMessageAtom) const activeModel = useAtomValue(activeModelAtom) const setActiveModel = useSetAtom(activeModelAtom) const setStateModel = useSetAtom(stateModelAtom) @@ -86,7 +88,7 @@ export default function ModelHandler() { }, [activeModelParams]) const onNewMessageResponse = useCallback( - (message: ThreadMessage) => { + async (message: ThreadMessage) => { if (message.type === MessageRequestType.Thread) { addNewMessage(message) } @@ -154,12 +156,15 @@ export default function ModelHandler() { ...thread, title: cleanedMessageContent, - metadata: thread.metadata, + metadata: { + ...thread.metadata, + title: cleanedMessageContent, + }, } extensionManager .get(ExtensionTypeEnum.Conversational) - ?.saveThread({ + ?.modifyThread({ ...updatedThread, }) .then(() => { @@ -233,7 +238,9 @@ export default function ModelHandler() { const thread = threadsRef.current?.find((e) => e.id == message.thread_id) if (!thread) return + const messageContent = message.content[0]?.text?.value + const metadata = { ...thread.metadata, ...(messageContent && { lastMessage: messageContent }), @@ -246,15 +253,19 @@ export default function ModelHandler() { extensionManager .get(ExtensionTypeEnum.Conversational) - ?.saveThread({ + ?.modifyThread({ ...thread, metadata, }) - - // If this is not the summary of the Thread, don't need to add it to the Thread - extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.addNewMessage(message) + ;(async () => { + const updatedMessage = await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.createMessage(message) + if (updatedMessage) { + deleteMessage(message.id) + addNewMessage(updatedMessage) + } + })() // Attempt to generate the title of the Thread when needed generateThreadTitle(message, thread) @@ -279,7 +290,9 @@ export default function ModelHandler() { const generateThreadTitle = (message: ThreadMessage, thread: Thread) => { // If this is the first ever prompt in the thread - if (thread.title?.trim() !== defaultThreadTitle) { + if ( + (thread.title ?? thread.metadata?.title)?.trim() !== defaultThreadTitle + ) { return } @@ -292,11 +305,14 @@ export default function ModelHandler() { const updatedThread: Thread = { ...thread, title: (thread.metadata?.lastMessage as string) || defaultThreadTitle, - metadata: thread.metadata, + metadata: { + ...thread.metadata, + title: (thread.metadata?.lastMessage as string) || defaultThreadTitle, + }, } return extensionManager .get(ExtensionTypeEnum.Conversational) - ?.saveThread({ + ?.modifyThread({ ...updatedThread, }) .then(() => { diff --git a/web/helpers/atoms/Assistant.atom.ts b/web/helpers/atoms/Assistant.atom.ts index d44703cf41..cb50a0553e 100644 --- a/web/helpers/atoms/Assistant.atom.ts +++ b/web/helpers/atoms/Assistant.atom.ts @@ -1,4 +1,12 @@ -import { Assistant } from '@janhq/core' +import { Assistant, ThreadAssistantInfo } from '@janhq/core' import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' export const assistantsAtom = atom([]) + +/** + * Get the current active assistant + */ +export const activeAssistantAtom = atomWithStorage< + ThreadAssistantInfo | undefined +>('activeAssistant', undefined, undefined, { getOnInit: true }) diff --git a/web/helpers/atoms/ChatMessage.atom.ts b/web/helpers/atoms/ChatMessage.atom.ts index 1f6099a2e0..b0ec6c4936 100644 --- a/web/helpers/atoms/ChatMessage.atom.ts +++ b/web/helpers/atoms/ChatMessage.atom.ts @@ -6,6 +6,8 @@ import { } from '@janhq/core' import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + import { getActiveThreadIdAtom, updateThreadStateLastMessageAtom, @@ -13,15 +15,23 @@ import { import { TokenSpeed } from '@/types/token' +const CHAT_MESSAGE_NAME = 'chatMessages' /** * Stores all chat messages for all threads */ -export const chatMessages = atom>({}) +export const chatMessages = atomWithStorage>( + CHAT_MESSAGE_NAME, + {}, + undefined, + { getOnInit: true } +) /** * Stores the status of the messages load for each thread */ -export const readyThreadsMessagesAtom = atom>({}) +export const readyThreadsMessagesAtom = atomWithStorage< + Record +>('currentThreadMessages', {}, undefined, { getOnInit: true }) /** * Store the token speed for current message @@ -34,6 +44,7 @@ export const getCurrentChatMessagesAtom = atom((get) => { const activeThreadId = get(getActiveThreadIdAtom) if (!activeThreadId) return [] const messages = get(chatMessages)[activeThreadId] + if (!Array.isArray(messages)) return [] return messages ?? [] }) diff --git a/web/hooks/useActiveModel.ts b/web/hooks/useActiveModel.ts index 63513bee28..e436d116e0 100644 --- a/web/hooks/useActiveModel.ts +++ b/web/hooks/useActiveModel.ts @@ -8,6 +8,7 @@ import { toaster } from '@/containers/Toast' import { LAST_USED_MODEL_ID } from './useRecommendedModel' import { vulkanEnabledAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' @@ -34,6 +35,7 @@ export function useActiveModel() { const setLoadModelError = useSetAtom(loadModelErrorAtom) const pendingModelLoad = useRef(false) const isVulkanEnabled = useAtomValue(vulkanEnabledAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const downloadedModelsRef = useRef([]) @@ -79,12 +81,12 @@ export function useActiveModel() { } /// Apply thread model settings - if (activeThread?.assistants[0]?.model.id === modelId) { + if (activeAssistant?.model.id === modelId) { model = { ...model, settings: { ...model.settings, - ...activeThread.assistants[0].model.settings, + ...activeAssistant?.model.settings, }, } } diff --git a/web/hooks/useCreateNewThread.ts b/web/hooks/useCreateNewThread.ts index 999c887cbc..41fa5e5467 100644 --- a/web/hooks/useCreateNewThread.ts +++ b/web/hooks/useCreateNewThread.ts @@ -1,7 +1,6 @@ import { useCallback } from 'react' import { - Assistant, ConversationalExtension, ExtensionTypeEnum, Thread, @@ -9,16 +8,17 @@ import { ThreadState, AssistantTool, Model, + Assistant, } from '@janhq/core' -import { atom, useAtomValue, useSetAtom } from 'jotai' +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' + +import { useDebouncedCallback } from 'use-debounce' import { copyOverInstructionEnabledAtom } from '@/containers/CopyInstruction' import { fileUploadAtom } from '@/containers/Providers/Jotai' import { toaster } from '@/containers/Toast' -import { generateThreadId } from '@/utils/thread' - import { useActiveModel } from './useActiveModel' import useRecommendedModel from './useRecommendedModel' @@ -27,6 +27,7 @@ import useSetActiveThread from './useSetActiveThread' import { extensionManager } from '@/extension' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { threadsAtom, @@ -34,7 +35,6 @@ import { updateThreadAtom, setThreadModelParamsAtom, isGeneratingResponseAtom, - activeThreadAtom, } from '@/helpers/atoms/Thread.atom' const createNewThreadAtom = atom(null, (get, set, newThread: Thread) => { @@ -64,7 +64,7 @@ export const useCreateNewThread = () => { const copyOverInstructionEnabled = useAtomValue( copyOverInstructionEnabledAtom ) - const activeThread = useAtomValue(activeThreadAtom) + const [activeAssistant, setActiveAssistant] = useAtom(activeAssistantAtom) const experimentalEnabled = useAtomValue(experimentalFeatureEnabledAtom) const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) @@ -75,7 +75,7 @@ export const useCreateNewThread = () => { const { stopInference } = useActiveModel() const requestCreateNewThread = async ( - assistant: Assistant, + assistant: (ThreadAssistantInfo & { id: string; name: string }) | Assistant, model?: Model | undefined ) => { // Stop generating if any @@ -124,7 +124,7 @@ export const useCreateNewThread = () => { const createdAt = Date.now() let instructions: string | undefined = assistant.instructions if (copyOverInstructionEnabled) { - instructions = activeThread?.assistants[0]?.instructions ?? undefined + instructions = activeAssistant?.instructions ?? undefined } const assistantInfo: ThreadAssistantInfo = { assistant_id: assistant.id, @@ -139,46 +139,95 @@ export const useCreateNewThread = () => { instructions, } - const threadId = generateThreadId(assistant.id) - const thread: Thread = { - id: threadId, + const thread: Partial = { object: 'thread', title: 'New Thread', assistants: [assistantInfo], created: createdAt, updated: createdAt, + metadata: { + title: 'New Thread', + }, } // add the new thread on top of the thread list to the state //TODO: Why do we have thread list then thread states? Should combine them - createNewThread(thread) - - setSelectedModel(defaultModel) - setThreadModelParams(thread.id, { - ...defaultModel?.settings, - ...defaultModel?.parameters, - ...overriddenSettings, - }) + try { + const createdThread = await persistNewThread(thread, assistantInfo) + if (!createdThread) throw 'Thread creation failed' + createNewThread(createdThread) + + setSelectedModel(defaultModel) + setThreadModelParams(createdThread.id, { + ...defaultModel?.settings, + ...defaultModel?.parameters, + ...overriddenSettings, + }) + + // Delete the file upload state + setFileUpload([]) + setActiveThread(createdThread) + } catch (ex) { + return toaster({ + title: 'Thread created failed.', + description: `To avoid piling up empty threads, please reuse previous one before creating new.`, + type: 'error', + }) + } + } - // Delete the file upload state - setFileUpload([]) - // Update thread metadata - await updateThreadMetadata(thread) + const updateThreadExtension = (thread: Thread) => { + return extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.modifyThread(thread) + } - setActiveThread(thread) + const updateAssistantExtension = ( + threadId: string, + assistant: ThreadAssistantInfo + ) => { + return extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.modifyThreadAssistant(threadId, assistant) } + const updateThreadCallback = useDebouncedCallback(updateThreadExtension, 300) + const updateAssistantCallback = useDebouncedCallback( + updateAssistantExtension, + 300 + ) + const updateThreadMetadata = useCallback( async (thread: Thread) => { updateThread(thread) - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.saveThread(thread) + setActiveAssistant(thread.assistants[0]) + updateThreadCallback(thread) + updateAssistantCallback(thread.id, thread.assistants[0]) }, - [updateThread] + [ + updateThread, + setActiveAssistant, + updateThreadCallback, + updateAssistantCallback, + ] ) + const persistNewThread = async ( + thread: Partial, + assistantInfo: ThreadAssistantInfo + ): Promise => { + return await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.createThread(thread) + .then(async (thread) => { + await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.createThreadAssistant(thread.id, assistantInfo) + return thread + }) + } + return { requestCreateNewThread, updateThreadMetadata, diff --git a/web/hooks/useDeleteThread.ts b/web/hooks/useDeleteThread.ts index 69e51228f1..7b98a4ea5c 100644 --- a/web/hooks/useDeleteThread.ts +++ b/web/hooks/useDeleteThread.ts @@ -1,13 +1,6 @@ import { useCallback } from 'react' -import { - ChatCompletionRole, - ExtensionTypeEnum, - ConversationalExtension, - fs, - joinPath, - Thread, -} from '@janhq/core' +import { ExtensionTypeEnum, ConversationalExtension } from '@janhq/core' import { useAtom, useAtomValue, useSetAtom } from 'jotai' @@ -15,89 +8,63 @@ import { currentPromptAtom } from '@/containers/Providers/Jotai' import { toaster } from '@/containers/Toast' +import { useCreateNewThread } from './useCreateNewThread' + import { extensionManager } from '@/extension/ExtensionManager' -import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' -import { - chatMessages, - cleanChatMessageAtom as cleanChatMessagesAtom, - deleteChatMessageAtom as deleteChatMessagesAtom, -} from '@/helpers/atoms/ChatMessage.atom' +import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' +import { deleteChatMessageAtom as deleteChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' +import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { threadsAtom, setActiveThreadIdAtom, deleteThreadStateAtom, - updateThreadStateLastMessageAtom, - updateThreadAtom, } from '@/helpers/atoms/Thread.atom' export default function useDeleteThread() { const [threads, setThreads] = useAtom(threadsAtom) - const messages = useAtomValue(chatMessages) - const janDataFolderPath = useAtomValue(janDataFolderPathAtom) + const { requestCreateNewThread } = useCreateNewThread() + const assistants = useAtomValue(assistantsAtom) + const models = useAtomValue(downloadedModelsAtom) const setCurrentPrompt = useSetAtom(currentPromptAtom) const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const deleteMessages = useSetAtom(deleteChatMessagesAtom) - const cleanMessages = useSetAtom(cleanChatMessagesAtom) const deleteThreadState = useSetAtom(deleteThreadStateAtom) - const updateThreadLastMessage = useSetAtom(updateThreadStateLastMessageAtom) - const updateThread = useSetAtom(updateThreadAtom) const cleanThread = useCallback( async (threadId: string) => { - cleanMessages(threadId) const thread = threads.find((c) => c.id === threadId) if (!thread) return - - const updatedMessages = (messages[threadId] ?? []).filter( - (msg) => msg.role === ChatCompletionRole.System - ) - - // remove files - try { - const threadFolderPath = await joinPath([ - janDataFolderPath, - 'threads', - threadId, - ]) - const threadFilesPath = await joinPath([threadFolderPath, 'files']) - const threadMemoryPath = await joinPath([threadFolderPath, 'memory']) - await fs.rm(threadFilesPath) - await fs.rm(threadMemoryPath) - } catch (err) { - console.warn('Error deleting thread files', err) - } - - await extensionManager + const assistantInfo = await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.writeMessages(threadId, updatedMessages) - - thread.metadata = { - ...thread.metadata, - } - - const updatedThread: Thread = { - ...thread, - title: 'New Thread', - metadata: { ...thread.metadata, lastMessage: undefined }, - } - + ?.getThreadAssistant(thread.id) + + if (!assistantInfo) return + const model = models.find((c) => c.id === assistantInfo?.model?.id) + + requestCreateNewThread( + { + ...assistantInfo, + id: assistants[0].id, + name: assistants[0].name, + }, + model + ? { + ...model, + parameters: assistantInfo?.model?.parameters ?? {}, + settings: assistantInfo?.model?.settings ?? {}, + } + : undefined + ) + // Delete this thread await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.saveThread(updatedThread) - updateThreadLastMessage(threadId, undefined) - updateThread(updatedThread) + ?.deleteThread(threadId) + .catch(console.error) }, - [ - cleanMessages, - threads, - messages, - updateThreadLastMessage, - updateThread, - janDataFolderPath, - ] + [assistants, models, requestCreateNewThread, threads] ) const deleteThread = async (threadId: string) => { @@ -105,30 +72,27 @@ export default function useDeleteThread() { alert('No active thread') return } - try { - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.deleteThread(threadId) - const availableThreads = threads.filter((c) => c.id !== threadId) - setThreads(availableThreads) - - // delete the thread state - deleteThreadState(threadId) - - deleteMessages(threadId) - setCurrentPrompt('') - toaster({ - title: 'Thread successfully deleted.', - description: `Thread ${threadId} has been successfully deleted.`, - type: 'success', - }) - if (availableThreads.length > 0) { - setActiveThreadId(availableThreads[0].id) - } else { - setActiveThreadId(undefined) - } - } catch (err) { - console.error(err) + await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.deleteThread(threadId) + .catch(console.error) + const availableThreads = threads.filter((c) => c.id !== threadId) + setThreads(availableThreads) + + // delete the thread state + deleteThreadState(threadId) + + deleteMessages(threadId) + setCurrentPrompt('') + toaster({ + title: 'Thread successfully deleted.', + description: `Thread ${threadId} has been successfully deleted.`, + type: 'success', + }) + if (availableThreads.length > 0) { + setActiveThreadId(availableThreads[0].id) + } else { + setActiveThreadId(undefined) } } diff --git a/web/hooks/usePath.ts b/web/hooks/usePath.ts index b732926a69..afdafe11ff 100644 --- a/web/hooks/usePath.ts +++ b/web/hooks/usePath.ts @@ -2,6 +2,7 @@ import { openFileExplorer, joinPath, baseName } from '@janhq/core' import { useAtomValue } from 'jotai' import { janDataFolderPathAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' @@ -9,13 +10,14 @@ export const usePath = () => { const janDataFolderPath = useAtomValue(janDataFolderPathAtom) const activeThread = useAtomValue(activeThreadAtom) const selectedModel = useAtomValue(selectedModelAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const onRevealInFinder = async (type: string) => { // TODO: this logic should be refactored. if (type !== 'Model' && !activeThread) return let filePath = undefined - const assistantId = activeThread?.assistants[0]?.assistant_id + const assistantId = activeAssistant?.assistant_id switch (type) { case 'Engine': case 'Thread': diff --git a/web/hooks/useRecommendedModel.ts b/web/hooks/useRecommendedModel.ts index d5bf0aba73..e1702701bb 100644 --- a/web/hooks/useRecommendedModel.ts +++ b/web/hooks/useRecommendedModel.ts @@ -6,6 +6,7 @@ import { atom, useAtomValue } from 'jotai' import { activeModelAtom } from './useActiveModel' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { downloadedModelsAtom } from '@/helpers/atoms/Model.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' @@ -28,6 +29,7 @@ export default function useRecommendedModel() { const [recommendedModel, setRecommendedModel] = useState() const activeThread = useAtomValue(activeThreadAtom) const downloadedModels = useAtomValue(downloadedModelsAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const getAndSortDownloadedModels = useCallback(async (): Promise => { const models = downloadedModels.sort((a, b) => @@ -45,8 +47,8 @@ export default function useRecommendedModel() { > => { const models = await getAndSortDownloadedModels() - if (!activeThread) return - const modelId = activeThread.assistants[0]?.model.id + if (!activeThread || !activeAssistant) return + const modelId = activeAssistant.model.id const model = models.find((model) => model.id === modelId) if (model) { diff --git a/web/hooks/useSendChatMessage.ts b/web/hooks/useSendChatMessage.ts index dc9a52f1be..86c2e41b67 100644 --- a/web/hooks/useSendChatMessage.ts +++ b/web/hooks/useSendChatMessage.ts @@ -10,6 +10,7 @@ import { ConversationalExtension, EngineManager, ToolManager, + ThreadAssistantInfo, } from '@janhq/core' import { extractInferenceParams, extractModelLoadParams } from '@janhq/core' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' @@ -28,6 +29,7 @@ import { ThreadMessageBuilder } from '@/utils/threadMessageBuilder' import { useActiveModel } from './useActiveModel' import { extensionManager } from '@/extension/ExtensionManager' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { addNewMessageAtom, deleteMessageAtom, @@ -48,6 +50,7 @@ export const reloadModelAtom = atom(false) export default function useSendChatMessage() { const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const addNewMessage = useSetAtom(addNewMessageAtom) const updateThread = useSetAtom(updateThreadAtom) const updateThreadWaiting = useSetAtom(updateThreadWaitingForResponseAtom) @@ -68,6 +71,7 @@ export default function useSendChatMessage() { const [fileUpload, setFileUpload] = useAtom(fileUploadAtom) const setIsGeneratingResponse = useSetAtom(isGeneratingResponseAtom) const activeThreadRef = useRef() + const activeAssistantRef = useRef() const setTokenSpeed = useSetAtom(tokenSpeedAtom) const selectedModelRef = useRef() @@ -84,36 +88,37 @@ export default function useSendChatMessage() { selectedModelRef.current = selectedModel }, [selectedModel]) - const resendChatMessage = async (currentMessage: ThreadMessage) => { + useEffect(() => { + activeAssistantRef.current = activeAssistant + }, [activeAssistant]) + + const resendChatMessage = async () => { // Delete last response before regenerating - const newConvoData = currentMessages - let toSendMessage = currentMessage - - do { - deleteMessage(currentMessage.id) - const msg = newConvoData.pop() - if (!msg) break - toSendMessage = msg - deleteMessage(toSendMessage.id ?? '') - } while (toSendMessage.role !== ChatCompletionRole.User) + const newConvoData = Array.from(currentMessages) + let toSendMessage = newConvoData.pop() - if (activeThreadRef.current) { + while (toSendMessage && toSendMessage?.role !== ChatCompletionRole.User) { await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.writeMessages(activeThreadRef.current.id, newConvoData) + ?.deleteMessage(toSendMessage.thread_id, toSendMessage.id) + .catch(console.error) + deleteMessage(toSendMessage.id ?? '') + toSendMessage = newConvoData.pop() } - sendChatMessage(toSendMessage.content[0]?.text.value) + if (toSendMessage?.content[0]?.text?.value) + sendChatMessage(toSendMessage.content[0].text.value, true) } const sendChatMessage = async ( message: string, + isResend: boolean = false, messages?: ThreadMessage[] ) => { if (!message || message.trim().length === 0) return - if (!activeThreadRef.current) { - console.error('No active thread') + if (!activeThreadRef.current || !activeAssistantRef.current) { + console.error('No active thread or assistant') return } @@ -139,11 +144,11 @@ export default function useSendChatMessage() { } const modelRequest = - selectedModelRef?.current ?? activeThreadRef.current.assistants[0].model + selectedModelRef?.current ?? activeAssistantRef.current?.model // Fallback support for previous broken threads - if (activeThreadRef.current?.assistants[0]?.model?.id === '*') { - activeThreadRef.current.assistants[0].model = { + if (activeAssistantRef.current?.model?.id === '*') { + activeAssistantRef.current.model = { id: modelRequest.id, settings: modelRequest.settings, parameters: modelRequest.parameters, @@ -163,46 +168,49 @@ export default function useSendChatMessage() { }, activeThreadRef.current, messages ?? currentMessages - ).addSystemMessage(activeThreadRef.current.assistants[0].instructions) - - requestBuilder.pushMessage(prompt, base64Blob, fileUpload[0]?.type) - - // Build Thread Message to persist - const threadMessageBuilder = new ThreadMessageBuilder( - requestBuilder - ).pushMessage(prompt, base64Blob, fileUpload) + ).addSystemMessage(activeAssistantRef.current?.instructions) + + if (!isResend) { + requestBuilder.pushMessage(prompt, base64Blob, fileUpload[0]?.type) + + // Build Thread Message to persist + const threadMessageBuilder = new ThreadMessageBuilder( + requestBuilder + ).pushMessage(prompt, base64Blob, fileUpload) + + const newMessage = threadMessageBuilder.build() + + // Update thread state + const updatedThread: Thread = { + ...activeThreadRef.current, + updated: newMessage.created, + metadata: { + ...activeThreadRef.current.metadata, + lastMessage: prompt, + }, + } + updateThread(updatedThread) - const newMessage = threadMessageBuilder.build() + // Add message + const createdMessage = await extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.createMessage(newMessage) - // Push to states - addNewMessage(newMessage) + if (!createdMessage) return - // Update thread state - const updatedThread: Thread = { - ...activeThreadRef.current, - updated: newMessage.created, - metadata: { - ...activeThreadRef.current.metadata, - lastMessage: prompt, - }, + // Push to states + addNewMessage(createdMessage) } - updateThread(updatedThread) - - // Add message - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.addNewMessage(newMessage) // Start Model if not started const modelId = - selectedModelRef.current?.id ?? - activeThreadRef.current.assistants[0].model.id + selectedModelRef.current?.id ?? activeAssistantRef.current?.model.id if (base64Blob) { setFileUpload([]) } - if (modelRef.current?.id !== modelId) { + if (modelRef.current?.id !== modelId && modelId) { const error = await startModel(modelId).catch((error: Error) => error) if (error) { updateThreadWaiting(activeThreadRef.current.id, false) diff --git a/web/hooks/useSetActiveThread.ts b/web/hooks/useSetActiveThread.ts index 6b306224db..8c7ed5361b 100644 --- a/web/hooks/useSetActiveThread.ts +++ b/web/hooks/useSetActiveThread.ts @@ -1,12 +1,10 @@ import { ExtensionTypeEnum, Thread, ConversationalExtension } from '@janhq/core' -import { useAtomValue, useSetAtom } from 'jotai' +import { useSetAtom } from 'jotai' import { extensionManager } from '@/extension' -import { - readyThreadsMessagesAtom, - setConvoMessagesAtom, -} from '@/helpers/atoms/ChatMessage.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' +import { setConvoMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { setActiveThreadIdAtom, setThreadModelParamsAtom, @@ -17,21 +15,27 @@ export default function useSetActiveThread() { const setActiveThreadId = useSetAtom(setActiveThreadIdAtom) const setThreadMessage = useSetAtom(setConvoMessagesAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) - const readyMessageThreads = useAtomValue(readyThreadsMessagesAtom) + const setActiveAssistant = useSetAtom(activeAssistantAtom) const setActiveThread = async (thread: Thread) => { - // Load local messages only if there are no messages in the state - if (!readyMessageThreads[thread?.id]) { - const messages = await getLocalThreadMessage(thread?.id) - setThreadMessage(thread?.id, messages) - } + if (!thread?.id) return setActiveThreadId(thread?.id) - const modelParams: ModelParams = { - ...thread?.assistants[0]?.model?.parameters, - ...thread?.assistants[0]?.model?.settings, + + try { + const assistantInfo = await getThreadAssistant(thread.id) + setActiveAssistant(assistantInfo) + // Load local messages only if there are no messages in the state + const messages = await getLocalThreadMessage(thread.id).catch(() => []) + const modelParams: ModelParams = { + ...assistantInfo?.model?.parameters, + ...assistantInfo?.model?.settings, + } + setThreadModelParams(thread?.id, modelParams) + setThreadMessage(thread.id, messages) + } catch (e) { + console.error(e) } - setThreadModelParams(thread?.id, modelParams) } return { setActiveThread } @@ -40,4 +44,9 @@ export default function useSetActiveThread() { const getLocalThreadMessage = async (threadId: string) => extensionManager .get(ExtensionTypeEnum.Conversational) - ?.getAllMessages(threadId) ?? [] + ?.listMessages(threadId) ?? [] + +const getThreadAssistant = async (threadId: string) => + extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.getThreadAssistant(threadId) diff --git a/web/hooks/useThreads.ts b/web/hooks/useThreads.ts index 9366101c3a..1e3b428a9f 100644 --- a/web/hooks/useThreads.ts +++ b/web/hooks/useThreads.ts @@ -68,6 +68,6 @@ const useThreads = () => { const getLocalThreads = async (): Promise => (await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.getThreads()) ?? [] + ?.listThreads()) ?? [] export default useThreads diff --git a/web/hooks/useUpdateModelParameters.ts b/web/hooks/useUpdateModelParameters.ts index 6eb7c3c5a9..977ebd10be 100644 --- a/web/hooks/useUpdateModelParameters.ts +++ b/web/hooks/useUpdateModelParameters.ts @@ -12,7 +12,10 @@ import { import { useAtom, useAtomValue, useSetAtom } from 'jotai' +import { useDebouncedCallback } from 'use-debounce' + import { extensionManager } from '@/extension' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { getActiveThreadModelParamsAtom, @@ -29,11 +32,28 @@ export type UpdateModelParameter = { export default function useUpdateModelParameters() { const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) + const [activeAssistant, setActiveAssistant] = useAtom(activeAssistantAtom) const [selectedModel] = useAtom(selectedModelAtom) const setThreadModelParams = useSetAtom(setThreadModelParamsAtom) + const updateAssistantExtension = ( + threadId: string, + assistant: ThreadAssistantInfo + ) => { + return extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.modifyThreadAssistant(threadId, assistant) + } + + const updateAssistantCallback = useDebouncedCallback( + updateAssistantExtension, + 300 + ) + const updateModelParameter = useCallback( async (thread: Thread, settings: UpdateModelParameter) => { + if (!activeAssistant) return + const toUpdateSettings = processStopWords(settings.params ?? {}) const updatedModelParams = settings.modelId ? toUpdateSettings @@ -48,30 +68,33 @@ export default function useUpdateModelParameters() { setThreadModelParams(thread.id, updatedModelParams) const runtimeParams = extractInferenceParams(updatedModelParams) const settingParams = extractModelLoadParams(updatedModelParams) - - const assistants = thread.assistants.map( - (assistant: ThreadAssistantInfo) => { - assistant.model.parameters = runtimeParams - assistant.model.settings = settingParams - if (selectedModel) { - assistant.model.id = settings.modelId ?? selectedModel?.id - assistant.model.engine = settings.engine ?? selectedModel?.engine - } - return assistant - } - ) - - // update thread - const updatedThread: Thread = { - ...thread, - assistants, + const assistantInfo = { + ...activeAssistant, + model: { + ...activeAssistant?.model, + parameters: runtimeParams, + settings: settingParams, + id: settings.modelId ?? selectedModel?.id ?? activeAssistant.model.id, + engine: + settings.engine ?? + selectedModel?.engine ?? + activeAssistant.model.engine, + }, } - - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.saveThread(updatedThread) + setActiveAssistant(assistantInfo) + updateAssistantCallback(thread.id, assistantInfo) }, - [activeModelParams, selectedModel, setThreadModelParams] + [ + activeAssistant, + selectedModel?.parameters, + selectedModel?.settings, + selectedModel?.id, + selectedModel?.engine, + activeModelParams, + setThreadModelParams, + setActiveAssistant, + updateAssistantCallback, + ] ) const processStopWords = (params: ModelParams): ModelParams => { diff --git a/web/screens/Thread/ThreadCenterPanel/AssistantSetting/index.tsx b/web/screens/Thread/ThreadCenterPanel/AssistantSetting/index.tsx index 95c905dde3..19ec3328a5 100644 --- a/web/screens/Thread/ThreadCenterPanel/AssistantSetting/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/AssistantSetting/index.tsx @@ -8,6 +8,7 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread' import SettingComponentBuilder from '../../../../containers/ModelSetting/SettingComponent' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { activeThreadAtom, engineParamsUpdateAtom, @@ -19,13 +20,14 @@ type Props = { const AssistantSetting: React.FC = ({ componentData }) => { const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const { updateThreadMetadata } = useCreateNewThread() const { stopModel } = useActiveModel() const setEngineParamsUpdate = useSetAtom(engineParamsUpdateAtom) const onValueChanged = useCallback( (key: string, value: string | number | boolean | string[]) => { - if (!activeThread) return + if (!activeThread || !activeAssistant) return const shouldReloadModel = componentData.find((x) => x.key === key)?.requireModelReload ?? false if (shouldReloadModel) { @@ -34,40 +36,40 @@ const AssistantSetting: React.FC = ({ componentData }) => { } if ( - activeThread.assistants[0].tools && + activeAssistant?.tools && (key === 'chunk_overlap' || key === 'chunk_size') ) { if ( - activeThread.assistants[0].tools[0]?.settings?.chunk_size < - activeThread.assistants[0].tools[0]?.settings?.chunk_overlap + activeAssistant.tools[0]?.settings?.chunk_size < + activeAssistant.tools[0]?.settings?.chunk_overlap ) { - activeThread.assistants[0].tools[0].settings.chunk_overlap = - activeThread.assistants[0].tools[0].settings.chunk_size + activeAssistant.tools[0].settings.chunk_overlap = + activeAssistant.tools[0].settings.chunk_size } if ( key === 'chunk_size' && - value < activeThread.assistants[0].tools[0].settings?.chunk_overlap + value < activeAssistant.tools[0].settings?.chunk_overlap ) { - activeThread.assistants[0].tools[0].settings.chunk_overlap = value + activeAssistant.tools[0].settings.chunk_overlap = value } else if ( key === 'chunk_overlap' && - value > activeThread.assistants[0].tools[0].settings?.chunk_size + value > activeAssistant.tools[0].settings?.chunk_size ) { - activeThread.assistants[0].tools[0].settings.chunk_size = value + activeAssistant.tools[0].settings.chunk_size = value } } updateThreadMetadata({ ...activeThread, assistants: [ { - ...activeThread.assistants[0], + ...activeAssistant, tools: [ { type: 'retrieval', enabled: true, settings: { - ...(activeThread.assistants[0].tools && - activeThread.assistants[0].tools[0]?.settings), + ...(activeAssistant.tools && + activeAssistant.tools[0]?.settings), [key]: value, }, }, @@ -77,6 +79,7 @@ const AssistantSetting: React.FC = ({ componentData }) => { }) }, [ + activeAssistant, activeThread, componentData, setEngineParamsUpdate, diff --git a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx index fbca6d2904..b88b26732f 100644 --- a/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/ChatInput/index.tsx @@ -33,6 +33,7 @@ import RichTextEditor from './RichTextEditor' import { showRightPanelAtom } from '@/helpers/atoms/App.atom' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { getCurrentChatMessagesAtom } from '@/helpers/atoms/ChatMessage.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { spellCheckAtom } from '@/helpers/atoms/Setting.atom' @@ -67,6 +68,7 @@ const ChatInput = () => { const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom) const isGeneratingResponse = useAtomValue(isGeneratingResponseAtom) const threadStates = useAtomValue(threadStatesAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const { stopInference } = useActiveModel() const [activeTabThreadRightPanel, setActiveTabThreadRightPanel] = useAtom( @@ -153,9 +155,9 @@ const ChatInput = () => { onClick={(e) => { if ( fileUpload.length > 0 || - (activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled && - !activeThread?.assistants[0].model.settings?.vision_model) + (activeAssistant?.tools && + !activeAssistant?.tools[0]?.enabled && + !activeAssistant?.model.settings?.vision_model) ) { e.stopPropagation() } else { @@ -171,16 +173,15 @@ const ChatInput = () => { } disabled={ isModelSupportRagAndTools && - activeThread?.assistants[0].tools && - activeThread?.assistants[0].tools[0]?.enabled + activeAssistant?.tools && + activeAssistant?.tools[0]?.enabled } content={ <> {fileUpload.length > 0 || - (activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled && - !activeThread?.assistants[0].model.settings - ?.vision_model && ( + (activeAssistant?.tools && + !activeAssistant?.tools[0]?.enabled && + !activeAssistant?.model.settings?.vision_model && ( <> {fileUpload.length !== 0 && ( @@ -188,9 +189,8 @@ const ChatInput = () => { time. )} - {activeThread?.assistants[0].tools && - activeThread?.assistants[0].tools[0]?.enabled === - false && + {activeAssistant?.tools && + activeAssistant?.tools[0]?.enabled === false && isModelSupportRagAndTools && ( Turn on Retrieval in Tools settings to use this @@ -221,14 +221,12 @@ const ChatInput = () => {
  • { - if ( - activeThread?.assistants[0].model.settings?.vision_model - ) { + if (activeAssistant?.model.settings?.vision_model) { imageInputRef.current?.click() setShowAttacmentMenus(false) } @@ -239,9 +237,7 @@ const ChatInput = () => {
  • } content="This feature only supports multimodal models." - disabled={ - activeThread?.assistants[0].model.settings?.vision_model - } + disabled={activeAssistant?.model.settings?.vision_model} /> { } content={ - (!activeThread?.assistants[0].tools || - !activeThread?.assistants[0].tools[0]?.enabled) && ( + (!activeAssistant?.tools || + !activeAssistant?.tools[0]?.enabled) && ( Turn on Retrieval in Assistant Settings to use this feature. diff --git a/web/screens/Thread/ThreadCenterPanel/EditChatInput/index.tsx b/web/screens/Thread/ThreadCenterPanel/EditChatInput/index.tsx index ea22e3a584..5bdaf24520 100644 --- a/web/screens/Thread/ThreadCenterPanel/EditChatInput/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/EditChatInput/index.tsx @@ -80,19 +80,17 @@ const EditChatInput: React.FC = ({ message }) => { setEditMessage('') const messageIdx = messages.findIndex((msg) => msg.id === message.id) const newMessages = messages.slice(0, messageIdx) - if (activeThread) { - setMessages(activeThread.id, newMessages) - await extensionManager - .get(ExtensionTypeEnum.Conversational) - ?.writeMessages( - activeThread.id, - // Remove all of the messages below this - newMessages - ) - .then(() => { - sendChatMessage(editPrompt, newMessages) - }) - } + const toDeleteMessages = messages.slice(messageIdx) + const threadId = messages[0].thread_id + await Promise.all( + toDeleteMessages.map(async (message) => + extensionManager + .get(ExtensionTypeEnum.Conversational) + ?.deleteMessage(message.thread_id, message.id) + ) + ) + setMessages(threadId, newMessages) + sendChatMessage(editPrompt, false, newMessages) } const onKeyDown = async (e: React.KeyboardEvent) => { diff --git a/web/screens/Thread/ThreadCenterPanel/LoadModelError/index.tsx b/web/screens/Thread/ThreadCenterPanel/LoadModelError/index.tsx index d6fed48043..204ec40fb9 100644 --- a/web/screens/Thread/ThreadCenterPanel/LoadModelError/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/LoadModelError/index.tsx @@ -10,15 +10,15 @@ import { MainViewState } from '@/constants/screens' import { loadModelErrorAtom } from '@/hooks/useActiveModel' import { mainViewStateAtom } from '@/helpers/atoms/App.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom' -import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const LoadModelError = () => { const setModalTroubleShooting = useSetAtom(modalTroubleShootingAtom) const loadModelError = useAtomValue(loadModelErrorAtom) const setMainState = useSetAtom(mainViewStateAtom) const setSelectedSettingScreen = useSetAtom(selectedSettingAtom) - const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const ErrorMessage = () => { if ( @@ -33,9 +33,9 @@ const LoadModelError = () => { className="cursor-pointer font-medium text-[hsla(var(--app-link))]" onClick={() => { setMainState(MainViewState.Settings) - if (activeThread?.assistants[0]?.model.engine) { + if (activeAssistant?.model.engine) { const engine = EngineManager.instance().get( - activeThread.assistants[0].model.engine + activeAssistant.model.engine ) engine?.name && setSelectedSettingScreen(engine.name) } diff --git a/web/screens/Thread/ThreadCenterPanel/MessageToolbar/index.tsx b/web/screens/Thread/ThreadCenterPanel/MessageToolbar/index.tsx index c4a97a6b9f..4dcc98e01a 100644 --- a/web/screens/Thread/ThreadCenterPanel/MessageToolbar/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/MessageToolbar/index.tsx @@ -58,12 +58,8 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { // Should also delete error messages to clear out the error state await extensionManager .get(ExtensionTypeEnum.Conversational) - ?.writeMessages( - thread.id, - messages.filter( - (msg) => msg.id !== message.id && msg.status !== MessageStatus.Error - ) - ) + ?.deleteMessage(thread.id, message.id) + .catch(console.error) const updatedThread: Thread = { ...thread, @@ -89,10 +85,6 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { setEditMessage(message.id ?? '') } - const onRegenerateClick = async () => { - resendChatMessage(message) - } - if (message.status === MessageStatus.Pending) return null return ( @@ -122,7 +114,7 @@ const MessageToolbar = ({ message }: { message: ThreadMessage }) => { ContentType.Pdf && (
    {isUser ? props.role - : (activeThread?.assistants[0].assistant_name ?? props.role)} + : (activeAssistant?.assistant_name ?? props.role)}

    - {displayDate(props.created)} + {props.created && displayDate(props.created ?? new Date())}

    {tokenSpeed && tokenSpeed.message === props.id && diff --git a/web/screens/Thread/ThreadCenterPanel/index.tsx b/web/screens/Thread/ThreadCenterPanel/index.tsx index 01ba0aaeb5..c9a92f455a 100644 --- a/web/screens/Thread/ThreadCenterPanel/index.tsx +++ b/web/screens/Thread/ThreadCenterPanel/index.tsx @@ -27,6 +27,7 @@ import RequestDownloadModel from './RequestDownloadModel' import { showSystemMonitorPanelAtom } from '@/helpers/atoms/App.atom' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' import { @@ -55,9 +56,9 @@ const ThreadCenterPanel = () => { const setFileUpload = useSetAtom(fileUploadAtom) const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom) const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) - const acceptedFormat: Accept = activeThread?.assistants[0].model.settings - ?.vision_model + const acceptedFormat: Accept = activeAssistant?.model.settings?.vision_model ? { 'application/pdf': ['.pdf'], 'image/jpeg': ['.jpeg'], @@ -78,14 +79,13 @@ const ThreadCenterPanel = () => { if (!experimentalFeature) return if ( e.dataTransfer.items.length === 1 && - ((activeThread?.assistants[0].tools && - activeThread?.assistants[0].tools[0]?.enabled) || - activeThread?.assistants[0].model.settings?.vision_model) + ((activeAssistant?.tools && activeAssistant?.tools[0]?.enabled) || + activeAssistant?.model.settings?.vision_model) ) { setDragOver(true) } else if ( - activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled + activeAssistant?.tools && + !activeAssistant?.tools[0]?.enabled ) { setDragRejected({ code: 'retrieval-off' }) } else { @@ -100,9 +100,9 @@ const ThreadCenterPanel = () => { !files || files.length !== 1 || rejectFiles.length !== 0 || - (activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled && - !activeThread?.assistants[0].model.settings?.vision_model) + (activeAssistant?.tools && + !activeAssistant?.tools[0]?.enabled && + !activeAssistant?.model.settings?.vision_model) ) return const imageType = files[0]?.type.includes('image') @@ -110,10 +110,7 @@ const ThreadCenterPanel = () => { setDragOver(false) }, onDropRejected: (e) => { - if ( - activeThread?.assistants[0].tools && - !activeThread?.assistants[0].tools[0]?.enabled - ) { + if (activeAssistant?.tools && !activeAssistant?.tools[0]?.enabled) { setDragRejected({ code: 'retrieval-off' }) } else { setDragRejected({ code: e[0].errors[0].code }) @@ -186,8 +183,7 @@ const ThreadCenterPanel = () => {
    {isDragReject ? `Currently, we only support 1 attachment at the same time with ${ - activeThread?.assistants[0].model.settings - ?.vision_model + activeAssistant?.model.settings?.vision_model ? 'PDF, JPEG, JPG, PNG' : 'PDF' } format` @@ -195,7 +191,7 @@ const ThreadCenterPanel = () => {
    {!isDragReject && (

    - {activeThread?.assistants[0].model.settings?.vision_model + {activeAssistant?.model.settings?.vision_model ? 'PDF, JPEG, JPG, PNG' : 'PDF'}

    diff --git a/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx index ddeaedf407..21b415f49f 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx @@ -15,13 +15,15 @@ const ModalEditTitleThread = () => { const [modalActionThread, setModalActionThread] = useAtom( modalActionThreadAtom ) - const [title, setTitle] = useState(modalActionThread.thread?.title as string) + const [title, setTitle] = useState( + modalActionThread.thread?.metadata?.title as string + ) useLayoutEffect(() => { - if (modalActionThread.thread?.title) { - setTitle(modalActionThread.thread?.title) + if (modalActionThread.thread?.metadata?.title) { + setTitle(modalActionThread.thread?.metadata?.title as string) } - }, [modalActionThread.thread?.title]) + }, [modalActionThread.thread?.metadata]) const onUpdateTitle = useCallback( (e: React.MouseEvent) => { @@ -30,6 +32,10 @@ const ModalEditTitleThread = () => { updateThreadMetadata({ ...modalActionThread?.thread, title: title || 'New Thread', + metadata: { + ...modalActionThread?.thread.metadata, + title: title || 'New Thread', + }, }) }, [modalActionThread?.thread, title, updateThreadMetadata] diff --git a/web/screens/Thread/ThreadLeftPanel/index.tsx b/web/screens/Thread/ThreadLeftPanel/index.tsx index 61c6672fcf..140ae2f28d 100644 --- a/web/screens/Thread/ThreadLeftPanel/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/index.tsx @@ -20,7 +20,10 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread' import useRecommendedModel from '@/hooks/useRecommendedModel' import useSetActiveThread from '@/hooks/useSetActiveThread' -import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' +import { + activeAssistantAtom, + assistantsAtom, +} from '@/helpers/atoms/Assistant.atom' import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom' import { @@ -34,6 +37,7 @@ import { const ThreadLeftPanel = () => { const threads = useAtomValue(threadsAtom) const activeThreadId = useAtomValue(getActiveThreadIdAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const { setActiveThread } = useSetActiveThread() const assistants = useAtomValue(assistantsAtom) const threadDataReady = useAtomValue(threadDataReadyAtom) @@ -67,6 +71,7 @@ const ThreadLeftPanel = () => { useEffect(() => { if ( threadDataReady && + activeAssistant && assistants.length > 0 && threads.length === 0 && downloadedModels.length > 0 @@ -75,7 +80,13 @@ const ThreadLeftPanel = () => { (model) => model.engine === InferenceEngine.cortex_llamacpp ) const selectedModel = model[0] || recommendedModel - requestCreateNewThread(assistants[0], selectedModel) + requestCreateNewThread( + { + ...assistants[0], + ...activeAssistant, + }, + selectedModel + ) } else if (threadDataReady && !activeThreadId) { setActiveThread(threads[0]) } @@ -88,6 +99,7 @@ const ThreadLeftPanel = () => { setActiveThread, recommendedModel, downloadedModels, + activeAssistant, ]) const onContextMenu = (event: React.MouseEvent, thread: Thread) => { @@ -138,7 +150,7 @@ const ThreadLeftPanel = () => { activeThreadId && 'font-medium' )} > - {thread.title} + {thread.title ?? thread.metadata?.title}
    { const experimentalFeature = useAtomValue(experimentalFeatureEnabledAtom) const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const { updateThreadMetadata } = useCreateNewThread() const { recommendedModel, downloadedModels } = useRecommendedModel() const componentDataAssistantSetting = getConfigurationsData( - (activeThread?.assistants[0]?.tools && - activeThread?.assistants[0]?.tools[0]?.settings) ?? - {} + (activeAssistant?.tools && activeAssistant?.tools[0]?.settings) ?? {} ) useEffect(() => { if (!activeThread) return let model = downloadedModels.find( - (model) => model.id === activeThread.assistants[0].model.id + (model) => model.id === activeAssistant?.model.id ) if (!model) { model = recommendedModel } setSelectedModel(model) - }, [recommendedModel, activeThread, downloadedModels, setSelectedModel]) + }, [ + recommendedModel, + activeThread, + downloadedModels, + setSelectedModel, + activeAssistant?.model.id, + ]) const onRetrievalSwitchUpdate = useCallback( (enabled: boolean) => { - if (!activeThread) return + if (!activeThread || !activeAssistant) return updateThreadMetadata({ ...activeThread, assistants: [ { - ...activeThread.assistants[0], + ...activeAssistant, tools: [ { type: 'retrieval', enabled: enabled, settings: - (activeThread.assistants[0].tools && - activeThread.assistants[0].tools[0]?.settings) ?? + (activeAssistant.tools && + activeAssistant.tools[0]?.settings) ?? {}, }, ], @@ -63,25 +69,25 @@ const Tools = () => { ], }) }, - [activeThread, updateThreadMetadata] + [activeAssistant, activeThread, updateThreadMetadata] ) const onTimeWeightedRetrieverSwitchUpdate = useCallback( (enabled: boolean) => { - if (!activeThread) return + if (!activeThread || !activeAssistant) return updateThreadMetadata({ ...activeThread, assistants: [ { - ...activeThread.assistants[0], + ...activeAssistant, tools: [ { type: 'retrieval', enabled: true, useTimeWeightedRetriever: enabled, settings: - (activeThread.assistants[0].tools && - activeThread.assistants[0].tools[0]?.settings) ?? + (activeAssistant.tools && + activeAssistant.tools[0]?.settings) ?? {}, }, ], @@ -89,23 +95,54 @@ const Tools = () => { ], }) }, - [activeThread, updateThreadMetadata] + [activeAssistant, activeThread, updateThreadMetadata] ) if (!experimentalFeature) return null return ( - {activeThread?.assistants[0]?.tools && - componentDataAssistantSetting.length > 0 && ( -
    -
    + {activeAssistant?.tools && componentDataAssistantSetting.length > 0 && ( +
    +
    +
    +
    -
    +
    +
    + {activeAssistant?.tools[0].enabled && ( +
    +
    +
    + { className="ml-2 flex-shrink-0 text-[hsl(var(--text-secondary))]" /> } - content="Retrieval helps the assistant use information from - files you send to it. Once you share a file, the - assistant automatically fetches the relevant content - based on your request." - /> - -
    - onRetrievalSwitchUpdate(e.target.checked)} + content="Embedding model is crucial for understanding and + processing the input text effectively by + converting text to numerical representations. + Align the model choice with your task, evaluate + its performance, and consider factors like + resource availability. Experiment to find the best + fit for your specific use case." />
    +
    + +
    -
    - {activeThread?.assistants[0]?.tools[0].enabled && ( -
    -
    -
    - +
    +
    +
    -
    - -
    -
    -
    -
    - -
    + /> + +
    -
    - -
    +
    +
    -
    -
    - - - } - content="Time-Weighted Retriever looks at how similar +
    +
    +
    + + + } + content="Time-Weighted Retriever looks at how similar they are and how new they are. It compares documents based on their meaning like usual, but also considers when they were added to give newer ones more importance." + /> +
    + + onTimeWeightedRetrieverSwitchUpdate(e.target.checked) + } /> -
    - - onTimeWeightedRetrieverSwitchUpdate(e.target.checked) - } - /> -
    -
    - )} -
    - )} + +
    + )} +
    + )} ) } diff --git a/web/screens/Thread/ThreadRightPanel/index.tsx b/web/screens/Thread/ThreadRightPanel/index.tsx index 952ba8eb32..04205329f6 100644 --- a/web/screens/Thread/ThreadRightPanel/index.tsx +++ b/web/screens/Thread/ThreadRightPanel/index.tsx @@ -38,6 +38,7 @@ import PromptTemplateSetting from './PromptTemplateSetting' import Tools from './Tools' import { experimentalFeatureEnabledAtom } from '@/helpers/atoms/AppConfig.atom' +import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { activeThreadAtom, @@ -53,6 +54,7 @@ const ENGINE_SETTINGS = 'Engine Settings' const ThreadRightPanel = () => { const activeThread = useAtomValue(activeThreadAtom) + const activeAssistant = useAtomValue(activeAssistantAtom) const activeModelParams = useAtomValue(getActiveThreadModelParamsAtom) const selectedModel = useAtomValue(selectedModelAtom) const [activeTabThreadRightPanel, setActiveTabThreadRightPanel] = useAtom( @@ -154,18 +156,18 @@ const ThreadRightPanel = () => { const onAssistantInstructionChanged = useCallback( (e: React.ChangeEvent) => { - if (activeThread) + if (activeThread && activeAssistant) updateThreadMetadata({ ...activeThread, assistants: [ { - ...activeThread.assistants[0], + ...activeAssistant, instructions: e.target.value || '', }, ], }) }, - [activeThread, updateThreadMetadata] + [activeAssistant, activeThread, updateThreadMetadata] ) const resetModel = useDebouncedCallback(() => { @@ -174,7 +176,7 @@ const ThreadRightPanel = () => { const onValueChanged = useCallback( (key: string, value: string | number | boolean | string[]) => { - if (!activeThread) { + if (!activeThread || !activeAssistant) { return } @@ -186,32 +188,38 @@ const ThreadRightPanel = () => { }) if ( - activeThread.assistants[0].model.parameters?.max_tokens && - activeThread.assistants[0].model.settings?.ctx_len + activeAssistant.model.parameters?.max_tokens && + activeAssistant.model.settings?.ctx_len ) { if ( key === 'max_tokens' && - Number(value) > activeThread.assistants[0].model.settings.ctx_len + Number(value) > activeAssistant.model.settings.ctx_len ) { updateModelParameter(activeThread, { params: { - max_tokens: activeThread.assistants[0].model.settings.ctx_len, + max_tokens: activeAssistant.model.settings.ctx_len, }, }) } if ( key === 'ctx_len' && - Number(value) < activeThread.assistants[0].model.parameters.max_tokens + Number(value) < activeAssistant.model.parameters.max_tokens ) { updateModelParameter(activeThread, { params: { - max_tokens: activeThread.assistants[0].model.settings.ctx_len, + max_tokens: activeAssistant.model.settings.ctx_len, }, }) } } }, - [activeThread, resetModel, setEngineParamsUpdate, updateModelParameter] + [ + activeAssistant, + activeThread, + resetModel, + setEngineParamsUpdate, + updateModelParameter, + ] ) if (!activeThread) { @@ -250,7 +258,7 @@ const ThreadRightPanel = () => {