From 53707a5083ef89a02fa57e34a4f4d355f31c2689 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Mon, 22 Sep 2025 15:50:22 +0700 Subject: [PATCH 01/56] chore: use default nsis template --- .../template-tauri-build-windows-x64.yml | 4 +-- src-tauri/tauri.windows.conf.json | 4 ++- src-tauri/windows/hooks.nsh | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src-tauri/windows/hooks.nsh diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 958b7c9f7..a074d2e56 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -95,8 +95,8 @@ jobs: # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json - mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json + # jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json + # mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 023c2a567..0af599f9a 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -4,10 +4,12 @@ "resources": ["resources/pre-install/**/*"], "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { - "signCommand": "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1", "webviewInstallMode": { "silent": true, "type": "downloadBootstrapper" + }, + "nsis": { + "installerHooks": "./windows/hooks.nsh" } } } diff --git a/src-tauri/windows/hooks.nsh b/src-tauri/windows/hooks.nsh new file mode 100644 index 000000000..a4801f040 --- /dev/null +++ b/src-tauri/windows/hooks.nsh @@ -0,0 +1,30 @@ +!macro NSIS_HOOK_POSTINSTALL + ; Check if Visual C++ 2019 Redistributable is installed (via Windows Registry) + ReadRegDWord $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed" + + ${If} $0 == 1 + DetailPrint "Visual C++ Redistributable already installed" + Goto vcredist_done + ${EndIf} + + ; Install from bundled MSI if not installed + ${If} ${FileExists} "$INSTDIR\resources\vc_redist.x64.msi" + DetailPrint "Installing Visual C++ Redistributable..." + ; Copy to TEMP folder and then execute installer + CopyFiles "$INSTDIR\resources\vc_redist.x64.msi" "$TEMP\vc_redist.x64.msi" + ExecWait 'msiexec /i "$TEMP\vc_redist.x64.msi" /passive /norestart' $0 + + ; Check wether installation process exited successfully (code 0) or not + ${If} $0 == 0 + DetailPrint "Visual C++ Redistributable installed successfully" + ${Else} + MessageBox MB_ICONEXCLAMATION "Visual C++ installation failed. Some features may not work." + ${EndIf} + + ; Clean up setup files from TEMP and your installed app + Delete "$TEMP\vc_redist.x64.msi" + Delete "$INSTDIR\resources\vc_redist.x64.msi" + ${EndIf} + + vcredist_done: +!macroend \ No newline at end of file From 0151274659019619e8d9545e8b66dead3e49e0d8 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Mon, 22 Sep 2025 20:33:13 +0700 Subject: [PATCH 02/56] chore: update installerIcon on nsis config --- src-tauri/tauri.windows.conf.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 0af599f9a..15a3275a9 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -4,12 +4,13 @@ "resources": ["resources/pre-install/**/*"], "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { + "nsis": { + "installerHooks": "./windows/hooks.nsh", + "installerIcon": "icons/icon.ico" + }, "webviewInstallMode": { "silent": true, "type": "downloadBootstrapper" - }, - "nsis": { - "installerHooks": "./windows/hooks.nsh" } } } From 89511235576bd273529267f13a635ca2363815b4 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Mon, 22 Sep 2025 23:51:48 +0700 Subject: [PATCH 03/56] chore: add libvulkan for windows --- src-tauri/tauri.windows.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 15a3275a9..fb29cf1f7 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,7 +1,7 @@ { "bundle": { "targets": ["nsis"], - "resources": ["resources/pre-install/**/*"], + "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll"], "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { "nsis": { From 7257eb4ae66dad0744036008f5179291cfd52931 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 00:15:25 +0700 Subject: [PATCH 04/56] chore: add LICENSE and vulkan-1.dll at instdir --- src-tauri/tauri.windows.conf.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index fb29cf1f7..12945ffed 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,8 +1,8 @@ { "bundle": { "targets": ["nsis"], - "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll"], - "externalBin": ["resources/bin/bun", "resources/bin/uv"], + "resources": ["resources/pre-install/**/*"], + "externalBin": ["resources/bin/bun", "resources/bin/uv", "resources/lib/vulkan-1.dll", "resources/LICENSE"], "windows": { "nsis": { "installerHooks": "./windows/hooks.nsh", From 50b66eff74f15c8f6a978688cf78039a43e677d8 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 13:01:09 +0700 Subject: [PATCH 05/56] chore: update hooks to install vcredist.exe and update path for dll and license file --- src-tauri/tauri.windows.conf.json | 4 +- src-tauri/windows/hooks.nsh | 79 ++++++++++++++++++++++--------- 2 files changed, 59 insertions(+), 24 deletions(-) diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 12945ffed..664053705 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,8 +1,8 @@ { "bundle": { "targets": ["nsis"], - "resources": ["resources/pre-install/**/*"], - "externalBin": ["resources/bin/bun", "resources/bin/uv", "resources/lib/vulkan-1.dll", "resources/LICENSE"], + "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/LICENSE"], + "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { "nsis": { "installerHooks": "./windows/hooks.nsh", diff --git a/src-tauri/windows/hooks.nsh b/src-tauri/windows/hooks.nsh index a4801f040..60aec1c80 100644 --- a/src-tauri/windows/hooks.nsh +++ b/src-tauri/windows/hooks.nsh @@ -1,30 +1,65 @@ !macro NSIS_HOOK_POSTINSTALL - ; Check if Visual C++ 2019 Redistributable is installed (via Windows Registry) - ReadRegDWord $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Installed" - - ${If} $0 == 1 - DetailPrint "Visual C++ Redistributable already installed" - Goto vcredist_done + ; Check if Visual C++ Redistributable is already installed + ReadRegStr $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version" + ${If} $0 == "" + ; Try alternative registry location + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version" ${EndIf} - ; Install from bundled MSI if not installed - ${If} ${FileExists} "$INSTDIR\resources\vc_redist.x64.msi" - DetailPrint "Installing Visual C++ Redistributable..." - ; Copy to TEMP folder and then execute installer - CopyFiles "$INSTDIR\resources\vc_redist.x64.msi" "$TEMP\vc_redist.x64.msi" - ExecWait 'msiexec /i "$TEMP\vc_redist.x64.msi" /passive /norestart' $0 - - ; Check wether installation process exited successfully (code 0) or not - ${If} $0 == 0 - DetailPrint "Visual C++ Redistributable installed successfully" + ${If} $0 == "" + ; VC++ Redistributable not found, need to install + DetailPrint "Visual C++ Redistributable not found, downloading and installing..." + + ; Download VC++ Redistributable + Delete "$TEMP\vc_redist.x64.exe" + DetailPrint "Downloading Visual C++ Redistributable..." + NSISdl::download "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe" + Pop $1 + + ${If} $1 == "success" + DetailPrint "Visual C++ Redistributable download successful" + + ; Install VC++ Redistributable silently + DetailPrint "Installing Visual C++ Redistributable..." + ExecWait '"$TEMP\vc_redist.x64.exe" /quiet /norestart' $2 + + ${If} $2 == 0 + DetailPrint "Visual C++ Redistributable installed successfully" + ${ElseIf} $2 == 1638 + DetailPrint "Visual C++ Redistributable already installed (newer version)" + ${ElseIf} $2 == 3010 + DetailPrint "Visual C++ Redistributable installed successfully (restart required)" + ${Else} + DetailPrint "Visual C++ installation failed with exit code: $2" + MessageBox MB_ICONEXCLAMATION "Visual C++ installation failed. Some features may not work." + ${EndIf} + + ; Clean up downloaded file + Delete "$TEMP\vc_redist.x64.exe" ${Else} - MessageBox MB_ICONEXCLAMATION "Visual C++ installation failed. Some features may not work." + DetailPrint "Failed to download Visual C++ Redistributable: $1" + MessageBox MB_ICONEXCLAMATION "Failed to download Visual C++ Redistributable. Some features may not work." ${EndIf} - - ; Clean up setup files from TEMP and your installed app - Delete "$TEMP\vc_redist.x64.msi" - Delete "$INSTDIR\resources\vc_redist.x64.msi" + ${Else} + DetailPrint "Visual C++ Redistributable already installed (version: $0)" ${EndIf} - vcredist_done: + ; ---- Copy LICENSE to install root ---- + ${If} ${FileExists} "$INSTDIR\resources\LICENSE" + CopyFiles /SILENT "$INSTDIR\resources\LICENSE" "$INSTDIR\LICENSE" + DetailPrint "Copied LICENSE to install root" + ${EndIf} + + ; ---- Copy vulkan-1.dll to install root ---- + ${If} ${FileExists} "$INSTDIR\resources\lib\vulkan-1.dll" + CopyFiles /SILENT "$INSTDIR\resources\lib\vulkan-1.dll" "$INSTDIR\vulkan-1.dll" + DetailPrint "Copied vulkan-1.dll to install root" + + ; Optional cleanup - remove from resources folder + Delete "$INSTDIR\resources\lib\vulkan-1.dll" + ; Only remove the lib directory if it's empty + RMDir "$INSTDIR\resources\lib" + ${Else} + DetailPrint "vulkan-1.dll not found at expected location: $INSTDIR\resources\lib\vulkan-1.dll" + ${EndIf} !macroend \ No newline at end of file From 6dc38f18cfbce5c45ddc698625d7d7e59a34adf5 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 15:42:07 +0700 Subject: [PATCH 06/56] chore: standardize build process on windows --- .../template-tauri-build-windows-x64.yml | 47 +- src-tauri/tauri.bundle.windows.nsis.template | 1007 ----------------- 2 files changed, 5 insertions(+), 1049 deletions(-) delete mode 100644 src-tauri/tauri.bundle.windows.nsis.template diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index a074d2e56..4d5c2befb 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -95,8 +95,6 @@ jobs: # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = true' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - # jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json - # mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json @@ -104,47 +102,19 @@ jobs: echo "---------Cargo.toml---------" cat ./src-tauri/Cargo.toml - generate_build_version() { - ### Examble - ### input 0.5.6 output will be 0.5.6 and 0.5.6.0 - ### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2 - ### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213 - local new_version="$1" - local base_version - local t_value - - # Check if it has a "-" - if [[ "$new_version" == *-* ]]; then - base_version="${new_version%%-*}" # part before - - suffix="${new_version#*-}" # part after - - - # Check if it is rcX-beta - if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then - t_value="${BASH_REMATCH[1]}" - else - t_value="$suffix" - fi - else - base_version="$new_version" - t_value="0" - fi - - # Export two values - new_base_version="$base_version" - new_build_version="${base_version}.${t_value}" - } - generate_build_version ${{ inputs.new_version }} - sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template - # Temporarily enable devtool on prod build ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" cat ./src-tauri/Cargo.toml # Change app name for beta and nightly builds if [ "${{ inputs.channel }}" != "stable" ]; then + # Update updater endpoint jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + + # Update product name + jq --arg name "Jan-${{ inputs.channel }}" '.productName = $name' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json + mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json chmod +x .github/scripts/rename-tauri-app.sh .github/scripts/rename-tauri-app.sh ./src-tauri/tauri.conf.json ${{ inputs.channel }} @@ -161,15 +131,8 @@ jobs: chmod +x .github/scripts/rename-workspace.sh .github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }} cat ./package.json - - sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template else - sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template fi - echo "---------nsis.template---------" - cat ./src-tauri/tauri.bundle.windows.nsis.template - name: Install AzureSignTool run: | diff --git a/src-tauri/tauri.bundle.windows.nsis.template b/src-tauri/tauri.bundle.windows.nsis.template deleted file mode 100644 index 2cd878dc2..000000000 --- a/src-tauri/tauri.bundle.windows.nsis.template +++ /dev/null @@ -1,1007 +0,0 @@ -Unicode true -ManifestDPIAware true -; Add in `dpiAwareness` `PerMonitorV2` to manifest for Windows 10 1607+ (note this should not affect lower versions since they should be able to ignore this and pick up `dpiAware` `true` set by `ManifestDPIAware true`) -; Currently undocumented on NSIS's website but is in the Docs folder of source tree, see -; https://github.com/kichik/nsis/blob/5fc0b87b819a9eec006df4967d08e522ddd651c9/Docs/src/attributes.but#L286-L300 -; https://github.com/tauri-apps/tauri/pull/10106 -ManifestDPIAwareness PerMonitorV2 - -!if "lzma" == "none" - SetCompress off -!else - ; Set the compression algorithm. We default to LZMA. - SetCompressor /SOLID "lzma" -!endif - -!include MUI2.nsh -!include FileFunc.nsh -!include x64.nsh -!include WordFunc.nsh -!include "utils.nsh" -!include "FileAssociation.nsh" -!include "Win\COM.nsh" -!include "Win\Propkey.nsh" -!include "StrFunc.nsh" -${StrCase} -${StrLoc} - - -!define WEBVIEW2APPGUID "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" - -!define MANUFACTURER "ai" -!define PRODUCTNAME "jan_productname" -!define VERSION "jan_version" -!define VERSIONWITHBUILD "jan_build" -!define HOMEPAGE "" -!define INSTALLMODE "currentUser" -!define LICENSE "" -!define INSTALLERICON "D:\a\jan\jan\src-tauri\icons\icon.ico" -!define SIDEBARIMAGE "" -!define HEADERIMAGE "" -!define MAINBINARYNAME "jan_mainbinaryname" -!define MAINBINARYSRCPATH "D:\a\jan\jan\src-tauri\target\release\jan_mainbinaryname.exe" -!define BUNDLEID "jan_mainbinaryname.ai.app" -!define COPYRIGHT "" -!define OUTFILE "nsis-output.exe" -!define ARCH "x64" -!define ADDITIONALPLUGINSPATH "D:\a\jan\jan\src-tauri\target\release\nsis\x64\Plugins\x86-unicode\additional" -!define ALLOWDOWNGRADES "true" -!define DISPLAYLANGUAGESELECTOR "false" -!define INSTALLWEBVIEW2MODE "downloadBootstrapper" -!define WEBVIEW2INSTALLERARGS "/silent" -!define WEBVIEW2BOOTSTRAPPERPATH "" -!define WEBVIEW2INSTALLERPATH "" -!define MINIMUMWEBVIEW2VERSION "" -!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}" -!define MANUKEY "Software\${MANUFACTURER}" -!define MANUPRODUCTKEY "${MANUKEY}\${PRODUCTNAME}" -!define UNINSTALLERSIGNCOMMAND "$\"powershell$\" $\"-ExecutionPolicy$\" $\"Bypass$\" $\"-File$\" $\"./sign.ps1$\" $\"%1$\"" -!define ESTIMATEDSIZE "793795" -!define STARTMENUFOLDER "" -!define VCREDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe" -!define VCREDIST_FILENAME "vc_redist.x64.exe" - -Var PassiveMode -Var UpdateMode -Var NoShortcutMode -Var WixMode -Var OldMainBinaryName - -Name "${PRODUCTNAME}" -BrandingText "${COPYRIGHT}" -OutFile "${OUTFILE}" - -ShowInstDetails nevershow -ShowUninstDetails nevershow - -; We don't actually use this value as default install path, -; it's just for nsis to append the product name folder in the directory selector -; https://nsis.sourceforge.io/Reference/InstallDir -!define PLACEHOLDER_INSTALL_DIR "placeholder\${PRODUCTNAME}" -InstallDir "${PLACEHOLDER_INSTALL_DIR}" - -VIProductVersion "${VERSIONWITHBUILD}" -VIAddVersionKey "ProductName" "${PRODUCTNAME}" -VIAddVersionKey "FileDescription" "${PRODUCTNAME}" -VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" -VIAddVersionKey "FileVersion" "${VERSION}" -VIAddVersionKey "ProductVersion" "${VERSION}" - -# additional plugins -!addplugindir "${ADDITIONALPLUGINSPATH}" - -; Uninstaller signing command -!if "${UNINSTALLERSIGNCOMMAND}" != "" - !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' -!endif - -; Handle install mode, `perUser`, `perMachine` or `both` -!if "${INSTALLMODE}" == "perMachine" - RequestExecutionLevel highest -!endif - -!if "${INSTALLMODE}" == "currentUser" - RequestExecutionLevel user -!endif - -!if "${INSTALLMODE}" == "both" - !define MULTIUSER_MUI - !define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}" - !define MULTIUSER_INSTALLMODE_COMMANDLINE - !if "${ARCH}" == "x64" - !define MULTIUSER_USE_PROGRAMFILES64 - !else if "${ARCH}" == "arm64" - !define MULTIUSER_USE_PROGRAMFILES64 - !endif - !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}" - !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser" - !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME - !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation - !define MULTIUSER_EXECUTIONLEVEL Highest - !include MultiUser.nsh -!endif - -; Installer icon -!if "${INSTALLERICON}" != "" - !define MUI_ICON "${INSTALLERICON}" -!endif - -; Installer sidebar image -!if "${SIDEBARIMAGE}" != "" - !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" -!endif - -; Installer header image -!if "${HEADERIMAGE}" != "" - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" -!endif - -; Define registry key to store installer language -!define MUI_LANGDLL_REGISTRY_ROOT "HKCU" -!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}" -!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" - -; Installer pages, must be ordered as they appear -; 1. Welcome Page -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!insertmacro MUI_PAGE_WELCOME - -; 2. License Page (if defined) -!if "${LICENSE}" != "" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !insertmacro MUI_PAGE_LICENSE "${LICENSE}" -!endif - -; 3. Install mode (if it is set to `both`) -!if "${INSTALLMODE}" == "both" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !insertmacro MULTIUSER_PAGE_INSTALLMODE -!endif - -; 4. Custom page to ask user if he wants to reinstall/uninstall -; only if a previous installation was detected -Var ReinstallPageCheck -Page custom PageReinstall PageLeaveReinstall -Function PageReinstall - ; Uninstall previous WiX installation if exists. - ; - ; A WiX installer stores the installation info in registry - ; using a UUID and so we have to loop through all keys under - ; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` - ; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER} - ; - ; This has a potential issue that there maybe another installation that matches - ; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer, - ; however, this should be fine since the user will have to confirm the uninstallation - ; and they can chose to abort it if doesn't make sense. - StrCpy $0 0 - wix_loop: - EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0 - StrCmp $1 "" wix_loop_done ; Exit loop if there is no more keys to loop on - IntOp $0 $0 + 1 - ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName" - ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher" - StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop - ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString" - ${StrCase} $R1 $R0 "L" - ${StrLoc} $R0 $R1 "msiexec" ">" - StrCmp $R0 0 0 wix_loop_done - StrCpy $WixMode 1 - StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" - Goto compare_version - wix_loop_done: - - ; Check if there is an existing installation, if not, abort the reinstall page - ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" - ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" - ${IfThen} "$R0$R1" == "" ${|} Abort ${|} - - ; Compare this installar version with the existing installation - ; and modify the messages presented to the user accordingly - compare_version: - StrCpy $R4 "$(older)" - ${If} $WixMode = 1 - ReadRegStr $R0 HKLM "$R6" "DisplayVersion" - ${Else} - ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" - ${EndIf} - ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} - - nsis_tauri_utils::SemverCompare "${VERSION}" $R0 - Pop $R0 - ; Reinstalling the same version - ${If} $R0 = 0 - StrCpy $R1 "$(alreadyInstalledLong)" - StrCpy $R2 "$(addOrReinstall)" - StrCpy $R3 "$(uninstallApp)" - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" - ; Upgrading - ${ElseIf} $R0 = 1 - StrCpy $R1 "$(olderOrUnknownVersionInstalled)" - StrCpy $R2 "$(uninstallBeforeInstalling)" - StrCpy $R3 "$(dontUninstall)" - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" - ; Downgrading - ${ElseIf} $R0 = -1 - StrCpy $R1 "$(newerVersionInstalled)" - StrCpy $R2 "$(uninstallBeforeInstalling)" - !if "${ALLOWDOWNGRADES}" == "true" - StrCpy $R3 "$(dontUninstall)" - !else - StrCpy $R3 "$(dontUninstallDowngrade)" - !endif - !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" - ${Else} - Abort - ${EndIf} - - ; Skip showing the page if passive - ; - ; Note that we don't call this earlier at the begining - ; of this function because we need to populate some variables - ; related to current installed version if detected and whether - ; we are downgrading or not. - ${If} $PassiveMode = 1 - Call PageLeaveReinstall - ${Else} - nsDialogs::Create 1018 - Pop $R4 - ${IfThen} $(^RTL) = 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} - - ${NSD_CreateLabel} 0 0 100% 24u $R1 - Pop $R1 - - ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 - Pop $R2 - ${NSD_OnClick} $R2 PageReinstallUpdateSelection - - ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 - Pop $R3 - ; Disable this radio button if downgrading and downgrades are disabled - !if "${ALLOWDOWNGRADES}" == "false" - ${IfThen} $R0 = -1 ${|} EnableWindow $R3 0 ${|} - !endif - ${NSD_OnClick} $R3 PageReinstallUpdateSelection - - ; Check the first radio button if this the first time - ; we enter this page or if the second button wasn't - ; selected the last time we were on this page - ${If} $ReinstallPageCheck <> 2 - SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 - ${Else} - SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 - ${EndIf} - - ${NSD_SetFocus} $R2 - nsDialogs::Show - ${EndIf} -FunctionEnd -Function PageReinstallUpdateSelection - ${NSD_GetState} $R2 $R1 - ${If} $R1 == ${BST_CHECKED} - StrCpy $ReinstallPageCheck 1 - ${Else} - StrCpy $ReinstallPageCheck 2 - ${EndIf} -FunctionEnd -Function PageLeaveReinstall - ; In passive mode, always uninstall when upgrading - ${If} $PassiveMode = 1 - ${AndIf} $R0 = 1 ; Upgrading - Goto reinst_uninstall - ${EndIf} - - ${NSD_GetState} $R2 $R1 - - ; If migrating from Wix, always uninstall - ${If} $WixMode = 1 - Goto reinst_uninstall - ${EndIf} - - ; In update mode, always proceeds without uninstalling - ${If} $UpdateMode = 1 - Goto reinst_done - ${EndIf} - - ; $R0 holds whether same(0)/upgrading(1)/downgrading(-1) version - ; $R1 holds the radio buttons state: - ; 1 => first choice was selected - ; 0 => second choice was selected - ${If} $R0 = 0 ; Same version, proceed - ${If} $R1 = 1 ; User chose to add/reinstall - Goto reinst_done - ${Else} ; User chose to uninstall - Goto reinst_uninstall - ${EndIf} - ${ElseIf} $R0 = 1 ; Upgrading - ${If} $R1 = 1 ; User chose to uninstall - Goto reinst_uninstall - ${Else} - Goto reinst_done ; User chose NOT to uninstall - ${EndIf} - ${ElseIf} $R0 = -1 ; Downgrading - ${If} $R1 = 1 ; User chose to uninstall - Goto reinst_uninstall - ${Else} - Goto reinst_done ; User chose NOT to uninstall - ${EndIf} - ${EndIf} - - reinst_uninstall: - HideWindow - ClearErrors - - ${If} $WixMode = 1 - ReadRegStr $R1 HKLM "$R6" "UninstallString" - ExecWait '$R1' $0 - ${Else} - ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" - ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" - ${IfThen} $UpdateMode = 1 ${|} StrCpy $R1 "$R1 /UPDATE" ${|} ; append /UPDATE - ${IfThen} $PassiveMode = 1 ${|} StrCpy $R1 "$R1 /P" ${|} ; append /P - StrCpy $R1 "$R1 _?=$4" ; append uninstall directory - ExecWait '$R1' $0 - ${EndIf} - - BringToFront - - ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code - - ${If} $0 <> 0 - ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" - ; User cancelled wix uninstaller? return to select un/reinstall page - ${If} $WixMode = 1 - ${AndIf} $0 = 1602 - Abort - ${EndIf} - - ; User cancelled NSIS uninstaller? return to select un/reinstall page - ${If} $0 = 1 - Abort - ${EndIf} - - ; Other erros? show generic error message and return to select un/reinstall page - MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" - Abort - ${EndIf} - reinst_done: -FunctionEnd - -; 5. Choose install directory page -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!insertmacro MUI_PAGE_DIRECTORY - -; 6. Start menu shortcut page -Var AppStartMenuFolder -!if "${STARTMENUFOLDER}" != "" - !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive - !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${STARTMENUFOLDER}" -!else - !define MUI_PAGE_CUSTOMFUNCTION_PRE Skip -!endif -!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder - -; 7. Installation page -!insertmacro MUI_PAGE_INSTFILES - -; 8. Finish page -; -; Don't auto jump to finish page after installation page, -; because the installation page has useful info that can be used debug any issues with the installer. -!define MUI_FINISHPAGE_NOAUTOCLOSE -; Use show readme button in the finish page as a button create a desktop shortcut -!define MUI_FINISHPAGE_SHOWREADME -!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)" -!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateOrUpdateDesktopShortcut -; Show run app after installation. -!define MUI_FINISHPAGE_RUN -!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary -!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive -!insertmacro MUI_PAGE_FINISH - -Function RunMainBinary - nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "" -FunctionEnd - -; Uninstaller Pages -; 1. Confirm uninstall page -Var DeleteAppDataCheckbox -Var DeleteAppDataCheckboxState -!define /ifndef WS_EX_LAYOUTRTL 0x00400000 -!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow -Function un.ConfirmShow ; Add add a `Delete app data` check box - ; $1 inner dialog HWND - ; $2 window DPI - ; $3 style - ; $4 x - ; $5 y - ; $6 width - ; $7 height - FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog - System::Call "user32::GetDpiForWindow(p r1) i .r2" - ${If} $(^RTL) = 1 - StrCpy $3 "${__NSD_CheckBox_EXSTYLE} | ${WS_EX_LAYOUTRTL}" - IntOp $4 50 * $2 - ${Else} - StrCpy $3 "${__NSD_CheckBox_EXSTYLE}" - IntOp $4 0 * $2 - ${EndIf} - IntOp $5 100 * $2 - IntOp $6 400 * $2 - IntOp $7 25 * $2 - IntOp $4 $4 / 96 - IntOp $5 $5 / 96 - IntOp $6 $6 / 96 - IntOp $7 $7 / 96 - System::Call 'user32::CreateWindowEx(i r3, w "${__NSD_CheckBox_CLASS}", w "$(deleteAppData)", i ${__NSD_CheckBox_STYLE}, i r4, i r5, i r6, i r7, p r1, i0, i0, i0) i .s' - Pop $DeleteAppDataCheckbox - SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 - SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 -FunctionEnd -!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave -Function un.ConfirmLeave - SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState -FunctionEnd -!define MUI_PAGE_CUSTOMFUNCTION_PRE un.SkipIfPassive -!insertmacro MUI_UNPAGE_CONFIRM - -; 2. Uninstalling Page -!insertmacro MUI_UNPAGE_INSTFILES - -;Languages -!insertmacro MUI_LANGUAGE "English" -!insertmacro MUI_RESERVEFILE_LANGDLL - !include "D:\a\jan\jan\src-tauri\target\release\nsis\x64\English.nsh" - -Function .onInit - ${GetOptions} $CMDLINE "/P" $PassiveMode - ${IfNot} ${Errors} - StrCpy $PassiveMode 1 - ${EndIf} - ; always run in passive mode - StrCpy $PassiveMode 1 - - ${GetOptions} $CMDLINE "/NS" $NoShortcutMode - ${IfNot} ${Errors} - StrCpy $NoShortcutMode 1 - ${EndIf} - - ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode - ${IfNot} ${Errors} - StrCpy $UpdateMode 1 - ${EndIf} - - !if "${DISPLAYLANGUAGESELECTOR}" == "true" - !insertmacro MUI_LANGDLL_DISPLAY - !endif - - !insertmacro SetContext - - ${If} $INSTDIR == "${PLACEHOLDER_INSTALL_DIR}" - ; Set default install location - !if "${INSTALLMODE}" == "perMachine" - ${If} ${RunningX64} - !if "${ARCH}" == "x64" - StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" - !else if "${ARCH}" == "arm64" - StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" - !else - StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" - !endif - ${Else} - StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" - ${EndIf} - !else if "${INSTALLMODE}" == "currentUser" - StrCpy $INSTDIR "$LOCALAPPDATA\Programs\${PRODUCTNAME}" - !endif - - Call RestorePreviousInstallLocation - ${EndIf} - - ; Remove old Jan if it exists - ${If} ${FileExists} "$INSTDIR\LICENSE.electron.txt" - DeleteRegKey HKLM "Software\${PRODUCTNAME}" - RMDir /r "$INSTDIR" - Delete "$INSTDIR\*.*" - ${EndIf} - - !if "${INSTALLMODE}" == "both" - !insertmacro MULTIUSER_INIT - !endif -FunctionEnd - - -Section EarlyChecks - ; Abort silent installer if downgrades is disabled - !if "${ALLOWDOWNGRADES}" == "false" - ${If} ${Silent} - ; If downgrading - ${If} $R0 = -1 - System::Call 'kernel32::AttachConsole(i -1)i.r0' - ${If} $0 <> 0 - System::Call 'kernel32::GetStdHandle(i -11)i.r0' - System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color - FileWrite $0 "$(silentDowngrades)" - ${EndIf} - Abort - ${EndIf} - ${EndIf} - !endif - -SectionEnd - -Section WebView2 - ; Check if Webview2 is already installed and skip this section - ${If} ${RunningX64} - ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" - ${Else} - ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" - ${EndIf} - ${If} $4 == "" - ReadRegStr $4 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv" - ${EndIf} - - ${If} $4 == "" - ; Webview2 installation - ; - ; Skip if updating - ${If} $UpdateMode <> 1 - !if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - DetailPrint "$(webview2Downloading)" - NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Pop $0 - ${If} $0 == "success" - DetailPrint "$(webview2DownloadSuccess)" - ${Else} - DetailPrint "$(webview2DownloadError)" - Abort "$(webview2AbortError)" - ${EndIf} - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif - - !if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper" - Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe" - File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe" - Goto install_webview2 - !endif - - !if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller" - Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}" - DetailPrint "$(installingWebview2)" - StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" - Goto install_webview2 - !endif - - Goto webview2_done - - install_webview2: - DetailPrint "$(installingWebview2)" - ; $6 holds the path to the webview2 installer - ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1 - ${If} $1 = 0 - DetailPrint "$(webview2InstallSuccess)" - ${Else} - DetailPrint "$(webview2InstallError)" - Abort "$(webview2AbortError)" - ${EndIf} - webview2_done: - ${EndIf} - ${Else} - !if "${MINIMUMWEBVIEW2VERSION}" != "" - ${VersionCompare} "${MINIMUMWEBVIEW2VERSION}" "$4" $R0 - ${If} $R0 = 1 - update_webview: - DetailPrint "$(installingWebview2)" - ${If} ${RunningX64} - ReadRegStr $R1 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" "path" - ${Else} - ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\EdgeUpdate" "path" - ${EndIf} - ${If} $R1 == "" - ReadRegStr $R1 HKCU "SOFTWARE\Microsoft\EdgeUpdate" "path" - ${EndIf} - ${If} $R1 != "" - ; Chromium updater docs: https://source.chromium.org/chromium/chromium/src/+/main:docs/updater/user_manual.md - ; Modified from "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView\ModifyPath" - ExecWait `"$R1" /install appguid=${WEBVIEW2APPGUID}&needsadmin=true` $1 - ${If} $1 = 0 - DetailPrint "$(webview2InstallSuccess)" - ${Else} - MessageBox MB_ICONEXCLAMATION|MB_ABORTRETRYIGNORE "$(webview2InstallError)" IDIGNORE ignore IDRETRY update_webview - Quit - ignore: - ${EndIf} - ${EndIf} - ${EndIf} - !endif - ${EndIf} -SectionEnd - -Section VCRedist - ; Check if VC++ Redistributable is already installed - ; Check for Visual Studio 2015-2022 redistributable (14.0 or higher) - ReadRegStr $0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version" - ${If} $0 == "" - ; Try alternative registry location - ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64" "Version" - ${EndIf} - ${If} $0 == "" - ; Try checking for any version of Visual C++ 2015-2022 Redistributable - ReadRegStr $0 HKLM "SOFTWARE\Classes\Installer\Dependencies\Microsoft.VS.VC_RuntimeMinimumVSU_amd64,v14" "Version" - ${EndIf} - - ${If} $0 == "" - ; VC++ Redistributable not found, need to install - DetailPrint "Visual C++ Redistributable not found, downloading and installing..." - - ; Download VC++ Redistributable - Delete "$TEMP\${VCREDIST_FILENAME}" - DetailPrint "Downloading Visual C++ Redistributable..." - NSISdl::download "${VCREDIST_URL}" "$TEMP\${VCREDIST_FILENAME}" - Pop $1 - - ${If} $1 == "success" - DetailPrint "Visual C++ Redistributable download successful" - - ; Install VC++ Redistributable silently - DetailPrint "Installing Visual C++ Redistributable..." - ExecWait '"$TEMP\${VCREDIST_FILENAME}" /quiet /norestart' $2 - - ${If} $2 == 0 - DetailPrint "Visual C++ Redistributable installed successfully" - ${ElseIf} $2 == 1638 - DetailPrint "Visual C++ Redistributable already installed (newer version)" - ${ElseIf} $2 == 3010 - DetailPrint "Visual C++ Redistributable installed successfully (restart required)" - ; You might want to handle restart requirement here - ${Else} - DetailPrint "Visual C++ Redistributable installation failed with exit code: $2" - MessageBox MB_ICONEXCLAMATION|MB_YESNO "Visual C++ Redistributable installation failed. Continue anyway?" IDYES continue_install - Abort "Installation cancelled due to Visual C++ Redistributable failure" - continue_install: - ${EndIf} - - ; Clean up downloaded file - Delete "$TEMP\${VCREDIST_FILENAME}" - ${Else} - DetailPrint "Failed to download Visual C++ Redistributable: $1" - MessageBox MB_ICONEXCLAMATION|MB_YESNO "Failed to download Visual C++ Redistributable. Continue anyway?" IDYES continue_install_download_fail - Abort "Installation cancelled due to download failure" - continue_install_download_fail: - ${EndIf} - ${Else} - DetailPrint "Visual C++ Redistributable already installed (version: $0)" - ${EndIf} -SectionEnd - -Section Install - SetDetailsPrint none - SetOutPath $INSTDIR - - !ifmacrodef NSIS_HOOK_PREINSTALL - !insertmacro NSIS_HOOK_PREINSTALL - !endif - - !insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}" - - ; Copy main executable - File "${MAINBINARYSRCPATH}" - - ; Copy resources - CreateDirectory "$INSTDIR\resources" - CreateDirectory "$INSTDIR\resources\pre-install" - SetOutPath $INSTDIR - File /a "/oname=vulkan-1.dll" "D:\a\jan\jan\src-tauri\resources\lib\vulkan-1.dll" - File /a "/oname=LICENSE" "D:\a\jan\jan\src-tauri\resources\LICENSE" - SetOutPath "$INSTDIR\resources\pre-install" - File /nonfatal /a /r "D:\a\jan\jan\src-tauri\resources\pre-install\" - SetOutPath $INSTDIR - - ; Copy external binaries - File /a "/oname=bun.exe" "D:\a\jan\jan\src-tauri\resources\bin\bun-x86_64-pc-windows-msvc.exe" - File /a "/oname=uv.exe" "D:\a\jan\jan\src-tauri\resources\bin\uv-x86_64-pc-windows-msvc.exe" - - ; Create file associations - - ; Register deep links - - ; Create uninstaller - WriteUninstaller "$INSTDIR\uninstall.exe" - - ; Save $INSTDIR in registry for future installations - WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR - - !if "${INSTALLMODE}" == "both" - ; Save install mode to be selected by default for the next installation such as updating - ; or when uninstalling - WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 - !endif - - ; Remove old main binary if it doesn't match new main binary name - ReadRegStr $OldMainBinaryName SHCTX "${UNINSTKEY}" "MainBinaryName" - ${If} $OldMainBinaryName != "" - ${AndIf} $OldMainBinaryName != "${MAINBINARYNAME}.exe" - Delete "$INSTDIR\$OldMainBinaryName" - ${EndIf} - - ; Save current MAINBINARYNAME for future updates - WriteRegStr SHCTX "${UNINSTKEY}" "MainBinaryName" "${MAINBINARYNAME}.exe" - - ; Registry information for add/remove programs - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" - WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" - WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" - WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" - WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" - WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" - WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" - - ${GetSize} "$INSTDIR" "/M=uninstall.exe /S=0K /G=0" $0 $1 $2 - IntOp $0 $0 + ${ESTIMATEDSIZE} - IntFmt $0 "0x%08X" $0 - WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$0" - - !if "${HOMEPAGE}" != "" - WriteRegStr SHCTX "${UNINSTKEY}" "URLInfoAbout" "${HOMEPAGE}" - WriteRegStr SHCTX "${UNINSTKEY}" "URLUpdateInfo" "${HOMEPAGE}" - WriteRegStr SHCTX "${UNINSTKEY}" "HelpLink" "${HOMEPAGE}" - !endif - - ; Create start menu shortcut - !insertmacro MUI_STARTMENU_WRITE_BEGIN Application - Call CreateOrUpdateStartMenuShortcut - !insertmacro MUI_STARTMENU_WRITE_END - - ; Create desktop shortcut for silent and passive installers - ; because finish page will be skipped - ${If} $PassiveMode = 1 - ${OrIf} ${Silent} - Call CreateOrUpdateDesktopShortcut - ${EndIf} - - !ifmacrodef NSIS_HOOK_POSTINSTALL - !insertmacro NSIS_HOOK_POSTINSTALL - !endif - - ; Auto close this page for passive mode - ${If} $PassiveMode = 1 - SetAutoClose true - ${EndIf} -SectionEnd - -Function .onInstSuccess - ; Check for `/R` flag only in silent and passive installers because - ; GUI installer has a toggle for the user to (re)start the app - ${If} $PassiveMode = 1 - ${OrIf} ${Silent} - ; ${GetOptions} $CMDLINE "/R" $R0 - ; ${IfNot} ${Errors} - ${GetOptions} $CMDLINE "/ARGS" $R0 - nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0" - ; ${EndIf} - ${EndIf} -FunctionEnd - -Function un.onInit - !insertmacro SetContext - - !if "${INSTALLMODE}" == "both" - !insertmacro MULTIUSER_UNINIT - !endif - - !insertmacro MUI_UNGETLANGUAGE - - ${GetOptions} $CMDLINE "/P" $PassiveMode - ${IfNot} ${Errors} - StrCpy $PassiveMode 1 - ${EndIf} - - ${GetOptions} $CMDLINE "/UPDATE" $UpdateMode - ${IfNot} ${Errors} - StrCpy $UpdateMode 1 - ${EndIf} -FunctionEnd - -Section Uninstall - SetDetailsPrint none - - !ifmacrodef NSIS_HOOK_PREUNINSTALL - !insertmacro NSIS_HOOK_PREUNINSTALL - !endif - - !insertmacro CheckIfAppIsRunning "${MAINBINARYNAME}.exe" "${PRODUCTNAME}" - - ; Delete the app directory and its content from disk - ; Copy main executable - Delete "$INSTDIR\${MAINBINARYNAME}.exe" - - ; Delete LICENSE file - Delete "$INSTDIR\LICENSE" - - ; Delete resources - Delete "$INSTDIR\resources\pre-install\janhq-assistant-extension-1.0.2.tgz" - Delete "$INSTDIR\resources\pre-install\janhq-conversational-extension-1.0.0.tgz" - Delete "$INSTDIR\resources\pre-install\janhq-engine-management-extension-1.0.3.tgz" - Delete "$INSTDIR\resources\pre-install\janhq-hardware-management-extension-1.0.0.tgz" - Delete "$INSTDIR\resources\pre-install\janhq-inference-cortex-extension-1.0.25.tgz" - Delete "$INSTDIR\resources\pre-install\janhq-model-extension-1.0.36.tgz" - - ; Delete external binaries - Delete "$INSTDIR\bun.exe" - Delete "$INSTDIR\uv.exe" - - ; Delete app associations - - ; Delete deep links - - - ; Delete uninstaller - Delete "$INSTDIR\uninstall.exe" - - RMDir /REBOOTOK "$INSTDIR\resources\pre-install" - RMDir /r /REBOOTOK "$INSTDIR\resources" - RMDir /r "$INSTDIR" - - ; Remove shortcuts if not updating - ${If} $UpdateMode <> 1 - !insertmacro DeleteAppUserModelId - - ; Remove start menu shortcut - !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder - !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - RMDir "$SMPROGRAMS\$AppStartMenuFolder" - ${EndIf} - !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" - Delete "$SMPROGRAMS\${PRODUCTNAME}.lnk" - ${EndIf} - - ; Remove desktop shortcuts - !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Pop $0 - ${If} $0 = 1 - !insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk" - Delete "$DESKTOP\${PRODUCTNAME}.lnk" - ${EndIf} - ${EndIf} - - ; Remove registry information for add/remove programs - !if "${INSTALLMODE}" == "both" - DeleteRegKey SHCTX "${UNINSTKEY}" - !else if "${INSTALLMODE}" == "perMachine" - DeleteRegKey HKLM "${UNINSTKEY}" - !else - DeleteRegKey HKCU "${UNINSTKEY}" - !endif - - ; Removes the Autostart entry for ${PRODUCTNAME} from the HKCU Run key if it exists. - ; This ensures the program does not launch automatically after uninstallation if it exists. - ; If it doesn't exist, it does nothing. - ; We do this when not updating (to preserve the registry value on updates) - ${If} $UpdateMode <> 1 - DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}" - ${EndIf} - - ; Delete app data if the checkbox is selected - ; and if not updating - ${If} $DeleteAppDataCheckboxState = 1 - ${AndIf} $UpdateMode <> 1 - ; Clear the install location $INSTDIR from registry - DeleteRegKey SHCTX "${MANUPRODUCTKEY}" - DeleteRegKey /ifempty SHCTX "${MANUKEY}" - - ; Clear the install language from registry - DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" - DeleteRegKey /ifempty HKCU "${MANUPRODUCTKEY}" - DeleteRegKey /ifempty HKCU "${MANUKEY}" - - SetShellVarContext current - RmDir /r "$APPDATA\${BUNDLEID}" - RmDir /r "$LOCALAPPDATA\${BUNDLEID}" - ${EndIf} - - !ifmacrodef NSIS_HOOK_POSTUNINSTALL - !insertmacro NSIS_HOOK_POSTUNINSTALL - !endif - - ; Auto close if passive mode or updating - ${If} $PassiveMode = 1 - ${OrIf} $UpdateMode = 1 - SetAutoClose true - ${EndIf} -SectionEnd - -Function RestorePreviousInstallLocation - ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" - StrCmp $4 "" +2 0 - StrCpy $INSTDIR $4 -FunctionEnd - -Function Skip - Abort -FunctionEnd - -Function SkipIfPassive - ${IfThen} $PassiveMode = 1 ${|} Abort ${|} -FunctionEnd -Function un.SkipIfPassive - ${IfThen} $PassiveMode = 1 ${|} Abort ${|} -FunctionEnd - -Function CreateOrUpdateStartMenuShortcut - ; We used to use product name as MAINBINARYNAME - ; migrate old shortcuts to target the new MAINBINARYNAME - StrCpy $R0 0 - - !insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - StrCpy $R0 1 - ${EndIf} - - !insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - StrCpy $R0 1 - ${EndIf} - - ${If} $R0 = 1 - Return - ${EndIf} - - ; Skip creating shortcut if in update mode or no shortcut mode - ; but always create if migrating from wix - ${If} $WixMode = 0 - ${If} $UpdateMode = 1 - ${OrIf} $NoShortcutMode = 1 - Return - ${EndIf} - ${EndIf} - - !if "${STARTMENUFOLDER}" != "" - CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" - CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" - !else - CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk" - !endif -FunctionEnd - -Function CreateOrUpdateDesktopShortcut - ; We used to use product name as MAINBINARYNAME - ; migrate old shortcuts to target the new MAINBINARYNAME - !insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName" - Pop $0 - ${If} $0 = 1 - !insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - Return - ${EndIf} - - ; Skip creating shortcut if in update mode or no shortcut mode - ; but always create if migrating from wix - ${If} $WixMode = 0 - ${If} $UpdateMode = 1 - ${OrIf} $NoShortcutMode = 1 - Return - ${EndIf} - ${EndIf} - - CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" - !insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk" -FunctionEnd \ No newline at end of file From 6c8358028326c3629e704150de739a57c7b089b7 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 15:47:45 +0700 Subject: [PATCH 07/56] ci: fix syntax error in windows build workflow --- .github/workflows/template-tauri-build-windows-x64.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 4d5c2befb..7e418ddc1 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -131,7 +131,6 @@ jobs: chmod +x .github/scripts/rename-workspace.sh .github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }} cat ./package.json - else fi - name: Install AzureSignTool From 7a15c2646d7f4abe8415c73ef3b42371ee2d2e2e Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 16:30:32 +0700 Subject: [PATCH 08/56] ci: update signCommand for tauri config windows --- .github/workflows/template-tauri-build-windows-x64.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 7e418ddc1..1f25e5295 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -97,11 +97,18 @@ jobs: mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json - + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------Cargo.toml---------" cat ./src-tauri/Cargo.toml + # Add sign commands to tauri.windows.conf.json + jq '.bundle.windows.signCommand = "powershell -ExecutionPolicy Bypass -File ./sign.ps1 %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json + mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json + + echo "---------tauri.windows.conf.json---------" + cat ./src-tauri/tauri.windows.conf.json + # Temporarily enable devtool on prod build ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" cat ./src-tauri/Cargo.toml From 2a905c6c02784075ed11ac19d5118ec2082542e6 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 18:57:06 +0700 Subject: [PATCH 09/56] ci: standardize nightly external build on windows --- ...plate-tauri-build-windows-x64-external.yml | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/.github/workflows/template-tauri-build-windows-x64-external.yml b/.github/workflows/template-tauri-build-windows-x64-external.yml index 59a200093..ed1d601a3 100644 --- a/.github/workflows/template-tauri-build-windows-x64-external.yml +++ b/.github/workflows/template-tauri-build-windows-x64-external.yml @@ -49,8 +49,6 @@ jobs: # Update tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version | .bundle.createUpdaterArtifacts = false' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json - jq '.bundle.windows.nsis.template = "tauri.bundle.windows.nsis.template"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json - mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json jq '.bundle.windows.signCommand = "echo External build - skipping signature: %1"' ./src-tauri/tauri.windows.conf.json > /tmp/tauri.windows.conf.json mv /tmp/tauri.windows.conf.json ./src-tauri/tauri.windows.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json @@ -59,58 +57,30 @@ jobs: ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" echo "---------Cargo.toml---------" cat ./src-tauri/Cargo.toml - - generate_build_version() { - ### Examble - ### input 0.5.6 output will be 0.5.6 and 0.5.6.0 - ### input 0.5.6-rc2-beta output will be 0.5.6 and 0.5.6.2 - ### input 0.5.6-1213 output will be 0.5.6 and and 0.5.6.1213 - local new_version="$1" - local base_version - local t_value - - # Check if it has a "-" - if [[ "$new_version" == *-* ]]; then - base_version="${new_version%%-*}" # part before - - suffix="${new_version#*-}" # part after - - - # Check if it is rcX-beta - if [[ "$suffix" =~ ^rc([0-9]+)-beta$ ]]; then - t_value="${BASH_REMATCH[1]}" - else - t_value="$suffix" - fi - else - base_version="$new_version" - t_value="0" - fi - - # Export two values - new_base_version="$base_version" - new_build_version="${base_version}.${t_value}" - } - generate_build_version ${{ inputs.new_version }} - sed -i "s/jan_version/$new_base_version/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_build/$new_build_version/g" ./src-tauri/tauri.bundle.windows.nsis.template - if [ "${{ inputs.channel }}" != "stable" ]; then jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + + # Update product name + jq --arg name "Jan-${{ inputs.channel }}" '.productName = $name' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json + mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json + chmod +x .github/scripts/rename-tauri-app.sh .github/scripts/rename-tauri-app.sh ./src-tauri/tauri.conf.json ${{ inputs.channel }} + + echo "---------tauri.conf.json---------" + cat ./src-tauri/tauri.conf.json + + # Update Cargo.toml ctoml ./src-tauri/Cargo.toml package.name "Jan-${{ inputs.channel }}" ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" + echo "------------------" + cat ./src-tauri/Cargo.toml + chmod +x .github/scripts/rename-workspace.sh .github/scripts/rename-workspace.sh ./package.json ${{ inputs.channel }} - sed -i "s/jan_productname/Jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_mainbinaryname/jan-${{ inputs.channel }}/g" ./src-tauri/tauri.bundle.windows.nsis.template - else - sed -i "s/jan_productname/Jan/g" ./src-tauri/tauri.bundle.windows.nsis.template - sed -i "s/jan_mainbinaryname/jan/g" ./src-tauri/tauri.bundle.windows.nsis.template + cat ./package.json fi - echo "---------nsis.template---------" - cat ./src-tauri/tauri.bundle.windows.nsis.template - - name: Build app shell: bash run: | From d3fff154d4b432cd142ce997215cd1ad3eb921d2 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 23 Sep 2025 20:17:51 +0700 Subject: [PATCH 10/56] fix: download management ui and double refresh model --- web-app/src/containers/DownloadManegement.tsx | 29 +- web-app/src/containers/LeftPanel.tsx | 24 +- web-app/src/containers/ModelCombobox.tsx | 294 +++++++++++------- 3 files changed, 202 insertions(+), 145 deletions(-) diff --git a/web-app/src/containers/DownloadManegement.tsx b/web-app/src/containers/DownloadManegement.tsx index f91a943d3..92bb3ee85 100644 --- a/web-app/src/containers/DownloadManegement.tsx +++ b/web-app/src/containers/DownloadManegement.tsx @@ -400,20 +400,23 @@ export function DownloadManagement() { className="text-main-view-fg/70 cursor-pointer" title="Cancel download" onClick={() => { - serviceHub.models().abortDownload(download.name).then(() => { - toast.info( - t('common:toast.downloadCancelled.title'), - { - id: 'cancel-download', - description: t( - 'common:toast.downloadCancelled.description' - ), + serviceHub + .models() + .abortDownload(download.name) + .then(() => { + toast.info( + t('common:toast.downloadCancelled.title'), + { + id: 'cancel-download', + description: t( + 'common:toast.downloadCancelled.description' + ), + } + ) + if (downloadProcesses.length === 0) { + setIsPopoverOpen(false) } - ) - if (downloadProcesses.length === 0) { - setIsPopoverOpen(false) - } - }) + }) }} /> diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index da596dd4a..1ad0ef560 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -35,7 +35,7 @@ import { toast } from 'sonner' import { DownloadManagement } from '@/containers/DownloadManegement' import { useSmallScreen } from '@/hooks/useMediaQuery' import { useClickOutside } from '@/hooks/useClickOutside' -import { useDownloadStore } from '@/hooks/useDownloadStore' + import { DeleteAllThreadsDialog } from '@/containers/dialogs' const mainMenus = [ @@ -122,7 +122,7 @@ const LeftPanel = () => { ) { if (currentIsSmallScreen && open) { setLeftPanel(false) - } else if(!open) { + } else if (!open) { setLeftPanel(true) } prevScreenSizeRef.current = currentIsSmallScreen @@ -179,8 +179,6 @@ const LeftPanel = () => { } }, [isSmallScreen, open]) - const { downloads, localDownloadingModels } = useDownloadStore() - return ( <> {/* Backdrop overlay for small screens */} @@ -262,15 +260,8 @@ const LeftPanel = () => { )} -
-
0 || localDownloadingModels.size > 0 - ? 'h-[calc(100%-200px)]' - : 'h-[calc(100%-140px)]' - )} - > +
+
{IS_MACOS && (
{ - +
@@ -468,8 +461,9 @@ const LeftPanel = () => { ) })} -
+ +
diff --git a/web-app/src/containers/ModelCombobox.tsx b/web-app/src/containers/ModelCombobox.tsx index ea5b3d670..5ed8ed14d 100644 --- a/web-app/src/containers/ModelCombobox.tsx +++ b/web-app/src/containers/ModelCombobox.tsx @@ -7,8 +7,15 @@ import { cn } from '@/lib/utils' import { useTranslation } from '@/i18n/react-i18next-compat' // Hook for the dropdown position -function useDropdownPosition(open: boolean, containerRef: React.RefObject) { - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }) +function useDropdownPosition( + open: boolean, + containerRef: React.RefObject +) { + const [dropdownPosition, setDropdownPosition] = useState({ + top: 0, + left: 0, + width: 0, + }) const updateDropdownPosition = useCallback(() => { if (containerRef.current) { @@ -51,10 +58,18 @@ function useDropdownPosition(open: boolean, containerRef: React.RefObject string }) => ( +const ErrorSection = ({ + error, + t, +}: { + error: string + t: (key: string) => string +}) => (
- {t('common:failedToLoadModels')} + + {t('common:failedToLoadModels')} +
{error}
@@ -67,12 +82,20 @@ const LoadingSection = ({ t }: { t: (key: string) => string }) => (
) -const EmptySection = ({ inputValue, t }: { inputValue: string; t: (key: string, options?: Record) => string }) => ( +const EmptySection = ({ + inputValue, + t, +}: { + inputValue: string + t: (key: string, options?: Record) => string +}) => (
{inputValue.trim() ? ( - {t('common:noModelsFoundFor', { searchValue: inputValue })} + + {t('common:noModelsFoundFor', { searchValue: inputValue })} + ) : ( {t('common:noModels')} )} @@ -86,7 +109,7 @@ const ModelsList = ({ value, highlightedIndex, onModelSelect, - onHighlight + onHighlight, }: { filteredModels: string[] value: string @@ -127,67 +150,95 @@ function useKeyboardNavigation( onModelSelect: (model: string) => void, dropdownRef: React.RefObject ) { - // Scroll to the highlighted element useEffect(() => { if (highlightedIndex >= 0 && dropdownRef.current) { requestAnimationFrame(() => { - const modelElements = dropdownRef.current?.querySelectorAll('[data-model]') - const highlightedElement = modelElements?.[highlightedIndex] as HTMLElement + const modelElements = + dropdownRef.current?.querySelectorAll('[data-model]') + const highlightedElement = modelElements?.[ + highlightedIndex + ] as HTMLElement if (highlightedElement) { highlightedElement.scrollIntoView({ block: 'nearest', - behavior: 'auto' + behavior: 'auto', }) } }) } }, [highlightedIndex, dropdownRef]) - const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - // Open the dropdown with the arrows if closed - if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { - if (models.length > 0) { - e.preventDefault() - setOpen(true) - setHighlightedIndex(0) - } - return - } - - if (!open) return - - switch (e.key) { - case 'ArrowDown': - e.preventDefault() - setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev < filteredModels.length - 1 ? prev + 1 : 0)) - break - case 'ArrowUp': - e.preventDefault() - setHighlightedIndex((prev: number) => filteredModels.length === 0 ? 0 : (prev > 0 ? prev - 1 : filteredModels.length - 1)) - break - case 'Enter': - e.preventDefault() - if (highlightedIndex >= 0 && highlightedIndex < filteredModels.length) { - onModelSelect(filteredModels[highlightedIndex]) + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + // Open the dropdown with the arrows if closed + if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + if (models.length > 0) { + e.preventDefault() + setOpen(true) + setHighlightedIndex(0) } - break - case 'Escape': - e.preventDefault() - e.stopPropagation() - setOpen(false) - setHighlightedIndex(-1) - break - case 'PageUp': - e.preventDefault() - setHighlightedIndex(0) - break - case 'PageDown': - e.preventDefault() - setHighlightedIndex(filteredModels.length - 1) - break - } - }, [open, setOpen, models.length, filteredModels, highlightedIndex, setHighlightedIndex, onModelSelect]) + return + } + + if (!open) return + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setHighlightedIndex((prev: number) => + filteredModels.length === 0 + ? 0 + : prev < filteredModels.length - 1 + ? prev + 1 + : 0 + ) + break + case 'ArrowUp': + e.preventDefault() + setHighlightedIndex((prev: number) => + filteredModels.length === 0 + ? 0 + : prev > 0 + ? prev - 1 + : filteredModels.length - 1 + ) + break + case 'Enter': + e.preventDefault() + if ( + highlightedIndex >= 0 && + highlightedIndex < filteredModels.length + ) { + onModelSelect(filteredModels[highlightedIndex]) + } + break + case 'Escape': + e.preventDefault() + e.stopPropagation() + setOpen(false) + setHighlightedIndex(-1) + break + case 'PageUp': + e.preventDefault() + setHighlightedIndex(0) + break + case 'PageDown': + e.preventDefault() + setHighlightedIndex(filteredModels.length - 1) + break + } + }, + [ + open, + setOpen, + models.length, + filteredModels, + highlightedIndex, + setHighlightedIndex, + onModelSelect, + ] + ) return { handleKeyDown } } @@ -266,13 +317,18 @@ export function ModelCombobox({ } const events = ['mousedown', 'touchstart'] - events.forEach(eventType => { - document.addEventListener(eventType, handleClickOutside, { capture: true, passive: true }) + events.forEach((eventType) => { + document.addEventListener(eventType, handleClickOutside, { + capture: true, + passive: true, + }) }) return () => { - events.forEach(eventType => { - document.removeEventListener(eventType, handleClickOutside, { capture: true }) + events.forEach((eventType) => { + document.removeEventListener(eventType, handleClickOutside, { + capture: true, + }) }) } }, [open]) @@ -286,26 +342,32 @@ export function ModelCombobox({ }, []) // Handler for the input change - const handleInputChange = useCallback((newValue: string) => { - setInputValue(newValue) - onChange(newValue) + const handleInputChange = useCallback( + (newValue: string) => { + setInputValue(newValue) + onChange(newValue) - // Open the dropdown if the user types and there are models - if (newValue.trim() && models.length > 0) { - setOpen(true) - } else { - setOpen(false) - } - }, [onChange, models.length]) + // Open the dropdown if the user types and there are models + if (newValue.trim() && models.length > 0) { + setOpen(true) + } else { + setOpen(false) + } + }, + [onChange, models.length] + ) // Handler for the model selection - const handleModelSelect = useCallback((model: string) => { - setInputValue(model) - onChange(model) - setOpen(false) - setHighlightedIndex(-1) - inputRef.current?.focus() - }, [onChange]) + const handleModelSelect = useCallback( + (model: string) => { + setInputValue(model) + onChange(model) + setOpen(false) + setHighlightedIndex(-1) + inputRef.current?.focus() + }, + [onChange] + ) // Hook for the keyboard navigation const { handleKeyDown } = useKeyboardNavigation( @@ -376,54 +438,52 @@ export function ModelCombobox({ onClick={handleDropdownToggle} className="h-6 w-6 p-0 no-underline hover:bg-main-view-fg/10" > - {loading ? ( - - ) : ( - - )} +
{/* Custom dropdown rendered as portal */} - {open && dropdownPosition.width > 0 && createPortal( -
e.stopPropagation()} - onWheel={(e) => e.stopPropagation()} - > - {/* Error state */} - {error && } + {open && + dropdownPosition.width > 0 && + createPortal( +
e.stopPropagation()} + onWheel={(e) => e.stopPropagation()} + > + {/* Error state */} + {error && } - {/* Loading state */} - {loading && } + {/* Loading state */} + {loading && } - {/* Models list */} - {!loading && !error && ( - filteredModels.length === 0 ? ( - - ) : ( - - ) - )} -
, - document.body - )} + {/* Models list */} + {!loading && + !error && + (filteredModels.length === 0 ? ( + + ) : ( + + ))} +
, + document.body + )}
) From 3a5580c725df6ac548baa64f9a46db995f1498d2 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 23 Sep 2025 20:25:05 +0700 Subject: [PATCH 11/56] chore: update test case --- .../__tests__/ModelCombobox.test.tsx | 125 +++++++++++------- 1 file changed, 79 insertions(+), 46 deletions(-) diff --git a/web-app/src/containers/__tests__/ModelCombobox.test.tsx b/web-app/src/containers/__tests__/ModelCombobox.test.tsx index 38f9b97c8..1c0815549 100644 --- a/web-app/src/containers/__tests__/ModelCombobox.test.tsx +++ b/web-app/src/containers/__tests__/ModelCombobox.test.tsx @@ -1,4 +1,12 @@ -import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from 'vitest' +import { + describe, + it, + expect, + vi, + beforeEach, + beforeAll, + afterAll, +} from 'vitest' import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' import userEvent from '@testing-library/user-event' import '@testing-library/jest-dom/vitest' @@ -11,7 +19,8 @@ vi.mock('@/i18n/react-i18next-compat', () => ({ t: (key: string, options?: Record) => { if (key === 'common:failedToLoadModels') return 'Failed to load models' if (key === 'common:loading') return 'Loading' - if (key === 'common:noModelsFoundFor') return `No models found for "${options?.searchValue}"` + if (key === 'common:noModelsFoundFor') + return `No models found for "${options?.searchValue}"` if (key === 'common:noModels') return 'No models available' return key }, @@ -21,7 +30,7 @@ vi.mock('@/i18n/react-i18next-compat', () => ({ describe('ModelCombobox', () => { const mockOnChange = vi.fn() const mockOnRefresh = vi.fn() - + const defaultProps = { value: '', onChange: mockOnChange, @@ -64,7 +73,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() expect(input).toHaveAttribute('placeholder', 'Type or select a model...') @@ -74,7 +83,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByRole('textbox') expect(input).toHaveAttribute('placeholder', 'Choose a model') }) @@ -83,7 +92,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const button = screen.getByRole('button') expect(button).toBeInTheDocument() }) @@ -92,7 +101,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByDisplayValue('gpt-4') expect(input).toBeInTheDocument() }) @@ -110,7 +119,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByRole('textbox') const button = screen.getByRole('button') @@ -118,27 +127,19 @@ describe('ModelCombobox', () => { expect(button).toBeDisabled() }) - it('shows loading spinner in trigger button', () => { - act(() => { - render() - }) - - const button = screen.getByRole('button') - const spinner = button.querySelector('.animate-spin') - expect(spinner).toBeInTheDocument() - }) - it('shows loading section when dropdown is opened during loading', async () => { const user = userEvent.setup() render() - + // Click input to trigger dropdown opening const input = screen.getByRole('textbox') await user.click(input) - + // Wait for dropdown to appear and check loading section await waitFor(() => { - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() expect(screen.getByText('Loading')).toBeInTheDocument() }) @@ -179,7 +180,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() }) @@ -188,7 +189,7 @@ describe('ModelCombobox', () => { act(() => { render() }) - + const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() }) @@ -259,7 +260,7 @@ describe('ModelCombobox', () => { /> ) }) - + const input = screen.getByRole('textbox') expect(input).toBeInTheDocument() expect(input).toBeDisabled() @@ -273,7 +274,9 @@ describe('ModelCombobox', () => { await user.click(button) await waitFor(() => { - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() }) }) @@ -287,7 +290,9 @@ describe('ModelCombobox', () => { expect(input).toHaveFocus() await waitFor(() => { - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() }) }) @@ -313,9 +318,11 @@ describe('ModelCombobox', () => { await waitFor(() => { // Dropdown should be open - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() - + // Should show GPT models expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument() expect(screen.getByText('gpt-4')).toBeInTheDocument() @@ -344,10 +351,14 @@ describe('ModelCombobox', () => { await waitFor(() => { // Dropdown should be open - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() // Should show empty state message - expect(screen.getByText('No models found for "nonexistent"')).toBeInTheDocument() + expect( + screen.getByText('No models found for "nonexistent"') + ).toBeInTheDocument() }) }) @@ -358,12 +369,12 @@ describe('ModelCombobox', () => { const input = screen.getByRole('textbox') await user.click(input) - + await waitFor(() => { const modelOption = screen.getByText('gpt-4') expect(modelOption).toBeInTheDocument() }) - + const modelOption = screen.getByText('gpt-4') await user.click(modelOption) @@ -385,7 +396,9 @@ describe('ModelCombobox', () => { it('displays error message in dropdown', async () => { const user = userEvent.setup() - render() + render( + + ) const input = screen.getByRole('textbox') // Click input to open dropdown @@ -393,7 +406,9 @@ describe('ModelCombobox', () => { await waitFor(() => { // Dropdown should be open - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() // Error messages should be displayed expect(screen.getByText('Failed to load models')).toBeInTheDocument() @@ -404,7 +419,13 @@ describe('ModelCombobox', () => { it('calls onRefresh when refresh button is clicked', async () => { const user = userEvent.setup() const localMockOnRefresh = vi.fn() - render() + render( + + ) const input = screen.getByRole('textbox') // Click input to open dropdown @@ -412,13 +433,19 @@ describe('ModelCombobox', () => { await waitFor(() => { // Dropdown should be open with error section - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() - const refreshButton = document.querySelector('[aria-label="Refresh models"]') + const refreshButton = document.querySelector( + '[aria-label="Refresh models"]' + ) expect(refreshButton).toBeInTheDocument() }) - const refreshButton = document.querySelector('[aria-label="Refresh models"]') + const refreshButton = document.querySelector( + '[aria-label="Refresh models"]' + ) if (refreshButton) { await user.click(refreshButton) expect(localMockOnRefresh).toHaveBeenCalledTimes(1) @@ -435,7 +462,9 @@ describe('ModelCombobox', () => { expect(input).toHaveFocus() await waitFor(() => { - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() }) }) @@ -446,16 +475,18 @@ describe('ModelCombobox', () => { const input = screen.getByRole('textbox') input.focus() - + // ArrowDown should open dropdown await user.keyboard('{ArrowDown}') - + await waitFor(() => { // Dropdown should be open - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() }) - + // Navigate to second item await user.keyboard('{ArrowDown}') @@ -474,13 +505,15 @@ describe('ModelCombobox', () => { const input = screen.getByRole('textbox') // Type 'gpt' to open dropdown and filter models await user.type(input, 'gpt') - + await waitFor(() => { // Dropdown should be open with filtered models - const dropdown = document.querySelector('[data-dropdown="model-combobox"]') + const dropdown = document.querySelector( + '[data-dropdown="model-combobox"]' + ) expect(dropdown).toBeInTheDocument() }) - + // Navigate to highlight first model and select it await user.keyboard('{ArrowDown}') await user.keyboard('{Enter}') From f1d97ac8348de44cb0a29eb38b050898d390fa9c Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 20:42:34 +0700 Subject: [PATCH 12/56] chore: install vc_redist.x64 from script --- scripts/download-lib.mjs | 19 ++++++++++++++ src-tauri/tauri.windows.conf.json | 2 +- src-tauri/windows/hooks.nsh | 42 +++++++++++++------------------ 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/scripts/download-lib.mjs b/scripts/download-lib.mjs index d2086b36e..ab73b8d79 100644 --- a/scripts/download-lib.mjs +++ b/scripts/download-lib.mjs @@ -77,6 +77,25 @@ async function main() { // Expect EEXIST error } + // Download VC++ Redistributable 17 + if (platform == 'win32') { + const vcFilename = 'vc_redist.x64.exe' + const vcUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe' + + console.log(`Downloading VC++ Redistributable...`) + const vcSavePath = path.join(tempDir, vcFilename) + if (!fs.existsSync(vcSavePath)) { + await download(vcUrl, vcSavePath) + } + + // copy to tauri resources + try { + copySync(vcSavePath, resourcesDir) + } catch (err) { + // Expect EEXIST error + } + } + console.log('Downloads completed.') } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 664053705..16cb9b10a 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,7 +1,7 @@ { "bundle": { "targets": ["nsis"], - "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/LICENSE"], + "resources": ["resources/pre-install/**/*", "resources/lib/vulkan-1.dll", "resources/lib/vc_redist.x64.exe", "resources/LICENSE"], "externalBin": ["resources/bin/bun", "resources/bin/uv"], "windows": { "nsis": { diff --git a/src-tauri/windows/hooks.nsh b/src-tauri/windows/hooks.nsh index 60aec1c80..5e1a32141 100644 --- a/src-tauri/windows/hooks.nsh +++ b/src-tauri/windows/hooks.nsh @@ -8,37 +8,31 @@ ${If} $0 == "" ; VC++ Redistributable not found, need to install - DetailPrint "Visual C++ Redistributable not found, downloading and installing..." - - ; Download VC++ Redistributable - Delete "$TEMP\vc_redist.x64.exe" - DetailPrint "Downloading Visual C++ Redistributable..." - NSISdl::download "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe" - Pop $1 - - ${If} $1 == "success" - DetailPrint "Visual C++ Redistributable download successful" - - ; Install VC++ Redistributable silently + DetailPrint "Visual C++ Redistributable not found, installing from bundled file..." + + ; Install from bundled EXE if not installed + ${If} ${FileExists} "$INSTDIR\resources\lib\vc_redist.x64.exe" DetailPrint "Installing Visual C++ Redistributable..." - ExecWait '"$TEMP\vc_redist.x64.exe" /quiet /norestart' $2 - - ${If} $2 == 0 + ; Copy to TEMP folder and then execute installer + CopyFiles "$INSTDIR\resources\lib\vc_redist.x64.exe" "$TEMP\vc_redist.x64.exe" + ExecWait '"$TEMP\vc_redist.x64.exe" /quiet /norestart' $1 + + ; Check whether installation process exited successfully (code 0) or not + ${If} $1 == 0 DetailPrint "Visual C++ Redistributable installed successfully" - ${ElseIf} $2 == 1638 + ${ElseIf} $1 == 1638 DetailPrint "Visual C++ Redistributable already installed (newer version)" - ${ElseIf} $2 == 3010 + ${ElseIf} $1 == 3010 DetailPrint "Visual C++ Redistributable installed successfully (restart required)" ${Else} - DetailPrint "Visual C++ installation failed with exit code: $2" - MessageBox MB_ICONEXCLAMATION "Visual C++ installation failed. Some features may not work." + DetailPrint "Visual C++ installation failed with exit code: $1" ${EndIf} - - ; Clean up downloaded file + + ; Clean up setup files from TEMP and your installed app Delete "$TEMP\vc_redist.x64.exe" + Delete "$INSTDIR\resources\lib\vc_redist.x64.exe" ${Else} - DetailPrint "Failed to download Visual C++ Redistributable: $1" - MessageBox MB_ICONEXCLAMATION "Failed to download Visual C++ Redistributable. Some features may not work." + DetailPrint "Visual C++ Redistributable not found at expected location: $INSTDIR\resources\lib\vc_redist.x64.exe" ${EndIf} ${Else} DetailPrint "Visual C++ Redistributable already installed (version: $0)" @@ -57,7 +51,7 @@ ; Optional cleanup - remove from resources folder Delete "$INSTDIR\resources\lib\vulkan-1.dll" - ; Only remove the lib directory if it's empty + ; Only remove the lib directory if it's empty after removing both files RMDir "$INSTDIR\resources\lib" ${Else} DetailPrint "vulkan-1.dll not found at expected location: $INSTDIR\resources\lib\vulkan-1.dll" From 8ba034233702685476464723c2dd5081429740af Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Tue, 23 Sep 2025 21:14:49 +0700 Subject: [PATCH 13/56] chore: update download folder to libDir --- scripts/download-lib.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/download-lib.mjs b/scripts/download-lib.mjs index ab73b8d79..6075a18d1 100644 --- a/scripts/download-lib.mjs +++ b/scripts/download-lib.mjs @@ -90,7 +90,7 @@ async function main() { // copy to tauri resources try { - copySync(vcSavePath, resourcesDir) + copySync(vcSavePath, libDir) } catch (err) { // Expect EEXIST error } From 1c1993fbd7dfe3d5562f303be15669e974b38be8 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 08:44:37 +0700 Subject: [PATCH 14/56] docs: update redirect page --- docs/_redirects | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/_redirects b/docs/_redirects index 748da60c5..d9a09250e 100644 --- a/docs/_redirects +++ b/docs/_redirects @@ -684,16 +684,16 @@ docs/guides/fine-tuning/what-models-can-be-fine-tuned/ /docs 302 /docs/server-installation/gcp /docs/desktop 302 /docs/server-installation/azure /docs/desktop 302 /about /docs 302 -/api-server /docs/api-server 302 +/api-server /docs/desktop/api-server 302 /cdn-cgi/l/email-protection 302 /docs/built-in/tensorrt-llm 302 /docs/desktop/beta /docs 302 -/docs/docs/data-folder /docs/data-folder 302 +/docs/docs/data-folder /docs/desktop/data-folder 302 /docs/docs/desktop/linux /docs/desktop/linux 302 -/docs/docs/troubleshooting /docs/troubleshooting 302 +/docs/docs/troubleshooting /docs/desktop/troubleshooting 302 /docs/local-engines/llama-cpp 302 /docs/models/model-parameters 302 -/mcp /docs/mcp 302 -/quickstart /docs/quickstart 302 -/server-examples/continue-dev /docs/server-examples/continue-dev 302 +/mcp /docs/desktop/mcp 302 +/quickstart /docs/desktop/quickstart 302 +/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 From 38ad8deae2954bb8dc47dcebd91d0d3175773284 Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Wed, 24 Sep 2025 07:19:32 +0530 Subject: [PATCH 15/56] feat: normalize LaTeX fragments in markdown rendering (#6488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: normalize LaTeX fragments in markdown rendering Added a preprocessing step that converts LaTeX delimiters `\[…\]` to `$$…$$` and `\(...\)` to `$…$` before rendering. The function skips code blocks, inline code, and HTML tags to avoid unintended transformations. This improves authoring experience by supporting common LaTeX syntax without requiring explicit `$` delimiters. * fix: correct inline LaTeX normalization replacement The replacement function for inline math (`\(...\)`) incorrectly accepted a fourth parameter (`post`) and appended it to the result, which could introduce stray characters or `undefined` into the rendered output. Updated the function to use only the captured prefix and inner content and removed the extraneous `${post}` interpolation, ensuring clean LaTeX conversion. * feat: optimize markdown rendering with LaTeX caching and memoized code blocks - Added cache to normalizeLatex to avoid reprocessing repeated content - Introduced CodeComponent with stable IDs and memoization to reduce re-renders - Replaced per-render code block ID mapping with hash-based IDs - Memoized copy handler and normalized markdown content - Simplified plugin/component setup with stable references - Added custom comparison for RenderMarkdown memoization to prevent unnecessary updates * refactor: memoize content only --------- Co-authored-by: Louis --- web-app/src/containers/RenderMarkdown.tsx | 336 +++++++++++++--------- 1 file changed, 208 insertions(+), 128 deletions(-) diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index 27bec0ea2..da702eff6 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -7,7 +7,7 @@ import remarkBreaks from 'remark-breaks' import rehypeKatex from 'rehype-katex' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import * as prismStyles from 'react-syntax-highlighter/dist/cjs/styles/prism' -import { memo, useState, useMemo, useRef, useEffect } from 'react' +import { memo, useState, useMemo, useCallback } from 'react' import { getReadableLanguageName } from '@/lib/utils' import { cn } from '@/lib/utils' import { useCodeblock } from '@/hooks/useCodeblock' @@ -25,6 +25,177 @@ interface MarkdownProps { isWrapping?: boolean } +// Cache for normalized LaTeX content +const latexCache = new Map() + +/** + * Optimized preprocessor: normalize LaTeX fragments into $ / $$. + * Uses caching to avoid reprocessing the same content. + */ +const normalizeLatex = (input: string): string => { + // Check cache first + if (latexCache.has(input)) { + return latexCache.get(input)! + } + + const segments = input.split(/(```[\s\S]*?```|`[^`]*`|<[^>]+>)/g) + + const result = segments + .map((segment) => { + if (!segment) return '' + + // Skip code blocks, inline code, html tags + if (/^```[\s\S]*```$/.test(segment)) return segment + if (/^`[^`]*`$/.test(segment)) return segment + if (/^<[^>]+>$/.test(segment)) return segment + + let s = segment + + // --- Display math: \[...\] surrounded by newlines + s = s.replace( + /(^|\n)\\\[\s*\n([\s\S]*?)\n\s*\\\](?=\n|$)/g, + (_, pre, inner) => `${pre}$$\n${inner.trim()}\n$$` + ) + + // --- Inline math: space \( ... \) + s = s.replace( + /(^|[^$\\])\\\((.+?)\\\)(?=[^$\\]|$)/g, + (_, pre, inner) => `${pre}$${inner.trim()}$` + ) + + return s + }) + .join('') + + // Cache the result (with size limit to prevent memory leaks) + if (latexCache.size > 100) { + const firstKey = latexCache.keys().next().value || '' + latexCache.delete(firstKey) + } + latexCache.set(input, result) + + return result +} + +// Memoized code component to prevent unnecessary re-renders +const CodeComponent = memo( + ({ + className, + children, + isUser, + codeBlockStyle, + showLineNumbers, + isWrapping, + onCopy, + copiedId, + ...props + }: any) => { + const { t } = useTranslation() + const match = /language-(\w+)/.exec(className || '') + const language = match ? match[1] : '' + const isInline = !match || !language + + const code = String(children).replace(/\n$/, '') + + // Generate a stable ID based on content hash instead of position + const codeId = useMemo(() => { + let hash = 0 + for (let i = 0; i < code.length; i++) { + const char = code.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32-bit integer + } + return `code-${Math.abs(hash)}-${language}` + }, [code, language]) + + const handleCopyClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation() + onCopy(code, codeId) + }, + [code, codeId, onCopy] + ) + + if (isInline || isUser) { + return {children} + } + + return ( +
+ +
+ + {getReadableLanguageName(language)} + + +
+ + index === 0 + ? part + : part.charAt(0).toUpperCase() + part.slice(1) + ) + .join('') as keyof typeof prismStyles + ] || prismStyles.oneLight + } + language={language} + showLineNumbers={showLineNumbers} + wrapLines={true} + lineProps={ + isWrapping + ? { + style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' }, + } + : {} + } + customStyle={{ + margin: 0, + padding: '8px', + borderRadius: '0 0 4px 4px', + overflow: 'auto', + border: 'none', + }} + PreTag="div" + CodeTag={'code'} + {...props} + > + {code} + +
+ ) + } +) + +CodeComponent.displayName = 'CodeComponent' + function RenderMarkdownComponent({ content, enableRawHtml, @@ -33,21 +204,13 @@ function RenderMarkdownComponent({ components, isWrapping, }: MarkdownProps) { - const { t } = useTranslation() const { codeBlockStyle, showLineNumbers } = useCodeblock() // State for tracking which code block has been copied const [copiedId, setCopiedId] = useState(null) - // Map to store unique IDs for code blocks based on content and position - const codeBlockIds = useRef(new Map()) - // Clear ID map when content changes - useEffect(() => { - codeBlockIds.current.clear() - }, [content]) - - // Function to handle copying code to clipboard - const handleCopy = (code: string, id: string) => { + // Memoized copy handler + const handleCopy = useCallback((code: string, id: string) => { navigator.clipboard.writeText(code) setCopiedId(id) @@ -55,134 +218,51 @@ function RenderMarkdownComponent({ setTimeout(() => { setCopiedId(null) }, 2000) - } + }, []) - // Default components for syntax highlighting and emoji rendering - const defaultComponents: Components = useMemo( - () => ({ - code: ({ className, children, ...props }) => { - const match = /language-(\w+)/.exec(className || '') - const language = match ? match[1] : '' - const isInline = !match || !language + // Memoize the normalized content to avoid reprocessing on every render + const normalizedContent = useMemo(() => normalizeLatex(content), [content]) - const code = String(children).replace(/\n$/, '') - - // Generate a unique ID based on content and language - const contentKey = `${code}-${language}` - let codeId = codeBlockIds.current.get(contentKey) - if (!codeId) { - codeId = `code-${codeBlockIds.current.size}` - codeBlockIds.current.set(contentKey, codeId) - } - - return !isInline && !isUser ? ( -
- -
- - {getReadableLanguageName(language)} - - -
- - index === 0 - ? part - : part.charAt(0).toUpperCase() + part.slice(1) - ) - .join('') as keyof typeof prismStyles - ] || prismStyles.oneLight - } - language={language} - showLineNumbers={showLineNumbers} - wrapLines={true} - // Temporary comment we try calculate main area width on __root - lineProps={ - isWrapping - ? { - style: { wordBreak: 'break-all', whiteSpace: 'pre-wrap' }, - } - : {} - } - customStyle={{ - margin: 0, - padding: '8px', - borderRadius: '0 0 4px 4px', - overflow: 'auto', - border: 'none', - }} - PreTag="div" - CodeTag={'code'} - {...props} - > - {String(children).replace(/\n$/, '')} - -
- ) : ( - {children} - ) - }, - }), - [codeBlockStyle, showLineNumbers, copiedId] - ) - - // Memoize the remarkPlugins to prevent unnecessary re-renders + // Stable remarkPlugins reference const remarkPlugins = useMemo(() => { - // Using a simpler configuration to avoid TypeScript errors const basePlugins = [remarkGfm, remarkMath, remarkEmoji] - // Add remark-breaks for user messages to handle single newlines as line breaks if (isUser) { basePlugins.push(remarkBreaks) } return basePlugins }, [isUser]) - // Memoize the rehypePlugins to prevent unnecessary re-renders + // Stable rehypePlugins reference const rehypePlugins = useMemo(() => { return enableRawHtml ? [rehypeKatex, rehypeRaw] : [rehypeKatex] }, [enableRawHtml]) - // Merge custom components with default components - const mergedComponents = useMemo( + // Memoized components with stable references + const markdownComponents: Components = useMemo( () => ({ - ...defaultComponents, + code: (props) => ( + + ), + // Add other optimized components if needed ...components, }), - [defaultComponents, components] + [ + isUser, + codeBlockStyle, + showLineNumbers, + isWrapping, + handleCopy, + copiedId, + components, + ] ) // Render the markdown content @@ -197,14 +277,14 @@ function RenderMarkdownComponent({ - {content} + {normalizedContent}
) } - -// Use a simple memo without custom comparison to allow re-renders when content changes -// This is important for streaming content to render incrementally -export const RenderMarkdown = memo(RenderMarkdownComponent) +export const RenderMarkdown = memo( + RenderMarkdownComponent, + (prevProps, nextProps) => prevProps.content === nextProps.content +) From 8102ca24e5abf5f7f61f01cc69e3997aaa7ffefc Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 08:56:06 +0700 Subject: [PATCH 16/56] chore: wipe out _redirects page --- docs/_redirects | 699 ------------------------------------------------ 1 file changed, 699 deletions(-) diff --git a/docs/_redirects b/docs/_redirects index d9a09250e..e69de29bb 100644 --- a/docs/_redirects +++ b/docs/_redirects @@ -1,699 +0,0 @@ -/team /about/team 302 -/about/teams /about/team 302 -/about/faq /docs 302 -/about/acknowledgements /docs 302 -/about/community /about 302 -/guides /docs 302 -/docs/troubleshooting/failed-to-fetch /docs/troubleshooting 302 -/guides/troubleshooting/gpu-not-used /docs/troubleshooting#troubleshooting-nvidia-gpu 302 -/guides/troubleshooting /docs/troubleshooting 302 -/docs/troubleshooting/stuck-on-broken-build /docs/troubleshooting 302 -/docs/troubleshooting/somethings-amiss /docs/troubleshooting 302 -/docs/troubleshooting/how-to-get-error-logs /docs/troubleshooting 302 -/docs/troubleshooting/permission-denied /docs/troubleshooting 302 -/docs/troubleshooting/unexpected-token /docs/troubleshooting 302 -/docs/troubleshooting/undefined-issue /docs/troubleshooting 302 -/getting-started/troubleshooting /docs/troubleshooting 302 -/docs/troubleshooting/gpu-not-used /docs/troubleshooting 302 -/guides/integrations/openrouter /docs/remote-models/openrouter 302 -/guides/integrations/continue /integrations/coding/continue-dev 302 -/docs/extension-capabilities /docs/extensions 302 -/guides/using-extensions /docs/extensions 302 -/docs/extension-guides /docs/extensions 302 -/features/extensions /docs/extensions 302 -/integrations/tensorrt /docs/built-in/tensorrt-llm 302 -/guides/using-models/integrate-with-remote-server /docs/remote-inference/generic-openai 302 -/guides/using-models/customize-engine-settings /docs/built-in/llama-cpp 302 -/developers/plugins/azure-openai /docs/remote-models/openai 302 -/docs/api-reference/assistants /api-reference#tag/assistants 302 -/docs/api-reference/models/list /api-reference#tag/models 302 -/docs/api-reference/threads /api-reference#tag/chat 302 -/docs/api-reference/messages /api-reference#tag/messages 302 -/docs/api-reference/models /api-reference#tag/models 302 -/chat /docs/threads 302 -/guides/chatting/manage-history /docs/threads/ 302 -/guides/chatting/start-thread /docs/threads/ 302 -/guides/using-server /docs/local-api/ 302 -/guides/using-server/server /docs/local-api#step-2-srt-and-use-the-built-in-api-server 302 -/docs/get-started /docs 302 -/guides/how-jan-works /about/how-we-work 302 -/acknowledgements /about/acknowledgements 302 -/community /about/community 302 -/faq /about/faq 302 -/how-we-work /about/how-we-work 302 -/wall-of-love /about/wall-of-love 302 -/guides/troubleshooting/failed-to-fetch /docs/troubleshooting 302 -/docs/troubleshooting/gpu-not-used /docs/troubleshooting 302 -/docs/troubleshooting/failed-to-fetch /docs/troubleshooting 302 -/guides/ /docs 302 -/guides/quickstart/ /docs/quickstart 302 -/guides/models/ /docs/models 302 -/guides/threads/ /docs/threads 302 -/guides/local-api/ /docs/local-api 302 -/guides/advanced/ /docs/settings 302 -/guides/engines/llamacpp/ /docs/built-in/llama-cpp 302 -/guides/engines/tensorrt-llm/ /docs/built-in/tensorrt-llm 302 -/guides/engines/lmstudio/ /docs/local-models/lmstudio 302 -/guides/engines/ollama/ /docs/built-in/llama-cpp 302 -/guides/engines/groq/ /docs/remote-models/groq 302 -/guides/engines/mistral/ /docs/remote-models/mistralai 302 -/guides/engines/openai/ /docs/remote-models/openai 302 -/guides/engines/remote-server/ /docs/remote-inference/generic-openai 302 -/extensions/ /docs/extensions 302 -/integrations/discord/ /integrations/messaging/llmcord 302 -/discord https://discord.gg/FTk2MvZwJH 301 -/integrations/interpreter/ /integrations/function-calling/interpreter 302 -/integrations/raycast/ /integrations/workflow-automation/raycast 302 -/docs/integrations/raycast /integrations/workflow-automation/raycast 302 -/docs/integrations /integrations 302 -/docs/engineering/files/ /docs 302 -/integrations/openrouter/ /docs/remote-models/openrouter 302 -/integrations/continue/ /integrations/coding/continue-dev 302 -/troubleshooting/ /docs/troubleshooting 302 -/changelog/changelog-v0.4.9/ /changelog 302 -/changelog/changelog-v0.4.8/ /changelog 302 -/changelog/changelog-v0.4.7/ /changelog 302 -/changelog/changelog-v0.4.6/ /changelog 302 -/changelog/changelog-v0.4.5/ /changelog 302 -/changelog/changelog-v0.4.4/ /changelog 302 -/changelog/changelog-v0.4.3/ /changelog 302 -/changelog/changelog-v0.4.2/ /changelog 302 -/changelog/changelog-v0.4.1/ /changelog 302 -/changelog/changelog-v0.4.0/ /changelog 302 -/changelog/changelog-v0.3.3/ /changelog 302 -/changelog/changelog-v0.3.2/ /changelog 302 -/changelog/changelog-v0.3.1/ /changelog 302 -/changelog/changelog-v0.3.0/ /changelog 302 -/changelog/changelog-v0.2.3/ /changelog 302 -/changelog/changelog-v0.2.2/ /changelog 302 -/changelog/changelog-v0.2.1/ /changelog 302 -/changelog/changelog-v0.2.0/ /changelog 302 -/team/ /about/team 302 -/team/contributor-program/ /about/team 302 -/team/join-us/ /about/team 302 -/how-we-work/ /about/how-we-work 302 -/how-we-work/strategy/ /about/how-we-work/strategy 302 -/how-we-work/project-management/ /about/how-we-work/project-management 302 -/engineering/ /about/how-we-work/engineering 302 -/engineering/ci-cd/ /about/how-we-work/engineering/ci-cd 302 -/engineering/qa/ /about/how-we-work/engineering/qa 302 -/how-we-work/product-design/ /about 302 -/about/how-we-work/product-design /about 302 -/how-we-work/analytics/ /about/how-we-work/analytics 302 -/how-we-work/website-docs/ /about/how-we-work/website-docs 302 -/blog/postmortems/january-10-2024-bitdefender-false-positive-flag/ /post/bitdefender 302 -/guides/error-codes/something-amiss/ /docs/troubleshooting#somethings-amiss 302 -/guides/error-codes/how-to-get-error-logs/ /docs/troubleshooting#how-to-get-error-logs 302 -/guides/chatting/ /docs/threads 302 -/guides/integration/openinterpreter/ /integrations/function-calling/interpreter 302 -/developer/build-assistant/ /docs/assistants 302 -/guides/integrations/ /integrations 302 -/specs/hub/ /docs 302 -/install/windows/ /docs/desktop/windows 302 -/install/linux/ /docs/desktop/linux 302 -/install/nightly/ /docs/desktop/windows 302 -/docs/engineering/fine-tuning/ /docs 302 -/developer/assistant/ /docs/assistants 302 -/guides/common-error/broken-build/ /docs/troubleshooting#broken-build 302 -/guides/using-server/using-server/ /docs/local-api 302 -/guides/integrations/azure-openai-service/ /docs/remote-models/openai 302 -/specs/messages/ /docs/threads 302 -/docs/engineering/models/ /docs/models 302 -/docs/specs/assistants/ /docs/assistants 302 -/docs/engineering/chats/ /docs/threads 302 -/guides/using-extensions/extension-settings/ /docs/extensions 302 -/guides/models/customize-engine/ /docs/models 302 -/guides/integration/mistral/ /docs/remote-models/mistralai 302 -/guides/common-error/ /docs/troubleshooting 302 -/guides/integrations/ollama/ /docs/local-models/ollama 302 -/server-suite/ /api-reference 302 -/guides/integrations/lmstudio/ /docs/local-models/lmstudio 302 -/guides/integrations/mistral-ai/ /docs/remote-models/mistralai 302 -/guides/start-server/ /docs/local-api 302 -/guides/changelog/ /changelog 302 -/guides/models-list/ /docs/models 302 -/guides/thread/ /docs/threads 302 -/docs/engineering/messages/ /docs/threads 302 -/guides/faqs/ /about/faq 302 -/docs/integrations/openrouter/ /docs/remote-models/openrouter 302 -/guides/windows /docs/desktop/windows 302 -/docs/integrations/ollama/ /docs/local-models/ollama 302 -/api/overview/ /api-reference 302 -/docs/extension-guides/ /docs/extensions 302 -/specs/settings/ /docs 302 -/docs/UI/ /docs 302 -/guides/using-models/import-models-using-absolute-filepath/ /docs/models 302 -/install/docker/ /docs/desktop 302 -/install/ /docs/desktop 302 -/install/from-source/ /docs/desktop 302 -/docs/installation/server /docs/desktop 302 -/v1/models /docs/models 302 -/guides/advanced-settings/ /docs/settings 302 -/guides/using-models/install-from-hub/ /docs/models/manage-models#download-from-jan-hub 302 -/guides/using-models/import-manually/ /docs/models 302 -/docs/team/contributor-program/ /about/team 302 -/docs/modules/models /docs/models 302 -/getting-started/install/linux /docs/desktop/linux 302 -/guides/chatting/start-thread/ /docs/threads 302 -/api/files/ /docs 302 -/specs/threads/ /docs/threads 302 -/about/brand-assets /about 302 -/guides/chatting/upload-images/ /docs/threads 302 -/guides/using-models/customize-models/ /docs/models 302 -/docs/modules/models/ /docs/models 302 -/getting-started/install/linux/ /docs/desktop/linux 302 -/specs/chats/ /docs/threads 302 -/specs/engine/ /docs 302 -/specs/data-structures /docs 302 -/docs/extension-capabilities/ /docs/extensions 302 -/docs/get-started/use-local-server/ /docs/local-api 302 -/guides/how-jan-works/ /about/how-we-work 302 -/guides/install/cloud-native /docs/desktop 302 -/guides/windows/ /docs/desktop/windows 302 -/specs/ /docs 302 -/docs/get-started/build-extension/ /docs/extensions 302 -/specs/files/ /docs 302 -/guides/using-models/package-models/ /docs/models 302 -/install/overview/ /docs/desktop/windows 302 -/docs/get-started/extension-anatomy/ /docs/extensions 302 -/docs/get-started/ /docs 302 -/guides/mac/ /docs/desktop/mac 302 -/intro/ /about 302 -/specs/fine-tuning/ /docs 302 -/guides/server/ /docs/desktop 302 -/specs/file-based/ /docs 302 -/docs/extension-guides/monitoring/ /docs/extensions 302 -/api/ /api-reference 302 -/getting-started/build-an-app /docs/quickstart 302 -/features/ai-models/ /docs/models 302 -/reference/store/ /api-reference 302 -/tutorials/build-chat-app /docs/quickstart 302 -/features/acceleration /docs/built-in/llama-cpp 302 -/getting-started/install/mac /docs/desktop/mac 302 -docs/guides/fine-tuning/what-models-can-be-fine-tuned /docs 302 -/docs/specs/threads /docs/threads 302 -/docs/api-reference/fine-tuning /api-reference 302 -/docs/guides/speech-to-text/prompting /docs/quickstart 302 -/docs/guides/legacy-fine-tuning/analyzing-your-fine-tuned-model /docs 302 -/getting-started/install/windows /docs/desktop/windows 302 -/docs/modules/assistants /docs/assistants 302 -/docs/modules/chats /docs/threads 302 -/docs/specs/chats /docs/threads 302 -/docs/modules/files /docs 302 -/tutorials/build-rag-app /docs/tools/retrieval 302 -/docs/models/model-endpoint-compatibility /docs/models 302 -/docs/guides/legacy-fine-tuning/creating-training-data /docs 302 -/docs/specs/models /docs/models 302 -/docs/guides/safety-best-practices/end-user-ids /docs/quickstart 302 -/docs/modules/assistants/ /docs/assistants 302 -/docs/models/overview /docs/models 302 -/docs/api-reference/files /api-reference 302 -/docs/models/tts /docs/models 302 -/docs/guides/fine-tuning /docs 302 -/docs/specs/files /docs 302 -/docs/modules/threads /docs/threads 302 -/guides/linux /docs/desktop/linux 302 -/developer/build-engine/engine-anatomy/ /docs 302 -/developer/engine/ /docs 302 -/docs/product/system-monitor/ /docs 302 -/docs/product/settings/ /docs 302 -/developer/build-assistant/your-first-assistant/ /docs 302 -/engineering/research/ /docs 302 -/guides/troubleshooting/gpu-not-used/ /docs/troubleshooting#troubleshooting-nvidia-gpu 302 -/troubleshooting/gpu-not-used/ /docs/troubleshooting#troubleshooting-nvidia-gpu 302 -/docs/integrations/langchain/ /integrations 302 -/onboarding/ /docs/quickstart 302 -/cortex/docs https://cortex.so/ 301 -/installation/hardware/ /docs/desktop/windows 302 -/docs/features/load-unload /docs 302 -/guides/chatting/upload-docs/ /docs/threads 302 -/developer/build-extension/package-your-assistant/ /docs 302 -/blog/hello-world /blog 302 -/docs/get-started/build-on-mobile/ /docs/quickstart 302 -/ai/anything-v4 /docs 302 -/nitro /docs 302 -/tokenizer /docs 302 -/hardware/examples/3090x1-@dan-jan /docs 302 -/guides/concepts/ /about 302 -/platform/ /docs 302 -/hardware/examples/AMAZON-LINK-HERE /docs 302 -/guides/threads/?productId=openai&prompt=What /docs 302 -/guides/threads/?productId=openjourney&prompt=realistic%20portrait%20of%20an%20gray%20dog,%20bright%20eyes,%20radiant%20and%20ethereal%20intricately%20detailed%20photography,%20cinematic%20lighting,%2050mm%20lens%20with%20bokeh /docs 302 -/guides/threads/?productId=openjourney&prompt=old,%20female%20robot,%20metal,%20rust,%20wisible%20wires,%20destroyed,%20sad,%20dark,%20dirty,%20looking%20at%20viewer,%20portrait,%20photography,%20detailed%20skin,%20realistic,%20photo-realistic,%208k,%20highly%20detailed,%20full%20length%20frame,%20High%20detail%20RAW%20color%20art,%20piercing,%20diffused%20soft%20lighting,%20shallow%20depth%20of%20field,%20sharp%20focus,%20hyperrealism,%20cinematic%20lighting /docs 302 -/guides/threads/?productId=openjourney&prompt=a%20young%20caucasian%20man%20holding%20his%20chin.pablo%20picasso%20style,%20acrylic%20painting,%20trending%20on%20pixiv%20fanbox,%20palette%20knife%20and%20brush.%20strokes /docs 302 -/guides/threads/?productId=airoboros&prompt=Let%27s%20role%20play.%20You%20are%20a%20robot%20in%20a%20post-apocalyptic%20world. /docs 302 -/chat?productId=pirsus-epic-realism /docs 302 -/chat?productId=ether-blu-mix /docs 302 -/chat?productId=deliberate /docs 302 -/chat?productId=wizard_vicuna /docs 302 -/chat?productId=disneypixar /docs 302 -/chat?productId=meina-mix /docs 302 -/chat?productId=anything-v4 /docs 302 -/chat?productId=airoboros /docs 302 -/chat?productId=ghost-mix /docs 302 -/ai/toonyou /docs 302 -/chat?productId=xrica-mix /docs 302 -/ai/openai /docs 302 -/chat?productId=been-you /docs 302 -/chat?productId=toonyou /docs 302 -/handbook/product-and-community/ /about/community 302 -/handbook/contributing-to-jan/how-to-get-involved-and-faq/ /about 302 -/handbook/engineering-exellence/one-the-tools-what-we-use-and-why/ /about 302 -/handbook/from-spaghetti-flinging-to-strategy/how-we-gtm/ /about/how-we-work/strategy 302 -/handbook/product-and-community/our-okrs/ /about 302 -/products-and-innovations/philosophy-behind-product-development/ /about 302 -/handbook/core-contributors/ /about/team 302 -/handbook/contributing-to-jan/feedback-channels/ /about/how-we-work 302 -/handbook/meet-jan/ /docs 302 -/handbook/engineering-exellence/ /about 302 -/blog/tags/hello/ /blog 302 -/about/community/events/nvidia-llm-day-nov-23/ /about 302 -/guides/gpus-and-vram /docs 302 -/careers/ /about/team 302 -/handbook/engineering/ /about/team 302 -/handbook/products-and-innovations/ /about 302 -/handbook/contributing-to-jan/ /about 302 -/handbook/meet-jan/vision-and-mission/ /about 302 -/handbook/products-and-innovations/roadmap-present-and-future-directions/ /about 302 -/handbook/what-we-do/ /about/team 302 -/handbook/onboarding/ /docs 302 -/handbook/products-and-innovations/overview-of-jan-framework-and-its-applications/ /docs 302 -/handbook/product/ /docs 302 -/running /docs 302 -/running?model=Open%20Journey%20SD /docs 302 -/ai/been-you /about 302 -/tokenizer?view=bpe /docs 302 -/docs/engineering/ /docs 302 -/developer/install-and-prerequisites#system-requirements /docs/desktop/windows 302 -/guides/quickstart /docs/quickstart 302 -/guides/models /docs/models 302 -/guides/threads /docs/threads 302 -/guides/local-api /docs/local-api 302 -/guides/advanced /docs/settings 302 -/guides/engines/llamacpp /docs/built-in/llama-cpp 302 -/guides/engines/tensorrt-llm /docs/built-in/tensorrt-llm 302 -/guides/engines/lmstudio /docs/local-models/lmstudio 302 -/guides/engines/ollama /docs/local-models/ollama 302 -/guides/engines/groq /docs/remote-models/groq 302 -/guides/engines/mistral /docs/remote-models/mistralai 302 -/guides/engines/openai /docs/remote-models/openai 302 -/guides/engines/remote-server /docs/remote-inference/generic-openai 302 -/extensions /docs/extensions 302 -/integrations/discord /integrations/messaging/llmcord 302 -/docs/integrations/discord /integrations/messaging/llmcord 302 -/integrations/interpreter /integrations/function-calling/interpreter 302 -/integrations/raycast /integrations/workflow-automation/raycast 302 -/integrations/openrouter /docs/remote-models/openrouter 302 -/integrations/continue /integrations/coding/continue-dev 302 -/troubleshooting /docs/troubleshooting 302 -/changelog/changelog-v0.4.9 /changelog 302 -/changelog/changelog-v0.4.8 /changelog 302 -/changelog/changelog-v0.4.7 /changelog 302 -/changelog/changelog-v0.4.6 /changelog 302 -/changelog/changelog-v0.4.5 /changelog 302 -/changelog/changelog-v0.4.4 /changelog 302 -/changelog/changelog-v0.4.3 /changelog 302 -/changelog/changelog-v0.4.2 /changelog 302 -/changelog/changelog-v0.4.1 /changelog 302 -/changelog/changelog-v0.4.0 /changelog 302 -/changelog/changelog-v0.3.3 /changelog 302 -/changelog/changelog-v0.3.2 /changelog 302 -/changelog/changelog-v0.3.1 /changelog 302 -/changelog/changelog-v0.3.0 /changelog 302 -/changelog/changelog-v0.2.3 /changelog 302 -/changelog/changelog-v0.2.2 /changelog 302 -/changelog/changelog-v0.2.1 /changelog 302 -/changelog/changelog-v0.2.0 /changelog 302 -/guides/troubleshooting/ /docs/troubleshooting 302 -/docs/troubleshooting/failed-to-fetch/ /docs/troubleshooting 302 -/docs/troubleshooting/stuck-on-broken-build/ /docs/troubleshooting 302 -/docs/troubleshooting/somethings-amiss/ /docs/troubleshooting 302 -/docs/troubleshooting/how-to-get-error-logs/ /docs/troubleshooting 302 -/docs/troubleshooting/permission-denied/ /docs/troubleshooting 302 -/docs/troubleshooting/unexpected-token/ /docs/troubleshooting 302 -/docs/troubleshooting/undefined-issue/ /docs/troubleshooting 302 -/getting-started/troubleshooting/ /docs/troubleshooting 302 -/docs/troubleshooting/gpu-not-used/ /docs/troubleshooting#troubleshooting-nvidia-gpu 302 -/guides/integrations/openrouter/ /docs/remote-models/openrouter 302 -/guides/integrations/continue/ /integrations/coding/continue-dev 302 -/guides/using-extensions/ /docs/extensions 302 -/features/extensions/ /docs/extensions 302 -/integrations/tensorrt /docs/built-in/tensorrt-llm 302 -/integrations/tensorrt/ /docs/built-in/tensorrt-llm 302 -/guides/using-models/integrate-with-remote-server/ /docs/remote-inference/generic-openai 302 -/guides/using-models/customize-engine-settings/ /docs/built-in/llama-cpp 302 -/developers/plugins/azure-openai/ /docs/remote-models/openai 302 -/docs/api-reference/assistants/ /api-reference#tag/assistants 302 -/docs/api-reference/models/list/ /api-reference#tag/models 302 -/docs/api-reference/threads/ /api-reference#tag/chat 302 -/docs/api-reference/messages/ /api-reference#tag/messages 302 -/docs/api-reference/models/ /api-reference#tag/models 302 -/chat/ /docs/threads 302 -/guides/chatting/manage-history/ /docs/threads/ 302 -/guides/using-server/ /docs/local-api 302 -/guides/using-server/server /docs/local-api 302 -/guides/server /docs/desktop 302 -/acknowledgements/ /about/acknowledgements 302 -/community/ /about/community 302 -/faq/ /about/faq 302 -/wall-of-love/ /about/wall-of-love 302 -/guides/troubleshooting/failed-to-fetch/ /docs/troubleshooting 302 -/docs/troubleshooting/gpu-not-used/ /docs/troubleshooting#troubleshooting-nvidia-gpu 302 -/docs/troubleshooting/failed-to-fetch/ /docs/troubleshooting 302 -/team/contributor-program /about/team 302 -/team/join-us /about/team 302 -/how-we-work/strategy /about/how-we-work/strategy 302 -/how-we-work/strategy/ /about/how-we-work/strategy 302 -/how-we-work/project-management /about/how-we-work/project-management 302 -/engineering /about/how-we-work/engineering 302 -/engineering/ci-cd /about/how-we-work/engineering/ci-cd 302 -/engineering/qa /about/how-we-work/engineering/qa 302 -/how-we-work/product-design /about 302 -/how-we-work/analytics /about/how-we-work/analytics 302 -/how-we-work/website-docs /about/how-we-work/website-docs 302 -/blog/postmortems/january-10-2024-bitdefender-false-positive-flag /post/bitdefender 302 -/guides/error-codes/something-amiss /docs/troubleshooting#somethings-amiss 302 -/guides/error-codes/how-to-get-error-logs /docs/troubleshooting#how-to-get-error-logs 302 -/guides/chatting /docs/threads 302 -/guides/integration/openinterpreter /integrations/function-calling/interpreter 302 -/developer/build-assistant /docs/assistants 302 -/guides/integrations /integrations 302 -/specs/hub /docs 302 -/install/windows /docs/desktop/windows 302 -/install/linux /docs/desktop/linux 302 -/install/nightly /docs/desktop/windows 302 -/docs/engineering/fine-tuning /docs 302 -/developer/assistant /docs/assistants 302 -/guides/common-error/broken-build /docs/troubleshooting#broken-build 302 -/guides/using-server/using-server /docs/local-api 302 -/guides/integrations/azure-openai-service /docs/remote-models/openai 302 -/specs/messages /docs/threads 302 -/docs/engineering/models /docs/models 302 -/docs/specs/assistants /docs/assistants 302 -/docs/engineering/chats /docs/threads 302 -/guides/using-extensions/extension-settings /docs/extensions 302 -/guides/models/customize-engine /docs/models 302 -/guides/integration/mistral /docs/remote-models/mistralai 302 -/guides/common-error /docs/troubleshooting 302 -/guides/integrations/ollama /docs/local-models/ollama 302 -/server-suite /api-reference 302 -/guides/integrations/lmstudio /docs/local-models/lmstudio 302 -/guides/integrations/mistral-ai /docs/remote-models/mistralai 302 -/guides/start-server /docs/local-api 302 -/guides/changelog /changelog 302 -/guides/models-list /docs/models 302 -/guides/thread /docs/threads 302 -/docs/engineering/messages /docs/threads 302 -/guides/faqs /about/faq 302 -/docs/integrations/openrouter /docs/remote-models/openrouter 302 -/docs/integrations/ollama/ /docs/local-models/ollama 302 -/api/overview /api-reference 302 -/docs/extension-guides /docs/extensions 302 -/specs/settings /docs 302 -/docs/UI /docs 302 -/guides/using-models/import-models-using-absolute-filepath /docs/models 302 -/install/docker /docs/desktop 302 -/v1/models/ /docs/models 302 -/guides/using-models/import-manually /docs/models 302 -/docs/team/contributor-program /about/team 302 -/guides/chatting/start-thread /docs/threads 302 -/api/files /docs 302 -/specs/threads /docs/threads 302 -/about/brand-assets/ /about 302 -/guides/chatting/upload-images /docs/threads 302 -/guides/using-models/customize-models /docs/models 302 -/specs/chats /docs/threads 302 -/specs/engine /docs 302 -/specs/data-structures/ /docs 302 -/docs/extension-capabilities /docs/extensions 302 -/docs/get-started/use-local-server /docs/local-api 302 -/guides/install/cloud-native/ /docs/desktop 302 -/guides/install/ /docs/desktop 302 -/docs/installation/desktop /docs/desktop 302 -/specs /docs 302 -/docs/get-started/build-extension /docs/extensions 302 -/specs/files /docs 302 -/guides/using-models/package-models /docs/models 302 -/guides/using-models/ /docs/models 302 -/install/overview /docs/desktop/windows 302 -/developer/prereq/ /docs 302 -/docs/get-started/extension-anatomy /docs/extensions 302 -/guides/mac /docs/desktop/mac 302 -/intro /about 302 -/specs/fine-tuning /docs 302 -/specs/file-based /docs 302 -/docs/extension-guides/monitoring /docs/extensions 302 -/api /api-reference 302 -/getting-started/build-an-app/ /docs/quickstart 302 -/features/ai-models /docs/models 302 -/reference/store /api-reference 302 -/tutorials/build-chat-app/ /docs/quickstart 302 -/features/acceleration/ /docs/built-in/llama-cpp 302 -/getting-started/install/mac/ /docs/desktop/mac 302 -docs/guides/fine-tuning/what-models-can-be-fine-tuned/ /docs 302 -/docs/specs/threads/ /docs/threads 302 -/docs/api-reference/fine-tuning/ /api-reference 302 -/docs/guides/speech-to-text/prompting/ /docs/quickstart 302 -/docs/guides/legacy-fine-tuning/analyzing-your-fine-tuned-model/ /docs 302 -/getting-started/install/windows/ /docs/desktop/windows 302 -/docs/modules/chats/ /docs/threads 302 -/docs/specs/chats/ /docs/threads 302 -/docs/modules/files/ /docs 302 -/tutorials/build-rag-app/ /docs/tools/retrieval 302 -/docs/models/model-endpoint-compatibility/ /docs/models 302 -/docs/guides/legacy-fine-tuning/creating-training-data/ /docs 302 -/docs/specs/models/ /docs/models 302 -/docs/guides/safety-best-practices/end-user-ids/ /docs/quickstart 302 -/docs/models/overview/ /docs/models 302 -/docs/api-reference/files/ /api-reference 302 -/docs/models/tts/ /docs/models 302 -/docs/guides/fine-tuning/ /docs 302 -/docs/specs/files/ /docs 302 -/docs/modules/threads/ /docs/threads 302 -/guides/linux/ /docs/desktop/linux 302 -/developer/build-engine/engine-anatomy /docs 302 -/developer/engine /docs 302 -/docs/product/system-monitor /docs 302 -/docs/product/settings /docs 302 -/developer/build-assistant/your-first-assistant /docs 302 -/engineering/research /docs 302 -/docs/integrations/langchain /integrations 302 -/onboarding /docs/quickstart 302 -/installation/hardware /docs/desktop/windows 302 -/docs/features/load-unload/ /docs 302 -/guides/chatting/upload-docs /docs/threads 302 -/developer/build-extension/package-your-assistant /docs 302 -/blog/hello-world/ /blog 302 -/docs/get-started/build-on-mobile /docs/quickstart 302 -/ai/anything-v4/ /docs 302 -/nitro/ /docs 302 -/tokenizer/ /docs 302 -/hardware/examples/3090x1-@dan-jan/ /docs 302 -/guides/concepts /about 302 -/platform /docs 302 -/hardware/examples/AMAZON-LINK-HERE/ /docs 302 -/guides/threads/?productId=openai&prompt=What/ /docs 302 -/guides/threads/?productId=openjourney&prompt=realistic%20portrait%20of%20an%20gray%20dog,%20bright%20eyes,%20radiant%20and%20ethereal%20intricately%20detailed%20photography,%20cinematic%20lighting,%2050mm%20lens%20with%20bokeh/ /docs 302 -/guides/threads/?productId=openjourney&prompt=old,%20female%20robot,%20metal,%20rust,%20wisible%20wires,%20destroyed,%20sad,%20dark,%20dirty,%20looking%20at%20viewer,%20portrait,%20photography,%20detailed%20skin,%20realistic,%20photo-realistic,%208k,%20highly%20detailed,%20full%20length%20frame,%20High%20detail%20RAW%20color%20art,%20piercing,%20diffused%20soft%20lighting,%20shallow%20depth%20of%20field,%20sharp%20focus,%20hyperrealism,%20cinematic%20lighting/ /docs 302 -/guides/threads/?productId=openjourney&prompt=a%20young%20caucasian%20man%20holding%20his%20chin.pablo%20picasso%20style,%20acrylic%20painting,%20trending%20on%20pixiv%20fanbox,%20palette%20knife%20and%20brush.%20strokes/ /docs 302 -/guides/threads/?productId=airoboros&prompt=Let%27s%20role%20play.%20You%20are%20a%20robot%20in%20a%20post-apocalyptic%20world./ /docs 302 -/chat?productId=pirsus-epic-realism/ /docs 302 -/chat?productId=ether-blu-mix/ /docs 302 -/chat?productId=deliberate/ /docs 302 -/chat?productId=wizard_vicuna/ /docs 302 -/chat?productId=disneypixar/ /docs 302 -/chat?productId=meina-mix/ /docs 302 -/chat?productId=anything-v4/ /docs 302 -/chat?productId=airoboros/ /docs 302 -/chat?productId=ghost-mix/ /docs 302 -/ai/toonyou/ /docs 302 -/chat?productId=xrica-mix/ /docs 302 -/ai/openai/ /docs 302 -/chat?productId=been-you/ /docs 302 -/chat?productId=toonyou/ /docs 302 -/handbook/product-and-community /about/community 302 -/handbook/contributing-to-jan/how-to-get-involved-and-faq /about 302 -/handbook/engineering-exellence/one-the-tools-what-we-use-and-why /about 302 -/handbook/from-spaghetti-flinging-to-strategy/how-we-gtm /about/how-we-work/strategy 302 -/handbook/product-and-community/our-okrs /about 302 -/products-and-innovations/philosophy-behind-product-development /about 302 -/handbook/core-contributors /about/team 302 -/handbook/contributing-to-jan/feedback-channels /about/how-we-work 302 -/handbook/meet-jan /docs 302 -/handbook/engineering-exellence /about 302 -/blog/tags/hello /blog 302 -/about/community/events/nvidia-llm-day-nov-23 /about 302 -/guides/gpus-and-vram/ /docs 302 -/careers /about/team 302 -/handbook/engineering /about/team 302 -/handbook/products-and-innovations /about 302 -/handbook/contributing-to-jan /about 302 -/handbook/meet-jan/vision-and-mission /about 302 -/handbook/products-and-innovations/roadmap-present-and-future-directions /about 302 -/handbook/what-we-do /about/team 302 -/handbook/onboarding /docs 302 -/handbook/products-and-innovations/overview-of-jan-framework-and-its-applications /docs 302 -/handbook/product /docs 302 -/running/ /docs 302 -/running?model=Open%20Journey%20SD/ /docs 302 -/ai/been-you/ /about 302 -/tokenizer?view=bpe/ /docs 302 -/docs/engineering /docs 302 -/developer /docs 302 -/developer/ /docs 302 -/developer/architecture /docs/architecture 302 -/developer/architecture/ /docs/architecture 302 -/developer/file-based /docs 302 -/developer/file-based/ /docs 302 -/developer/framework /docs 302 -/developer/framework/ /docs 302 -/developer/framework/engineering /docs 302 -/developer/framework/engineering/ /docs 302 -/developer/framework/engineering/assistants /docs/assistants 302 -/developer/framework/engineering/assistants/ /docs/assistants 302 -/developer/framework/engineering/chats /docs/threads 302 -/developer/framework/engineering/chats/ /docs/threads 302 -/developer/framework/engineering/engine /docs 302 -/developer/framework/engineering/engine/ /docs 302 -/developer/framework/engineering/files /docs 302 -/developer/framework/engineering/files/ /docs 302 -/developer/framework/engineering/fine-tuning /docs 302 -/developer/framework/engineering/fine-tuning/ /docs 302 -/developer/framework/engineering/messages /docs/threads 302 -/developer/framework/engineering/messages/ /docs/threads 302 -/developer/framework/engineering/models /docs/models 302 -/developer/framework/engineering/models/ /docs/models 302 -/developer/framework/engineering/prompts /docs 302 -/developer/framework/engineering/prompts/ /docs 302 -/developer/framework/engineering/threads /docs/threads 302 -/developer/framework/engineering/threads/ /docs/threads 302 -/developer/framework/product /docs 302 -/developer/framework/product/ /docs 302 -/developer/framework/product/chat /docs/threads 302 -/developer/framework/product/chat/ /docs/threads 302 -/developer/framework/product/hub /docs 302 -/developer/framework/product/hub/ /docs 302 -/developer/framework/product/jan /about 302 -/developer/framework/product/jan/ /about 302 -/developer/framework/product/settings /docs/settings 302 -/developer/framework/product/settings/ /docs/settings 302 -/developer/framework/product/system-monitor /docs 302 -/developer/framework/product/system-monitor/ /docs 302 -/developer/user-interface /docs 302 -/developer/user-interface/ /docs 302 -/docs/desktop /docs/desktop/windows 302 -/docs/desktop/ /docs/desktop/windows 302 -/docs/inferences/groq /docs/remote-models/groq 302 -/docs/inferences/groq/ /docs/remote-models/groq 302 -/docs/inferences/llamacpp /docs/built-in/llama-cpp 302 -/docs/inferences/llamacpp/ /docs/built-in/llama-cpp 302 -/docs/inferences/lmstudio /docs/local-models/lmstudio 302 -/docs/inferences/lmstudio/ /docs/local-models/lmstudio 302 -/docs/inferences/mistralai /docs/remote-models/mistralai 302 -/docs/inferences/mistralai/ /docs/remote-models/mistralai 302 -/docs/inferences/ollama /docs/local-models/ollama 302 -/docs/inferences/ollama/ /docs/local-models/ollama 302 -/docs/inferences/openai /docs/remote-models/openai 302 -/docs/inferences/openai/ /docs/remote-models/openai 302 -/docs/inferences/remote-server-integration /docs/remote-inference/generic-openai 302 -/docs/inferences/remote-server-integration/ /docs/remote-inference/generic-openai 302 -/docs/inferences/tensorrtllm /docs/built-in/tensorrt-llm 302 -/docs/inferences/tensorrtllm/ /docs/built-in/tensorrt-llm 302 -/docs/integrations/router /docs/remote-models/openrouter 302 -/docs/integrations/router/ /docs/remote-models/openrouter 302 -/docs/server /docs/local-api 302 -/docs/server/ /docs/local-api 302 -/features/ /docs 302 -/features /docs 302 -/features/local/ /docs/local-api 302 -/features/local /docs/local-api 302 -/guides/providers/tensorrt-llm /docs/built-in/tensorrt-llm 302 -/guides/providers/tensorrt-llm/ /docs/built-in/tensorrt-llm 302 -/hardware/recommendations/by-model/ /docs 302 -/hardware/recommendations/by-hardware/ /docs 302 -/product /docs 302 -/product/features /docs 302 -/product/features/agents-framework /docs 302 -/product/features/api-server /docs/local-api 302 -/product/features/data-security /docs 302 -/product/features/extensions-framework /docs/extensions 302 -/product/features/local /docs 302 -/product/features/remote /docs 302 -/product/home-server /docs/local-api 302 -/guides/providers/tensorrt-llm/ /docs/built-in/tensorrt-llm 302 -/docs/tools /docs/tools/retrieval 302 -/docs/local-inference/llamacpp /docs/built-in/llama-cpp 302 -/docs/local-inference/tensorrtllm /docs/built-in/tensorrt-llm 302 -/guides/using-server/server/ /docs/local-api 302 -/integrations/coding/vscode /integrations/coding/continue-dev 302 -/docs/integrations/interpreter /integrations/function-calling/interpreter 302 -/cortex/built-in/llama-cpp /docs 302 -/docs/desktop-installation/linux /docs/desktop/linux 302 -/docs/desktop-installation/windows /docs/desktop/windows 302 -/docs/desktop-installation/mac /docs/desktop/mac 302 -/desktop/ /docs/desktop 302 -/developer/ui/ /docs 302 -/docs/local-inference/lmstudio /docs/local-models/lmstudio 302 -/docs/local-inference/ollama /docs/local-models/ollama 302 -/docs/remote-inference/openai /docs/remote-models/openai 302 -/docs/remote-inference/groq /docs/remote-models/groq 302 -/docs/remote-inference/mistralai /docs/remote-models/mistralai 302 -/docs/remote-inference/openrouter /docs/remote-models/openrouter 302 -/docs/remote-inference/generic-openai /docs/remote-models/generic-openai 302 -/docs/desktop-installation /docs/desktop 302 -/hardware/concepts/gpu-and-vram/ /docs 302 -/hardware/recommendations/by-usecase/ /docs 302 -/about/how-we-work/strategy /about 302 -/docs/engineering/assistants/ /docs 302 -/cortex https://cortex.so/docs/ 301 -/cortex/quickstart https://cortex.so/docs/quickstart 301 -/cortex/hardware https://cortex.so/docs/hardware 301 -/cortex/installation https://cortex.so/docs/category/installation 301 -/cortex/installation/mac https://cortex.so/docs/instalation/mac 301 -/cortex/installation/windows https://cortex.so/docs/instalation/windows 301 -/cortex/installation/linux https://cortex.so/docs/instalation/linux 301 -/cortex/command-line https://cortex.so/docs/command-line 301 -/cortex/ts-library https://cortex.so/docs/ts-library 301 -/cortex/py-library https://cortex.so/docs/py-library 301 -/cortex/server https://cortex.so/docs/server 301 -/cortex/text-generation https://cortex.so/docs/text-generation 301 -/cortex/cli https://cortex.so/docs/cli/ 301 -/cortex/cli/init https://cortex.so/docs/cli/init 301 -/cortex/cli/pull https://cortex.so/docs/cli/pull 301 -/cortex/cli/run https://cortex.so/docs/cli/run 301 -/cortex/cli/models https://cortex.so/docs/cli/models/ 301 -/cortex/cli/models/download https://cortex.so/docs/cli/models/download 301 -/cortex/cli/models/list https://cortex.so/docs/cli/models/list 301 -/cortex/cli/models/get https://cortex.so/docs/cli/models/get 301 -/cortex/cli/models/update https://cortex.so/docs/cli/models/update 301 -/cortex/cli/models/start https://cortex.so/docs/cli/models/start 301 -/cortex/cli/models/stop https://cortex.so/docs/cli/models/stop 301 -/cortex/cli/models/remove https://cortex.so/docs/cli/models/remove 301 -/cortex/cli/ps https://cortex.so/docs/cli/ps 301 -/cortex/cli/chat https://cortex.so/docs/cli/chat 301 -/cortex/cli/kill https://cortex.so/docs/cli/kill 301 -/cortex/cli/serve https://cortex.so/docs/cli/serve 301 -/cortex/architecture https://cortex.so/docs/architecture 301 -/cortex/cortex-cpp https://cortex.so/docs/cortex-cpp 301 -/cortex/cortex-llamacpp https://cortex.so/docs/cortex-llamacpp 301 -/api-reference https://cortex.so/api-reference 301 -/docs/assistants /docs 302 -/docs/server-installation/ /docs/desktop 302 -/docs/server-installation/onprem /docs/desktop 302 -/docs/server-installation/aws /docs/desktop 302 -/docs/server-installation/gcp /docs/desktop 302 -/docs/server-installation/azure /docs/desktop 302 -/about /docs 302 -/api-server /docs/desktop/api-server 302 -/cdn-cgi/l/email-protection 302 -/docs/built-in/tensorrt-llm 302 -/docs/desktop/beta /docs 302 -/docs/docs/data-folder /docs/desktop/data-folder 302 -/docs/docs/desktop/linux /docs/desktop/linux 302 -/docs/docs/troubleshooting /docs/desktop/troubleshooting 302 -/docs/local-engines/llama-cpp 302 -/docs/models/model-parameters 302 -/mcp /docs/desktop/mcp 302 -/quickstart /docs/desktop/quickstart 302 -/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 - From dc097eaef918b527c83ad41dfd15c52d14936096 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 24 Sep 2025 09:45:27 +0700 Subject: [PATCH 17/56] fix: missing url on article --- docs/src/pages/docs/desktop/data-folder.mdx | 6 +++--- docs/src/pages/docs/desktop/index.mdx | 8 ++++---- docs/src/pages/docs/desktop/install/linux.mdx | 8 ++++---- docs/src/pages/docs/desktop/install/mac.mdx | 4 ++-- docs/src/pages/docs/desktop/install/windows.mdx | 2 +- docs/src/pages/docs/desktop/llama-cpp-server.mdx | 2 +- docs/src/pages/docs/desktop/manage-models.mdx | 10 +++++----- docs/src/pages/docs/desktop/mcp.mdx | 2 +- docs/src/pages/docs/desktop/privacy.mdx | 2 +- docs/src/pages/docs/desktop/quickstart.mdx | 6 +++--- .../pages/docs/desktop/remote-models/anthropic.mdx | 4 ++-- docs/src/pages/docs/desktop/remote-models/cohere.mdx | 4 ++-- docs/src/pages/docs/desktop/remote-models/google.mdx | 4 ++-- docs/src/pages/docs/desktop/remote-models/groq.mdx | 4 ++-- .../pages/docs/desktop/remote-models/huggingface.mdx | 2 +- .../pages/docs/desktop/remote-models/mistralai.mdx | 4 ++-- docs/src/pages/docs/desktop/remote-models/openai.mdx | 4 ++-- .../pages/docs/desktop/remote-models/openrouter.mdx | 2 +- docs/src/pages/docs/desktop/server-settings.mdx | 6 +++--- .../pages/docs/desktop/server-troubleshooting.mdx | 8 ++++---- docs/src/pages/docs/desktop/settings.mdx | 4 ++-- docs/src/pages/docs/desktop/troubleshooting.mdx | 12 ++++++------ .../pages/post/benchmarking-nvidia-tensorrt-llm.mdx | 2 +- docs/src/pages/post/deepresearch.mdx | 2 +- 24 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/src/pages/docs/desktop/data-folder.mdx b/docs/src/pages/docs/desktop/data-folder.mdx index 4c582c801..9db2402f4 100644 --- a/docs/src/pages/docs/desktop/data-folder.mdx +++ b/docs/src/pages/docs/desktop/data-folder.mdx @@ -155,7 +155,7 @@ Debugging headquarters (`/logs/app.txt`): The silicon brain collection. Each model has its own `model.json`. -Full parameters: [here](/docs/model-parameters) +Full parameters: [here](/docs/desktop/desktop/model-parameters) ### `threads/` @@ -216,5 +216,5 @@ Chat archive. Each thread (`/threads/jan_unixstamp/`) contains: ## Delete Jan Data -Uninstall guides: [Mac](/docs/desktop/mac#step-2-clean-up-data-optional), -[Windows](/docs/desktop/windows#step-2-handle-jan-data), or [Linux](docs/desktop/linux#uninstall-jan). +Uninstall guides: [Mac](/docs/desktop/desktop/install/mac#step-2-clean-up-data-optional), +[Windows](/docs/desktop/desktop/install/windows#step-2-handle-jan-data), or [Linux](docs/desktop/install/linux#uninstall-jan). diff --git a/docs/src/pages/docs/desktop/index.mdx b/docs/src/pages/docs/desktop/index.mdx index 5e37e76b3..c46ddfeda 100644 --- a/docs/src/pages/docs/desktop/index.mdx +++ b/docs/src/pages/docs/desktop/index.mdx @@ -184,9 +184,9 @@ Jan is built on the shoulders of giants: **Supported OS**: - - [Windows 10+](/docs/desktop/windows#compatibility) - - [macOS 12+](/docs/desktop/mac#compatibility) - - [Linux (Ubuntu 20.04+)](/docs/desktop/linux) + - [Windows 10+](/docs/desktop/desktop/install/windows#compatibility) + - [macOS 12+](/docs/desktop/desktop/install/mac#compatibility) + - [Linux (Ubuntu 20.04+)](/docs/desktop/desktop/install/linux) **Hardware**: - Minimum: 8GB RAM, 10GB storage @@ -216,7 +216,7 @@ Jan is built on the shoulders of giants: - Runs 100% offline once models are downloaded - - All data stored locally in [Jan Data Folder](/docs/data-folder) + - All data stored locally in [Jan Data Folder](/docs/desktop/desktop/data-folder) - No telemetry without explicit consent - Open source code you can audit diff --git a/docs/src/pages/docs/desktop/install/linux.mdx b/docs/src/pages/docs/desktop/install/linux.mdx index 7eacd67ce..2d42a59f1 100644 --- a/docs/src/pages/docs/desktop/install/linux.mdx +++ b/docs/src/pages/docs/desktop/install/linux.mdx @@ -193,7 +193,7 @@ $XDG_CONFIG_HOME = /home/username/custom_config ~/.config/Jan/data ``` -See [Jan Data Folder](/docs/data-folder) for details. +See [Jan Data Folder](/docs/desktop/data-folder) for details. ## GPU Acceleration @@ -244,7 +244,7 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64 ### Step 2: Enable GPU Acceleration 1. Navigate to **Settings** () > **Local Engine** > **Llama.cpp** -2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/local-engines/llama-cpp). +2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/desktop/local-engines/llama-cpp). CUDA offers better performance than Vulkan. @@ -258,7 +258,7 @@ CUDA offers better performance than Vulkan. Requires Vulkan support. 1. Navigate to **Settings** () > **Hardware** > **GPUs** -2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/local-engines/llama-cpp). +2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/desktop/local-engines/llama-cpp). @@ -266,7 +266,7 @@ Requires Vulkan support. Requires Vulkan support. 1. Navigate to **Settings** () > **Hardware** > **GPUs** -2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/local-engines/llama-cpp). +2. Select appropriate backend in **llama-cpp Backend**. Details in our [guide](/docs/desktop/local-engines/llama-cpp). diff --git a/docs/src/pages/docs/desktop/install/mac.mdx b/docs/src/pages/docs/desktop/install/mac.mdx index d62c67878..827329d6e 100644 --- a/docs/src/pages/docs/desktop/install/mac.mdx +++ b/docs/src/pages/docs/desktop/install/mac.mdx @@ -111,7 +111,7 @@ Default location: # Default installation directory ~/Library/Application\ Support/Jan/data ``` -See [Jan Data Folder](/docs/data-folder) for details. +See [Jan Data Folder](/docs/desktop/data-folder) for details. ## Uninstall Jan @@ -158,7 +158,7 @@ No, it cannot be restored once you delete the Jan data folder during uninstallat -💡 Warning: If you have any trouble during installation, please see our [Troubleshooting](/docs/troubleshooting) +💡 Warning: If you have any trouble during installation, please see our [Troubleshooting](/docs/desktop/troubleshooting) guide to resolve your problem. diff --git a/docs/src/pages/docs/desktop/install/windows.mdx b/docs/src/pages/docs/desktop/install/windows.mdx index 7cda7c8a3..2c56e2319 100644 --- a/docs/src/pages/docs/desktop/install/windows.mdx +++ b/docs/src/pages/docs/desktop/install/windows.mdx @@ -119,7 +119,7 @@ Default installation path: ~\Users\\AppData\Roaming\Jan\data ``` -See [Jan Data Folder](/docs/data-folder) for complete folder structure details. +See [Jan Data Folder](/docs/desktop/data-folder) for complete folder structure details. ## GPU Acceleration diff --git a/docs/src/pages/docs/desktop/llama-cpp-server.mdx b/docs/src/pages/docs/desktop/llama-cpp-server.mdx index 3a3d24c46..54efcdd20 100644 --- a/docs/src/pages/docs/desktop/llama-cpp-server.mdx +++ b/docs/src/pages/docs/desktop/llama-cpp-server.mdx @@ -24,7 +24,7 @@ import { Settings } from 'lucide-react' `llama.cpp` is the core **inference engine** Jan uses to run AI models locally on your computer. This section covers the settings for the engine itself, which control *how* a model processes information on your hardware. -Looking for API server settings (like port, host, CORS)? They have been moved to the dedicated [**Local API Server**](/docs/api-server) page. +Looking for API server settings (like port, host, CORS)? They have been moved to the dedicated [**Local API Server**](/docs/desktop/desktop/api-server) page. ## Accessing Engine Settings diff --git a/docs/src/pages/docs/desktop/manage-models.mdx b/docs/src/pages/docs/desktop/manage-models.mdx index 645c36fe7..08781f47f 100644 --- a/docs/src/pages/docs/desktop/manage-models.mdx +++ b/docs/src/pages/docs/desktop/manage-models.mdx @@ -30,9 +30,9 @@ This guide shows you how to add, customize, and delete models within Jan. Local models are managed through [Llama.cpp](https://github.com/ggerganov/llama.cpp), and these models are in a format called GGUF. When you run them locally, they will use your computer's memory (RAM) and processing power, so please make sure that you download models that match the hardware specifications for your operating system: -- [Mac](/docs/desktop/mac#compatibility) -- [Windows](/docs/desktop/windows#compatibility) -- [Linux](/docs/desktop/linux#compatibility). +- [Mac](/docs/desktop/desktop/install/mac#compatibility) +- [Windows](/docs/desktop/desktop/install/windows#compatibility) +- [Linux](/docs/desktop/desktop/install/linux#compatibility). ### Adding Models @@ -156,7 +156,7 @@ For advanced users who want to add a specific model that is not available within Key fields to configure: 1. The **Settings** array is where you can set the path or location of your model in your computer, the context length allowed, and the chat template expected by your model. -2. The [**Parameters**](/docs/model-parameters) are the adjustable settings that affect how your model operates or +2. The [**Parameters**](/docs/desktop/desktop/model-parameters) are the adjustable settings that affect how your model operates or processes the data. The fields in the parameters array are typically general and can be used across different models. Here is an example of model parameters: @@ -186,7 +186,7 @@ models. Here is an example of model parameters: When using cloud models, be aware of any associated costs and rate limits from the providers. See detailed guide for -each cloud model provider [here](/docs/remote-models/anthropic). +each cloud model provider [here](/docs/desktop/desktop/remote-models/anthropic). Jan supports connecting to various AI cloud providers that are OpenAI API-compatible, including: OpenAI (GPT-4o, o3,...), diff --git a/docs/src/pages/docs/desktop/mcp.mdx b/docs/src/pages/docs/desktop/mcp.mdx index 03eaa0556..0c3fcfa1f 100644 --- a/docs/src/pages/docs/desktop/mcp.mdx +++ b/docs/src/pages/docs/desktop/mcp.mdx @@ -100,7 +100,7 @@ making your workflows more modular and adaptable over time. To use MCP effectively, ensure your AI model supports tool calling capabilities: - For cloud models (like Claude or GPT-4): Verify tool calling is enabled in your API settings - - For local models: Enable tool calling in the model parameters [click the edit button in Model Capabilities](/docs/model-parameters#model-capabilities-edit-button) + - For local models: Enable tool calling in the model parameters [click the edit button in Model Capabilities](/docs/desktop/desktop/model-parameters#model-capabilities-edit-button) - Check the model's documentation to confirm MCP compatibility diff --git a/docs/src/pages/docs/desktop/privacy.mdx b/docs/src/pages/docs/desktop/privacy.mdx index 4fd0a1830..5f9d0d3dd 100644 --- a/docs/src/pages/docs/desktop/privacy.mdx +++ b/docs/src/pages/docs/desktop/privacy.mdx @@ -26,7 +26,7 @@ import { Callout } from 'nextra/components' Jan is your AI. Period. Here's what we do with data. -Full privacy policy lives [here](/docs/privacy-policy), if you're into that sort of thing. +Full privacy policy lives [here](/docs/desktop/desktop/privacy-policy), if you're into that sort of thing. diff --git a/docs/src/pages/docs/desktop/quickstart.mdx b/docs/src/pages/docs/desktop/quickstart.mdx index b9a923b57..9999f1644 100644 --- a/docs/src/pages/docs/desktop/quickstart.mdx +++ b/docs/src/pages/docs/desktop/quickstart.mdx @@ -27,7 +27,7 @@ Get up and running with Jan in minutes. This guide will help you install Jan, do ### Step 1: Install Jan 1. [Download Jan](/download) -2. Install the app ([Mac](/docs/desktop/mac), [Windows](/docs/desktop/windows), [Linux](/docs/desktop/linux)) +2. Install the app ([Mac](/docs/desktop/desktop/install/mac), [Windows](/docs/desktop/desktop/install/windows), [Linux](/docs/desktop/desktop/install/linux)) 3. Launch Jan ### Step 2: Download Jan v1 @@ -61,7 +61,7 @@ Try asking Jan v1 questions like: - "What are the pros and cons of electric vehicles?" -**Want to give Jan v1 access to current web information?** Check out our [Serper MCP tutorial](/docs/mcp-examples/search/serper) to enable real-time web search with 2,500 free searches! +**Want to give Jan v1 access to current web information?** Check out our [Serper MCP tutorial](/docs/desktop/desktop/mcp-examples/search/serper) to enable real-time web search with 2,500 free searches! @@ -138,4 +138,4 @@ Connect to OpenAI, Anthropic, Groq, Mistral, and others: ![Connect Remote APIs](./_assets/quick-start-03.png) -For detailed setup, see [Remote APIs](/docs/remote-models/openai). +For detailed setup, see [Remote APIs](/docs/desktop/desktop/remote-models/openai). diff --git a/docs/src/pages/docs/desktop/remote-models/anthropic.mdx b/docs/src/pages/docs/desktop/remote-models/anthropic.mdx index 6662ecbb1..09418aad0 100644 --- a/docs/src/pages/docs/desktop/remote-models/anthropic.mdx +++ b/docs/src/pages/docs/desktop/remote-models/anthropic.mdx @@ -56,7 +56,7 @@ Ensure your API key has sufficient credits ## Available Anthropic Models Jan automatically includes Anthropic's available models. In case you want to use a specific Anthropic model -that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/manage-models#add-models-1): +that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/desktop/manage-models#add-models-1): - See list of available models in [Anthropic Models](https://docs.anthropic.com/claude/docs/models-overview). - The `id` property must match the model name in the list. For example, `claude-opus-4@20250514`, `claude-sonnet-4@20250514`, or `claude-3-5-haiku@20241022`. @@ -72,7 +72,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify Anthropic's system status -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/cohere.mdx b/docs/src/pages/docs/desktop/remote-models/cohere.mdx index af9098480..05d2a4c74 100644 --- a/docs/src/pages/docs/desktop/remote-models/cohere.mdx +++ b/docs/src/pages/docs/desktop/remote-models/cohere.mdx @@ -55,7 +55,7 @@ Ensure your API key has sufficient credits. ## Available Cohere Models Jan automatically includes Cohere's available models. In case you want to use a specific -Cohere model that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/manage-models): +Cohere model that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/desktop/manage-models): - See list of available models in [Cohere Documentation](https://docs.cohere.com/v2/docs/models). - The `id` property must match the model name in the list. For example, `command-nightly` or `command-light`. @@ -71,7 +71,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify Cohere's [system status](https://status.cohere.com/) -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/google.mdx b/docs/src/pages/docs/desktop/remote-models/google.mdx index d29f1290b..3984e429a 100644 --- a/docs/src/pages/docs/desktop/remote-models/google.mdx +++ b/docs/src/pages/docs/desktop/remote-models/google.mdx @@ -53,7 +53,7 @@ Ensure your API key has sufficient credits ## Available Google Models Jan automatically includes Google's available models like Gemini series. In case you want to use a specific -Gemini model that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/manage-models#add-models-1): +Gemini model that you cannot find in **Jan**, follow instructions in [Add Cloud Models](/docs/desktop/manage-models#add-models-1): - See list of available models in [Google Models](https://ai.google.dev/gemini-api/docs/models/gemini). - The `id` property must match the model name in the list. For example, `gemini-1.5-pro` or `gemini-2.0-flash-lite-preview`. @@ -69,7 +69,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify [Gemini's system status](https://www.google.com/appsstatus/dashboard/) -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/groq.mdx b/docs/src/pages/docs/desktop/remote-models/groq.mdx index 7db6a97b2..95feb1d6e 100644 --- a/docs/src/pages/docs/desktop/remote-models/groq.mdx +++ b/docs/src/pages/docs/desktop/remote-models/groq.mdx @@ -54,7 +54,7 @@ Ensure your API key has sufficient credits ## Available Models Through Groq Jan automatically includes Groq's available models. In case you want to use a specific Groq model that -you cannot find in **Jan**, follow the instructions in the [Add Cloud Models](/docs/manage-models#add-models-1): +you cannot find in **Jan**, follow the instructions in the [Add Cloud Models](/docs/desktop/manage-models#add-models-1): - See list of available models in [Groq Documentation](https://console.groq.com/docs/models). - The `id` property must match the model name in the list. For example, if you want to use Llama3.3 70B, you must set the `id` property to `llama-3.3-70b-versatile`. @@ -70,7 +70,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify Groq's system status -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/huggingface.mdx b/docs/src/pages/docs/desktop/remote-models/huggingface.mdx index 07f2103d2..4a7891586 100644 --- a/docs/src/pages/docs/desktop/remote-models/huggingface.mdx +++ b/docs/src/pages/docs/desktop/remote-models/huggingface.mdx @@ -141,7 +141,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify Hugging Face's system status -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/mistralai.mdx b/docs/src/pages/docs/desktop/remote-models/mistralai.mdx index ea403a701..52271cd68 100644 --- a/docs/src/pages/docs/desktop/remote-models/mistralai.mdx +++ b/docs/src/pages/docs/desktop/remote-models/mistralai.mdx @@ -56,7 +56,7 @@ Ensure your API key has sufficient credits ## Available Mistral Models Jan automatically includes Mistral's available models. In case you want to use a specific Mistral model -that you cannot find in **Jan**, follow the instructions in [Add Cloud Models](/docs/manage-models#add-models-1): +that you cannot find in **Jan**, follow the instructions in [Add Cloud Models](/docs/desktop/manage-models#add-models-1): - See list of available models in [Mistral AI Documentation](https://docs.mistral.ai/platform/endpoints). - The `id` property must match the model name in the list. For example, if you want to use Mistral Large, you must set the `id` property to `mistral-large-latest` @@ -73,7 +73,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify Mistral AI's system status -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/openai.mdx b/docs/src/pages/docs/desktop/remote-models/openai.mdx index 92be21a29..6c78f31c3 100644 --- a/docs/src/pages/docs/desktop/remote-models/openai.mdx +++ b/docs/src/pages/docs/desktop/remote-models/openai.mdx @@ -58,7 +58,7 @@ Start chatting ## Available OpenAI Models Jan automatically includes popular OpenAI models. In case you want to use a specific model that you -cannot find in Jan, follow instructions in [Add Cloud Models](/docs/manage-models#add-models-1): +cannot find in Jan, follow instructions in [Add Cloud Models](/docs/desktop/manage-models#add-models-1): - See list of available models in [OpenAI Platform](https://platform.openai.com/docs/models/overview). - The id property must match the model name in the list. For example, if you want to use the [GPT-4.5](https://platform.openai.com/docs/models/), you must set the id property @@ -76,7 +76,7 @@ Common issues and solutions: 2. Connection Problems - Check your internet connection - Verify OpenAI's [system status](https://status.openai.com) -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) 3. Model Unavailable - Confirm your API key has access to the model diff --git a/docs/src/pages/docs/desktop/remote-models/openrouter.mdx b/docs/src/pages/docs/desktop/remote-models/openrouter.mdx index 186a504b9..0faf68dac 100644 --- a/docs/src/pages/docs/desktop/remote-models/openrouter.mdx +++ b/docs/src/pages/docs/desktop/remote-models/openrouter.mdx @@ -88,7 +88,7 @@ Common issues and solutions: **2. Connection Problems** - Check your internet connection - Verify OpenRouter's [system status](https://status.openrouter.ai) -- Look for error messages in [Jan's logs](/docs/troubleshooting#how-to-get-error-logs) +- Look for error messages in [Jan's logs](/docs/desktop/troubleshooting#how-to-get-error-logs) **3. Model Unavailable** - Confirm the model is currently available on OpenRouter diff --git a/docs/src/pages/docs/desktop/server-settings.mdx b/docs/src/pages/docs/desktop/server-settings.mdx index b352293e5..8e0a7bde9 100644 --- a/docs/src/pages/docs/desktop/server-settings.mdx +++ b/docs/src/pages/docs/desktop/server-settings.mdx @@ -69,7 +69,7 @@ Click the gear icon next to any model to adjust how it behaves: - **Presence Penalty**: Encourages the model to use varied vocabulary -For detailed explanations of these parameters, see our [Model Parameters Guide](/docs/model-parameters). +For detailed explanations of these parameters, see our [Model Parameters Guide](/docs/desktop/desktop/model-parameters). ## Hardware Monitoring @@ -117,7 +117,7 @@ Access privacy settings at **Settings** > **Privacy**: - Change this setting anytime -See exactly what we collect (with your permission) in our [Privacy Policy](/docs/privacy). +See exactly what we collect (with your permission) in our [Privacy Policy](/docs/desktop/desktop/privacy). ![Analytics](./_assets/settings-07.png) @@ -174,7 +174,7 @@ This includes configuration for: - CORS (Cross-Origin Resource Sharing) - Verbose Logging -[**Go to Local API Server Settings →**](/docs/api-server) +[**Go to Local API Server Settings →**](/docs/desktop/desktop/api-server) ## Emergency Options diff --git a/docs/src/pages/docs/desktop/server-troubleshooting.mdx b/docs/src/pages/docs/desktop/server-troubleshooting.mdx index 2bd8f649a..4f5c1e983 100644 --- a/docs/src/pages/docs/desktop/server-troubleshooting.mdx +++ b/docs/src/pages/docs/desktop/server-troubleshooting.mdx @@ -226,7 +226,7 @@ When models won't respond or show these errors: - **RAM:** Use models under 80% of available memory - 8GB system: Use models under 6GB - 16GB system: Use models under 13GB -- **Hardware:** Verify your system meets [minimum requirements](/docs/troubleshooting#step-1-verify-hardware-and-system-requirements) +- **Hardware:** Verify your system meets [minimum requirements](/docs/desktop/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) **2. Adjust Model Settings** - Open model settings in the chat sidebar @@ -318,9 +318,9 @@ If these solutions don't work: - Include your logs and system info **3. Check Resources:** -- [System requirements](/docs/troubleshooting#step-1-verify-hardware-and-system-requirements) -- [Model compatibility guides](/docs/manage-models) -- [Hardware setup guides](/docs/desktop/) +- [System requirements](/docs/desktop/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) +- [Model compatibility guides](/docs/desktop/desktop/manage-models) +- [Hardware setup guides](/docs/desktop/desktop/) When sharing logs, remove personal information first. We only keep logs for 24 hours, so report issues promptly. diff --git a/docs/src/pages/docs/desktop/settings.mdx b/docs/src/pages/docs/desktop/settings.mdx index def78e867..d910ec875 100644 --- a/docs/src/pages/docs/desktop/settings.mdx +++ b/docs/src/pages/docs/desktop/settings.mdx @@ -68,7 +68,7 @@ Click the gear icon next to a model to configure advanced settings: - **Repeat Penalty**: Controls how strongly the model avoids repeating phrases (higher values reduce repetition) - **Presence Penalty**: Discourages reusing words that already appeared in the text (helps with variety) -_See [Model Parameters](/docs/model-parameters) for a more detailed explanation._ +_See [Model Parameters](/docs/desktop/desktop/model-parameters) for a more detailed explanation._ ## Hardware @@ -108,7 +108,7 @@ You can help improve Jan by sharing anonymous usage data: 2. You can change this setting at any time -Read more about that we collect with opt-in users at [Privacy](/docs/privacy). +Read more about that we collect with opt-in users at [Privacy](/docs/desktop/desktop/privacy).
diff --git a/docs/src/pages/docs/desktop/troubleshooting.mdx b/docs/src/pages/docs/desktop/troubleshooting.mdx index 0a905e9b4..420cd17b3 100644 --- a/docs/src/pages/docs/desktop/troubleshooting.mdx +++ b/docs/src/pages/docs/desktop/troubleshooting.mdx @@ -328,19 +328,19 @@ This command ensures that the necessary permissions are granted for Jan's instal When you start a chat with a model and encounter a **Failed to Fetch** or **Something's Amiss** error, here are some possible solutions to resolve it: **1. Check System & Hardware Requirements** -- Hardware dependencies: Ensure your device meets all [hardware requirements](docs/troubleshooting#step-1-verify-hardware-and-system-requirements) -- OS: Ensure your operating system meets the minimum requirements ([Mac](/docs/desktop/mac#minimum-requirements), [Windows](/docs/desktop/windows#compatibility), [Linux](docs/desktop/linux#compatibility)) +- Hardware dependencies: Ensure your device meets all [hardware requirements](docs/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) +- OS: Ensure your operating system meets the minimum requirements ([Mac](/docs/desktop/desktop/install/mac#minimum-requirements), [Windows](/docs/desktop/desktop/install/windows#compatibility), [Linux](/docs/desktop/desktop/install/linux#compatibility)) - RAM: Choose models that use less than 80% of your available RAM - For 8GB systems: Use models under 6GB - For 16GB systems: Use models under 13GB **2. Check Model Parameters** -- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/models/model-parameters#engine-parameters)) setting to see if it's too high +- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/desktop/desktop/models/model-parameters#engine-parameters)) setting to see if it's too high - Start with a lower NGL value and increase gradually based on your GPU memory **3. Port Conflicts** -If you check your [app logs](/docs/troubleshooting#how-to-get-error-logs) & see "Bind address failed at 127.0.0.1:39291", check port availability: +If you check your [app logs](/docs/desktop/desktop/troubleshooting#how-to-get-error-logs) & see "Bind address failed at 127.0.0.1:39291", check port availability: ``` # Mac netstat -an | grep 39291 @@ -371,7 +371,7 @@ This will delete all chat history, models, and settings.
**5. Try a clean installation** -- Uninstall Jan & clean Jan data folders ([Mac](/docs/desktop/mac#uninstall-jan), [Windows](/docs/desktop/windows#uninstall-jan), [Linux](docs/desktop/linux#uninstall-jan)) +- Uninstall Jan & clean Jan data folders ([Mac](/docs/desktop/desktop/install/mac#uninstall-jan), [Windows](/docs/desktop/desktop/install/windows#uninstall-jan), [Linux](/docs/desktop/desktop/install/linux#uninstall-jan)) - Install the latest [stable release](/download) @@ -392,7 +392,7 @@ The "Unexpected token" error usually relates to OpenAI API authentication or reg ## Need Further Support? If you can't find what you need in our troubleshooting guide, feel free reach out to us for extra help: -- **Copy** your [app logs](/docs/troubleshooting#how-to-get-error-logs) +- **Copy** your [app logs](/docs/desktop/desktop/troubleshooting#how-to-get-error-logs) - Go to our [Discord](https://discord.com/invite/FTk2MvZwJH) & send it to **#🆘|jan-help** channel for further support. diff --git a/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx b/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx index 4d0df7cc5..0d4bc9aa2 100644 --- a/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx +++ b/docs/src/pages/post/benchmarking-nvidia-tensorrt-llm.mdx @@ -17,7 +17,7 @@ Jan now supports [NVIDIA TensorRT-LLM](https://github.com/NVIDIA/TensorRT-LLM) i We've been excited for TensorRT-LLM for a while, and [had a lot of fun implementing it](https://github.com/menloresearch/nitro-tensorrt-llm). As part of the process, we've run some benchmarks, to see how TensorRT-LLM fares on consumer hardware (e.g. [4090s](https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/), [3090s](https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/)) we commonly see in the [Jan's hardware community](https://discord.com/channels/1107178041848909847/1201834752206974996). - **Give it a try!** Jan's [TensorRT-LLM extension](/docs/built-in/tensorrt-llm) is available in Jan v0.4.9 and up ([see more](/docs/built-in/tensorrt-llm)). We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂 + **Give it a try!** Jan's [TensorRT-LLM extension](/docs/desktop/built-in/tensorrt-llm) is available in Jan v0.4.9 and up ([see more](/docs/desktop/built-in/tensorrt-llm)). We precompiled some TensorRT-LLM models for you to try: `Mistral 7b`, `TinyLlama-1.1b`, `TinyJensen-1.1b` 😂 Bugs or feedback? Let us know on [GitHub](https://github.com/menloresearch/jan) or via [Discord](https://discord.com/channels/1107178041848909847/1201832734704795688). diff --git a/docs/src/pages/post/deepresearch.mdx b/docs/src/pages/post/deepresearch.mdx index 62e584082..11edd4f04 100644 --- a/docs/src/pages/post/deepresearch.mdx +++ b/docs/src/pages/post/deepresearch.mdx @@ -126,7 +126,7 @@ any version with Model Context Protocol in it (>`v0.6.3`). **The Key: Assistants + Tools** Running deep research in Jan can be accomplished by combining [custom assistants](https://jan.ai/docs/assistants) -with [MCP search tools](https://jan.ai/docs/mcp-examples/search/exa). This pairing allows any model—local or +with [MCP search tools](https://jan.ai/docs/desktop/mcp-examples/search/exa). This pairing allows any model—local or cloud—to follow a systematic research workflow, to create a report similar to that of other providers, with some visible limitations (for now). From 78fc5a8184dbfdbb8d689ffd2dd30dc14eda10c8 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 24 Sep 2025 09:48:08 +0700 Subject: [PATCH 18/56] chore: fix double desktop --- docs/src/pages/docs/desktop/data-folder.mdx | 6 +++--- docs/src/pages/docs/desktop/index.mdx | 8 ++++---- docs/src/pages/docs/desktop/llama-cpp-server.mdx | 2 +- docs/src/pages/docs/desktop/manage-models.mdx | 10 +++++----- docs/src/pages/docs/desktop/mcp.mdx | 2 +- docs/src/pages/docs/desktop/privacy.mdx | 2 +- docs/src/pages/docs/desktop/quickstart.mdx | 6 +++--- docs/src/pages/docs/desktop/server-settings.mdx | 6 +++--- docs/src/pages/docs/desktop/server-troubleshooting.mdx | 8 ++++---- docs/src/pages/docs/desktop/settings.mdx | 4 ++-- docs/src/pages/docs/desktop/troubleshooting.mdx | 10 +++++----- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/src/pages/docs/desktop/data-folder.mdx b/docs/src/pages/docs/desktop/data-folder.mdx index 9db2402f4..2e44df6f3 100644 --- a/docs/src/pages/docs/desktop/data-folder.mdx +++ b/docs/src/pages/docs/desktop/data-folder.mdx @@ -155,7 +155,7 @@ Debugging headquarters (`/logs/app.txt`): The silicon brain collection. Each model has its own `model.json`. -Full parameters: [here](/docs/desktop/desktop/model-parameters) +Full parameters: [here](/docs/desktop/model-parameters) ### `threads/` @@ -216,5 +216,5 @@ Chat archive. Each thread (`/threads/jan_unixstamp/`) contains: ## Delete Jan Data -Uninstall guides: [Mac](/docs/desktop/desktop/install/mac#step-2-clean-up-data-optional), -[Windows](/docs/desktop/desktop/install/windows#step-2-handle-jan-data), or [Linux](docs/desktop/install/linux#uninstall-jan). +Uninstall guides: [Mac](/docs/desktop/install/mac#step-2-clean-up-data-optional), +[Windows](/docs/desktop/install/windows#step-2-handle-jan-data), or [Linux](docs/desktop/install/linux#uninstall-jan). diff --git a/docs/src/pages/docs/desktop/index.mdx b/docs/src/pages/docs/desktop/index.mdx index c46ddfeda..a6ebed221 100644 --- a/docs/src/pages/docs/desktop/index.mdx +++ b/docs/src/pages/docs/desktop/index.mdx @@ -184,9 +184,9 @@ Jan is built on the shoulders of giants: **Supported OS**: - - [Windows 10+](/docs/desktop/desktop/install/windows#compatibility) - - [macOS 12+](/docs/desktop/desktop/install/mac#compatibility) - - [Linux (Ubuntu 20.04+)](/docs/desktop/desktop/install/linux) + - [Windows 10+](/docs/desktop/install/windows#compatibility) + - [macOS 12+](/docs/desktop/install/mac#compatibility) + - [Linux (Ubuntu 20.04+)](/docs/desktop/install/linux) **Hardware**: - Minimum: 8GB RAM, 10GB storage @@ -216,7 +216,7 @@ Jan is built on the shoulders of giants: - Runs 100% offline once models are downloaded - - All data stored locally in [Jan Data Folder](/docs/desktop/desktop/data-folder) + - All data stored locally in [Jan Data Folder](/docs/desktop/data-folder) - No telemetry without explicit consent - Open source code you can audit diff --git a/docs/src/pages/docs/desktop/llama-cpp-server.mdx b/docs/src/pages/docs/desktop/llama-cpp-server.mdx index 54efcdd20..0d72020db 100644 --- a/docs/src/pages/docs/desktop/llama-cpp-server.mdx +++ b/docs/src/pages/docs/desktop/llama-cpp-server.mdx @@ -24,7 +24,7 @@ import { Settings } from 'lucide-react' `llama.cpp` is the core **inference engine** Jan uses to run AI models locally on your computer. This section covers the settings for the engine itself, which control *how* a model processes information on your hardware. -Looking for API server settings (like port, host, CORS)? They have been moved to the dedicated [**Local API Server**](/docs/desktop/desktop/api-server) page. +Looking for API server settings (like port, host, CORS)? They have been moved to the dedicated [**Local API Server**](/docs/desktop/api-server) page. ## Accessing Engine Settings diff --git a/docs/src/pages/docs/desktop/manage-models.mdx b/docs/src/pages/docs/desktop/manage-models.mdx index 08781f47f..5014cf431 100644 --- a/docs/src/pages/docs/desktop/manage-models.mdx +++ b/docs/src/pages/docs/desktop/manage-models.mdx @@ -30,9 +30,9 @@ This guide shows you how to add, customize, and delete models within Jan. Local models are managed through [Llama.cpp](https://github.com/ggerganov/llama.cpp), and these models are in a format called GGUF. When you run them locally, they will use your computer's memory (RAM) and processing power, so please make sure that you download models that match the hardware specifications for your operating system: -- [Mac](/docs/desktop/desktop/install/mac#compatibility) -- [Windows](/docs/desktop/desktop/install/windows#compatibility) -- [Linux](/docs/desktop/desktop/install/linux#compatibility). +- [Mac](/docs/desktop/install/mac#compatibility) +- [Windows](/docs/desktop/install/windows#compatibility) +- [Linux](/docs/desktop/install/linux#compatibility). ### Adding Models @@ -156,7 +156,7 @@ For advanced users who want to add a specific model that is not available within Key fields to configure: 1. The **Settings** array is where you can set the path or location of your model in your computer, the context length allowed, and the chat template expected by your model. -2. The [**Parameters**](/docs/desktop/desktop/model-parameters) are the adjustable settings that affect how your model operates or +2. The [**Parameters**](/docs/desktop/model-parameters) are the adjustable settings that affect how your model operates or processes the data. The fields in the parameters array are typically general and can be used across different models. Here is an example of model parameters: @@ -186,7 +186,7 @@ models. Here is an example of model parameters: When using cloud models, be aware of any associated costs and rate limits from the providers. See detailed guide for -each cloud model provider [here](/docs/desktop/desktop/remote-models/anthropic). +each cloud model provider [here](/docs/desktop/remote-models/anthropic). Jan supports connecting to various AI cloud providers that are OpenAI API-compatible, including: OpenAI (GPT-4o, o3,...), diff --git a/docs/src/pages/docs/desktop/mcp.mdx b/docs/src/pages/docs/desktop/mcp.mdx index 0c3fcfa1f..3440ddaab 100644 --- a/docs/src/pages/docs/desktop/mcp.mdx +++ b/docs/src/pages/docs/desktop/mcp.mdx @@ -100,7 +100,7 @@ making your workflows more modular and adaptable over time. To use MCP effectively, ensure your AI model supports tool calling capabilities: - For cloud models (like Claude or GPT-4): Verify tool calling is enabled in your API settings - - For local models: Enable tool calling in the model parameters [click the edit button in Model Capabilities](/docs/desktop/desktop/model-parameters#model-capabilities-edit-button) + - For local models: Enable tool calling in the model parameters [click the edit button in Model Capabilities](/docs/desktop/model-parameters#model-capabilities-edit-button) - Check the model's documentation to confirm MCP compatibility diff --git a/docs/src/pages/docs/desktop/privacy.mdx b/docs/src/pages/docs/desktop/privacy.mdx index 5f9d0d3dd..429b052dd 100644 --- a/docs/src/pages/docs/desktop/privacy.mdx +++ b/docs/src/pages/docs/desktop/privacy.mdx @@ -26,7 +26,7 @@ import { Callout } from 'nextra/components' Jan is your AI. Period. Here's what we do with data. -Full privacy policy lives [here](/docs/desktop/desktop/privacy-policy), if you're into that sort of thing. +Full privacy policy lives [here](/docs/desktop/privacy-policy), if you're into that sort of thing. diff --git a/docs/src/pages/docs/desktop/quickstart.mdx b/docs/src/pages/docs/desktop/quickstart.mdx index 9999f1644..668354a39 100644 --- a/docs/src/pages/docs/desktop/quickstart.mdx +++ b/docs/src/pages/docs/desktop/quickstart.mdx @@ -27,7 +27,7 @@ Get up and running with Jan in minutes. This guide will help you install Jan, do ### Step 1: Install Jan 1. [Download Jan](/download) -2. Install the app ([Mac](/docs/desktop/desktop/install/mac), [Windows](/docs/desktop/desktop/install/windows), [Linux](/docs/desktop/desktop/install/linux)) +2. Install the app ([Mac](/docs/desktop/install/mac), [Windows](/docs/desktop/install/windows), [Linux](/docs/desktop/install/linux)) 3. Launch Jan ### Step 2: Download Jan v1 @@ -61,7 +61,7 @@ Try asking Jan v1 questions like: - "What are the pros and cons of electric vehicles?" -**Want to give Jan v1 access to current web information?** Check out our [Serper MCP tutorial](/docs/desktop/desktop/mcp-examples/search/serper) to enable real-time web search with 2,500 free searches! +**Want to give Jan v1 access to current web information?** Check out our [Serper MCP tutorial](/docs/desktop/mcp-examples/search/serper) to enable real-time web search with 2,500 free searches! @@ -138,4 +138,4 @@ Connect to OpenAI, Anthropic, Groq, Mistral, and others: ![Connect Remote APIs](./_assets/quick-start-03.png) -For detailed setup, see [Remote APIs](/docs/desktop/desktop/remote-models/openai). +For detailed setup, see [Remote APIs](/docs/desktop/remote-models/openai). diff --git a/docs/src/pages/docs/desktop/server-settings.mdx b/docs/src/pages/docs/desktop/server-settings.mdx index 8e0a7bde9..f7be5af26 100644 --- a/docs/src/pages/docs/desktop/server-settings.mdx +++ b/docs/src/pages/docs/desktop/server-settings.mdx @@ -69,7 +69,7 @@ Click the gear icon next to any model to adjust how it behaves: - **Presence Penalty**: Encourages the model to use varied vocabulary -For detailed explanations of these parameters, see our [Model Parameters Guide](/docs/desktop/desktop/model-parameters). +For detailed explanations of these parameters, see our [Model Parameters Guide](/docs/desktop/model-parameters). ## Hardware Monitoring @@ -117,7 +117,7 @@ Access privacy settings at **Settings** > **Privacy**: - Change this setting anytime -See exactly what we collect (with your permission) in our [Privacy Policy](/docs/desktop/desktop/privacy). +See exactly what we collect (with your permission) in our [Privacy Policy](/docs/desktop/privacy). ![Analytics](./_assets/settings-07.png) @@ -174,7 +174,7 @@ This includes configuration for: - CORS (Cross-Origin Resource Sharing) - Verbose Logging -[**Go to Local API Server Settings →**](/docs/desktop/desktop/api-server) +[**Go to Local API Server Settings →**](/docs/desktop/api-server) ## Emergency Options diff --git a/docs/src/pages/docs/desktop/server-troubleshooting.mdx b/docs/src/pages/docs/desktop/server-troubleshooting.mdx index 4f5c1e983..dd51aed99 100644 --- a/docs/src/pages/docs/desktop/server-troubleshooting.mdx +++ b/docs/src/pages/docs/desktop/server-troubleshooting.mdx @@ -226,7 +226,7 @@ When models won't respond or show these errors: - **RAM:** Use models under 80% of available memory - 8GB system: Use models under 6GB - 16GB system: Use models under 13GB -- **Hardware:** Verify your system meets [minimum requirements](/docs/desktop/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) +- **Hardware:** Verify your system meets [minimum requirements](/docs/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) **2. Adjust Model Settings** - Open model settings in the chat sidebar @@ -318,9 +318,9 @@ If these solutions don't work: - Include your logs and system info **3. Check Resources:** -- [System requirements](/docs/desktop/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) -- [Model compatibility guides](/docs/desktop/desktop/manage-models) -- [Hardware setup guides](/docs/desktop/desktop/) +- [System requirements](/docs/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) +- [Model compatibility guides](/docs/desktop/manage-models) +- [Hardware setup guides](/docs/desktop/) When sharing logs, remove personal information first. We only keep logs for 24 hours, so report issues promptly. diff --git a/docs/src/pages/docs/desktop/settings.mdx b/docs/src/pages/docs/desktop/settings.mdx index d910ec875..6bc750f43 100644 --- a/docs/src/pages/docs/desktop/settings.mdx +++ b/docs/src/pages/docs/desktop/settings.mdx @@ -68,7 +68,7 @@ Click the gear icon next to a model to configure advanced settings: - **Repeat Penalty**: Controls how strongly the model avoids repeating phrases (higher values reduce repetition) - **Presence Penalty**: Discourages reusing words that already appeared in the text (helps with variety) -_See [Model Parameters](/docs/desktop/desktop/model-parameters) for a more detailed explanation._ +_See [Model Parameters](/docs/desktop/model-parameters) for a more detailed explanation._ ## Hardware @@ -108,7 +108,7 @@ You can help improve Jan by sharing anonymous usage data: 2. You can change this setting at any time -Read more about that we collect with opt-in users at [Privacy](/docs/desktop/desktop/privacy). +Read more about that we collect with opt-in users at [Privacy](/docs/desktop/privacy).
diff --git a/docs/src/pages/docs/desktop/troubleshooting.mdx b/docs/src/pages/docs/desktop/troubleshooting.mdx index 420cd17b3..16bbdfa9a 100644 --- a/docs/src/pages/docs/desktop/troubleshooting.mdx +++ b/docs/src/pages/docs/desktop/troubleshooting.mdx @@ -329,18 +329,18 @@ When you start a chat with a model and encounter a **Failed to Fetch** or **Some **1. Check System & Hardware Requirements** - Hardware dependencies: Ensure your device meets all [hardware requirements](docs/desktop/troubleshooting#step-1-verify-hardware-and-system-requirements) -- OS: Ensure your operating system meets the minimum requirements ([Mac](/docs/desktop/desktop/install/mac#minimum-requirements), [Windows](/docs/desktop/desktop/install/windows#compatibility), [Linux](/docs/desktop/desktop/install/linux#compatibility)) +- OS: Ensure your operating system meets the minimum requirements ([Mac](/docs/desktop/install/mac#minimum-requirements), [Windows](/docs/desktop/install/windows#compatibility), [Linux](/docs/desktop/install/linux#compatibility)) - RAM: Choose models that use less than 80% of your available RAM - For 8GB systems: Use models under 6GB - For 16GB systems: Use models under 13GB **2. Check Model Parameters** -- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/desktop/desktop/models/model-parameters#engine-parameters)) setting to see if it's too high +- In **Engine Settings** in right sidebar, check your `ngl` ([number of GPU layers](/docs/desktop/models/model-parameters#engine-parameters)) setting to see if it's too high - Start with a lower NGL value and increase gradually based on your GPU memory **3. Port Conflicts** -If you check your [app logs](/docs/desktop/desktop/troubleshooting#how-to-get-error-logs) & see "Bind address failed at 127.0.0.1:39291", check port availability: +If you check your [app logs](/docs/desktop/troubleshooting#how-to-get-error-logs) & see "Bind address failed at 127.0.0.1:39291", check port availability: ``` # Mac netstat -an | grep 39291 @@ -371,7 +371,7 @@ This will delete all chat history, models, and settings.
**5. Try a clean installation** -- Uninstall Jan & clean Jan data folders ([Mac](/docs/desktop/desktop/install/mac#uninstall-jan), [Windows](/docs/desktop/desktop/install/windows#uninstall-jan), [Linux](/docs/desktop/desktop/install/linux#uninstall-jan)) +- Uninstall Jan & clean Jan data folders ([Mac](/docs/desktop/install/mac#uninstall-jan), [Windows](/docs/desktop/install/windows#uninstall-jan), [Linux](/docs/desktop/install/linux#uninstall-jan)) - Install the latest [stable release](/download) @@ -392,7 +392,7 @@ The "Unexpected token" error usually relates to OpenAI API authentication or reg ## Need Further Support? If you can't find what you need in our troubleshooting guide, feel free reach out to us for extra help: -- **Copy** your [app logs](/docs/desktop/desktop/troubleshooting#how-to-get-error-logs) +- **Copy** your [app logs](/docs/desktop/troubleshooting#how-to-get-error-logs) - Go to our [Discord](https://discord.com/invite/FTk2MvZwJH) & send it to **#🆘|jan-help** channel for further support. From db35d045b8de934963e535539ec19425523230dc Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 10:35:10 +0700 Subject: [PATCH 19/56] docs: add cache control --- docs/_headers | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_headers b/docs/_headers index d080abfd5..7635cd5f6 100644 --- a/docs/_headers +++ b/docs/_headers @@ -1,4 +1,6 @@ /* X-Frame-Options: SAMEORIGIN Permissions-Policy: interest-cohort=() - Strict-Transport-Security: max-age=31536000; includeSubDomains; preload \ No newline at end of file + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + Cache-Control: no-store, no-cache, must-revalidate + Pragma: no-cache \ No newline at end of file From 4dc55fc43b4aa73388b69ccb1eb1eac17cd132d4 Mon Sep 17 00:00:00 2001 From: hiento09 <136591877+hiento09@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:56:01 +0700 Subject: [PATCH 20/56] chore: fix docs (#6575) --- docs/_headers | 4 +- docs/public/sitemap-0.xml | 267 +++++++++++++++++--------------------- 2 files changed, 123 insertions(+), 148 deletions(-) diff --git a/docs/_headers b/docs/_headers index 7635cd5f6..d080abfd5 100644 --- a/docs/_headers +++ b/docs/_headers @@ -1,6 +1,4 @@ /* X-Frame-Options: SAMEORIGIN Permissions-Policy: interest-cohort=() - Strict-Transport-Security: max-age=31536000; includeSubDomains; preload - Cache-Control: no-store, no-cache, must-revalidate - Pragma: no-cache \ No newline at end of file + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload \ No newline at end of file diff --git a/docs/public/sitemap-0.xml b/docs/public/sitemap-0.xml index 517d84329..131222295 100644 --- a/docs/public/sitemap-0.xml +++ b/docs/public/sitemap-0.xml @@ -1,148 +1,125 @@ -https://jan.ai2025-03-10T05:06:47.876Zdaily1 -https://jan.ai/about2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/analytics2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/engineering2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/engineering/ci-cd2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/engineering/qa2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/product-design2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/project-management2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/strategy2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/handbook/website-docs2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/investors2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/team2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/vision2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/about/wall-of-love2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/blog2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2023-12-21-faster-inference-across-platform2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-01-16-settings-options-right-panel2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-01-29-local-api-server2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-02-05-jan-data-folder2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-02-10-jan-is-more-stable2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-02-26-home-servers-with-helm2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-03-06-ui-revamp-settings2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-03-11-import-models2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-04-02-groq-api-integration2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-04-15-new-mistral-extension2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-06-21-nvidia-nim-support2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-07-15-claude-3-5-support2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-09-17-improved-cpu-performance2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-10-24-jan-stable2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-11-22-jan-bugs2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-11.14-jan-supports-qwen-coder2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-12-03-jan-is-faster2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-12-05-jan-hot-fix-mac2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2024-12-30-jan-new-privacy2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2025-01-06-key-issues-resolved2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/changelog/2025-01-23-deepseek-r1-jan2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/architecture2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/assistants2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/build-extension2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/chat2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/init2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/kill2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/download2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/get2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/list2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/remove2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/start2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/stop2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/models/update2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/ps2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/pull2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/run2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cli/serve2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/command-line2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cortex-cpp2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cortex-llamacpp2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cortex-openvino2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cortex-python2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/cortex-tensorrt-llm2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/embeddings2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/embeddings/overview2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/error-codes2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/ext-architecture2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/fine-tuning2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/fine-tuning/overview2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/function-calling2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/hardware2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/installation2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/installation/linux2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/installation/mac2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/installation/windows2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/model-operations2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/model-operations/overview2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/py-library2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/quickstart2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/rag2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/rag/overview2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/server2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/text-generation2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/ts-library2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/vision2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/cortex/vision/overview2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/api-server2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/assistants2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/configure-extensions2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/data-folder2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/desktop2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/desktop/linux2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/desktop/mac2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/desktop/windows2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/error-codes2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/extensions2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/extensions-settings/model-management2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/extensions-settings/system-monitoring2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/install-engines2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/install-extensions2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/local-engines/llama-cpp2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/models2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/models/manage-models2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/models/model-parameters2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/privacy2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/privacy-policy2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/quickstart2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/anthropic2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/cohere2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/deepseek2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/google2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/groq2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/martian2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/mistralai2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/nvidia-nim2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/openai2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/openrouter2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/remote-models/triton2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/settings2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/threads2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/tools/retrieval2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/docs/troubleshooting2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/download2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations/coding/continue-dev2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations/coding/tabby2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations/function-calling/interpreter2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations/messaging/llmcord2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/integrations/workflow-automation/n8n2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/benchmarking-nvidia-tensorrt-llm2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/bitdefender2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/data-is-moat2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/deepseek-r1-locally2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/offline-chatgpt-alternative2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/rag-is-not-enough2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/post/run-ai-models-locally2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/privacy2025-03-10T05:06:47.877Zdaily1 -https://jan.ai/support2025-03-10T05:06:47.877Zdaily1 +https://jan.ai2025-09-24T03:40:05.491Zdaily1 +https://jan.ai/api-reference2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/api-reference/api-reference2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/api-reference/architecture2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/api-reference/configuration2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/api-reference/development2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/api-reference/installation2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/blog2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2023-12-21-faster-inference-across-platform2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-01-16-settings-options-right-panel2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-01-29-local-api-server2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-02-05-jan-data-folder2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-02-10-jan-is-more-stable2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-02-26-home-servers-with-helm2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-03-06-ui-revamp-settings2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-03-11-import-models2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-03-19-nitro-tensorrt-llm-extension2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-04-02-groq-api-integration2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-04-15-new-mistral-extension2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-04-25-llama3-command-r-hugginface2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-05-20-llamacpp-upgrade-new-remote-models2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-05-28-cohere-aya-23-8b-35b-phi-3-medium2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-06-21-nvidia-nim-support2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-07-15-claude-3-5-support2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-09-01-llama3-1-gemma2-support2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-09-17-improved-cpu-performance2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-10-24-jan-stable2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-11-22-jan-bugs2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-11.14-jan-supports-qwen-coder2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-12-03-jan-is-faster2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-12-05-jan-hot-fix-mac2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2024-12-30-jan-new-privacy2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-01-06-key-issues-resolved2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-01-23-deepseek-r1-jan2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-02-18-advanced-llama.cpp-settings2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-03-14-jan-security-patch2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-05-14-jan-qwen3-patch2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-06-19-jan-ui-revamp2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-06-26-jan-nano-mcp2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-07-17-responsive-ui2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-07-31-llamacpp-tutorials2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-08-07-gpt-oss2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-08-14-general-improvs2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-08-28-image-support2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/changelog/2025-09-18-auto-optimize-vision-imports2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/api-server2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/assistants2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/data-folder2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/install/linux2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/install/mac2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/install/windows2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/jan-models/jan-nano-1282025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/jan-models/jan-nano-322025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/jan-models/jan-v12025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/jan-models/lucy2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/llama-cpp2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/llama-cpp-server2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/manage-models2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/browser/browserbase2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/data-analysis/e2b2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/data-analysis/jupyter2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/deepresearch/octagon2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/design/canva2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/productivity/linear2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/productivity/todoist2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/search/exa2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/mcp-examples/search/serper2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/model-parameters2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/privacy2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/privacy-policy2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/quickstart2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/anthropic2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/cohere2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/google2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/groq2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/huggingface2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/mistralai2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/openai2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/remote-models/openrouter2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-examples/continue-dev2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-examples/llmcord2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-examples/n8n2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-examples/tabby2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-settings2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/server-troubleshooting2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/settings2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/desktop/troubleshooting2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-administration2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-authentication2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-chat2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-chat-conversations2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-conversations2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-jan-responses2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/api-reference-jan-server2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/architecture2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/configuration2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/development2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/installation2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/docs/server/overview2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/download2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/handbook2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/handbook/betting-on-open-source2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/handbook/open-superintelligence2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/benchmarking-nvidia-tensorrt-llm2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/bitdefender2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/data-is-moat2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/deepresearch2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/deepseek-r1-locally2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/jan-v1-for-research2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/offline-chatgpt-alternative2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/qwen3-settings2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/rag-is-not-enough2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/run-ai-models-locally2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/post/run-gpt-oss-locally2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/privacy2025-09-24T03:40:05.492Zdaily1 +https://jan.ai/support2025-09-24T03:40:05.492Zdaily1 \ No newline at end of file From 91e30d3c19c1d250d7b999a8eb4f87d8f6288853 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 12:12:41 +0700 Subject: [PATCH 21/56] docs: add clean output dir step --- .github/workflows/jan-docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/jan-docs.yml b/.github/workflows/jan-docs.yml index 3e92903c5..e6cc0977b 100644 --- a/.github/workflows/jan-docs.yml +++ b/.github/workflows/jan-docs.yml @@ -53,6 +53,9 @@ jobs: - name: Install dependencies working-directory: docs run: yarn install + - name: Clean output directory + working-directory: docs + run: rm -rf out/* .next/* - name: Build website working-directory: docs run: export NODE_ENV=production && yarn build && cp _redirects out/_redirects && cp _headers out/_headers From 9568ff12e86401eb0c8d5321953f8bcee12d37ee Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 15:47:46 +0700 Subject: [PATCH 22/56] feat: add cleanup logic for windows installer --- src-tauri/windows/hooks.nsh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src-tauri/windows/hooks.nsh b/src-tauri/windows/hooks.nsh index 5e1a32141..d1beed199 100644 --- a/src-tauri/windows/hooks.nsh +++ b/src-tauri/windows/hooks.nsh @@ -42,6 +42,11 @@ ${If} ${FileExists} "$INSTDIR\resources\LICENSE" CopyFiles /SILENT "$INSTDIR\resources\LICENSE" "$INSTDIR\LICENSE" DetailPrint "Copied LICENSE to install root" + + ; Optional cleanup - remove from resources folder + Delete "$INSTDIR\resources\LICENSE" + ${Else} + DetailPrint "LICENSE not found at expected location: $INSTDIR\resources\LICENSE" ${EndIf} ; ---- Copy vulkan-1.dll to install root ---- @@ -51,6 +56,7 @@ ; Optional cleanup - remove from resources folder Delete "$INSTDIR\resources\lib\vulkan-1.dll" + ; Only remove the lib directory if it's empty after removing both files RMDir "$INSTDIR\resources\lib" ${Else} From 57110d2bd7fe10c6658e388dfcacb6e10d981a7d Mon Sep 17 00:00:00 2001 From: Louis Date: Wed, 24 Sep 2025 17:57:10 +0700 Subject: [PATCH 23/56] fix: allow users to download the same model from different authors (#6577) * fix: allow users to download the same model from different authors * fix: importing models should have author name in the ID * fix: incorrect model id show * fix: tests * fix: default to mmproj f16 instead of bf16 * fix: type * fix: build error --- .../browser/extensions/engines/AIEngine.ts | 6 + .../src/jan-provider-web/provider.ts | 102 +++++-- extensions/llamacpp-extension/src/index.ts | 32 +- web-app/src/containers/DownloadButton.tsx | 142 +++++++++ web-app/src/containers/RenderMarkdown.tsx | 1 + web-app/src/routes/hub/$modelId.tsx | 40 ++- web-app/src/routes/hub/index.tsx | 282 ++++-------------- web-app/src/services/__tests__/models.test.ts | 106 +++++-- web-app/src/services/models/default.ts | 6 +- web-app/src/services/models/types.ts | 1 + 10 files changed, 407 insertions(+), 311 deletions(-) create mode 100644 web-app/src/containers/DownloadButton.tsx diff --git a/core/src/browser/extensions/engines/AIEngine.ts b/core/src/browser/extensions/engines/AIEngine.ts index 0e8a75fca..855f6e4dc 100644 --- a/core/src/browser/extensions/engines/AIEngine.ts +++ b/core/src/browser/extensions/engines/AIEngine.ts @@ -240,6 +240,12 @@ export abstract class AIEngine extends BaseExtension { EngineManager.instance().register(this) } + /** + * Gets model info + * @param modelId + */ + abstract get(modelId: string): Promise + /** * Lists available models */ diff --git a/extensions-web/src/jan-provider-web/provider.ts b/extensions-web/src/jan-provider-web/provider.ts index 216da66c9..dfdfe01b4 100644 --- a/extensions-web/src/jan-provider-web/provider.ts +++ b/extensions-web/src/jan-provider-web/provider.ts @@ -22,7 +22,7 @@ export default class JanProviderWeb extends AIEngine { override async onLoad() { console.log('Loading Jan Provider Extension...') - + try { // Initialize authentication and fetch models await janApiClient.initialize() @@ -37,20 +37,43 @@ export default class JanProviderWeb extends AIEngine { override async onUnload() { console.log('Unloading Jan Provider Extension...') - + // Clear all sessions for (const sessionId of this.activeSessions.keys()) { await this.unload(sessionId) } - + janProviderStore.reset() console.log('Jan Provider Extension unloaded') } + async get(modelId: string): Promise { + return janApiClient + .getModels() + .then((list) => list.find((e) => e.id === modelId)) + .then((model) => + model + ? { + id: model.id, + name: model.id, // Use ID as name for now + quant_type: undefined, + providerId: this.provider, + port: 443, // HTTPS port for API + sizeBytes: 0, // Size not provided by Jan API + tags: [], + path: undefined, // Remote model, no local path + owned_by: model.owned_by, + object: model.object, + capabilities: ['tools'], // Jan models support both tools via MCP + } + : undefined + ) + } + async list(): Promise { try { const janModels = await janApiClient.getModels() - + return janModels.map((model) => ({ id: model.id, name: model.id, // Use ID as name for now @@ -75,7 +98,7 @@ export default class JanProviderWeb extends AIEngine { // For Jan API, we don't actually "load" models in the traditional sense // We just create a session reference for tracking const sessionId = `jan-${modelId}-${Date.now()}` - + const sessionInfo: SessionInfo = { pid: Date.now(), // Use timestamp as pseudo-PID port: 443, // HTTPS port @@ -85,8 +108,10 @@ export default class JanProviderWeb extends AIEngine { } this.activeSessions.set(sessionId, sessionInfo) - - console.log(`Jan model session created: ${sessionId} for model ${modelId}`) + + console.log( + `Jan model session created: ${sessionId} for model ${modelId}` + ) return sessionInfo } catch (error) { console.error(`Failed to load Jan model ${modelId}:`, error) @@ -97,23 +122,23 @@ export default class JanProviderWeb extends AIEngine { async unload(sessionId: string): Promise { try { const session = this.activeSessions.get(sessionId) - + if (!session) { return { success: false, - error: `Session ${sessionId} not found` + error: `Session ${sessionId} not found`, } } this.activeSessions.delete(sessionId) console.log(`Jan model session unloaded: ${sessionId}`) - + return { success: true } } catch (error) { console.error(`Failed to unload Jan session ${sessionId}:`, error) return { success: false, - error: error instanceof Error ? error.message : 'Unknown error' + error: error instanceof Error ? error.message : 'Unknown error', } } } @@ -136,9 +161,12 @@ export default class JanProviderWeb extends AIEngine { } // Convert core chat completion request to Jan API format - const janMessages: JanChatMessage[] = opts.messages.map(msg => ({ + const janMessages: JanChatMessage[] = opts.messages.map((msg) => ({ role: msg.role as 'system' | 'user' | 'assistant', - content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + content: + typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content), })) const janRequest = { @@ -162,18 +190,18 @@ export default class JanProviderWeb extends AIEngine { } else { // Return single response const response = await janApiClient.createChatCompletion(janRequest) - + // Check if aborted after completion if (abortController?.signal?.aborted) { throw new Error('Request was aborted') } - + return { id: response.id, object: 'chat.completion' as const, created: response.created, model: response.model, - choices: response.choices.map(choice => ({ + choices: response.choices.map((choice) => ({ index: choice.index, message: { role: choice.message.role, @@ -182,7 +210,12 @@ export default class JanProviderWeb extends AIEngine { reasoning_content: choice.message.reasoning_content, tool_calls: choice.message.tool_calls, }, - finish_reason: (choice.finish_reason || 'stop') as 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'function_call', + finish_reason: (choice.finish_reason || 'stop') as + | 'stop' + | 'length' + | 'tool_calls' + | 'content_filter' + | 'function_call', })), usage: response.usage, } @@ -193,7 +226,10 @@ export default class JanProviderWeb extends AIEngine { } } - private async *createStreamingGenerator(janRequest: any, abortController?: AbortController) { + private async *createStreamingGenerator( + janRequest: any, + abortController?: AbortController + ) { let resolve: () => void let reject: (error: Error) => void const chunks: any[] = [] @@ -231,7 +267,7 @@ export default class JanProviderWeb extends AIEngine { object: chunk.object, created: chunk.created, model: chunk.model, - choices: chunk.choices.map(choice => ({ + choices: chunk.choices.map((choice) => ({ index: choice.index, delta: { role: choice.delta.role, @@ -261,14 +297,14 @@ export default class JanProviderWeb extends AIEngine { if (abortController?.signal?.aborted) { throw new Error('Request was aborted') } - + while (yieldedIndex < chunks.length) { yield chunks[yieldedIndex] yieldedIndex++ } - + // Wait a bit before checking again - await new Promise(resolve => setTimeout(resolve, 10)) + await new Promise((resolve) => setTimeout(resolve, 10)) } // Yield any remaining chunks @@ -291,24 +327,32 @@ export default class JanProviderWeb extends AIEngine { } async delete(modelId: string): Promise { - throw new Error(`Delete operation not supported for remote Jan API model: ${modelId}`) + throw new Error( + `Delete operation not supported for remote Jan API model: ${modelId}` + ) } async import(modelId: string, _opts: ImportOptions): Promise { - throw new Error(`Import operation not supported for remote Jan API model: ${modelId}`) + throw new Error( + `Import operation not supported for remote Jan API model: ${modelId}` + ) } async abortImport(modelId: string): Promise { - throw new Error(`Abort import operation not supported for remote Jan API model: ${modelId}`) + throw new Error( + `Abort import operation not supported for remote Jan API model: ${modelId}` + ) } async getLoadedModels(): Promise { - return Array.from(this.activeSessions.values()).map(session => session.model_id) + return Array.from(this.activeSessions.values()).map( + (session) => session.model_id + ) } async isToolSupported(modelId: string): Promise { // Jan models support tool calls via MCP - console.log(`Checking tool support for Jan model ${modelId}: supported`); - return true; + console.log(`Checking tool support for Jan model ${modelId}: supported`) + return true } -} \ No newline at end of file +} diff --git a/extensions/llamacpp-extension/src/index.ts b/extensions/llamacpp-extension/src/index.ts index 77b0aafcd..8fad4fd87 100644 --- a/extensions/llamacpp-extension/src/index.ts +++ b/extensions/llamacpp-extension/src/index.ts @@ -922,6 +922,30 @@ export default class llamacpp_extension extends AIEngine { return hash } + override async get(modelId: string): Promise { + const modelPath = await joinPath([ + await this.getProviderPath(), + 'models', + modelId, + ]) + const path = await joinPath([modelPath, 'model.yml']) + + if (!(await fs.existsSync(path))) return undefined + + const modelConfig = await invoke('read_yaml', { + path, + }) + + return { + id: modelId, + name: modelConfig.name ?? modelId, + quant_type: undefined, // TODO: parse quantization type from model.yml or model.gguf + providerId: this.provider, + port: 0, // port is not known until the model is loaded + sizeBytes: modelConfig.size_bytes ?? 0, + } as modelInfo + } + // Implement the required LocalProvider interface methods override async list(): Promise { const modelsDir = await joinPath([await this.getProviderPath(), 'models']) @@ -1085,7 +1109,10 @@ export default class llamacpp_extension extends AIEngine { const archiveName = await basename(path) logger.info(`Installing backend from path: ${path}`) - if (!(await fs.existsSync(path)) || (!path.endsWith('tar.gz') && !path.endsWith('zip'))) { + if ( + !(await fs.existsSync(path)) || + (!path.endsWith('tar.gz') && !path.endsWith('zip')) + ) { logger.error(`Invalid path or file ${path}`) throw new Error(`Invalid path or file ${path}`) } @@ -2601,7 +2628,8 @@ export default class llamacpp_extension extends AIEngine { metadata: Record ): Promise { // Extract vision parameters from metadata - const projectionDim = Math.floor(Number(metadata['clip.vision.projection_dim']) / 10) || 256 + const projectionDim = + Math.floor(Number(metadata['clip.vision.projection_dim']) / 10) || 256 // Count images in messages let imageCount = 0 diff --git a/web-app/src/containers/DownloadButton.tsx b/web-app/src/containers/DownloadButton.tsx new file mode 100644 index 000000000..7d4db703b --- /dev/null +++ b/web-app/src/containers/DownloadButton.tsx @@ -0,0 +1,142 @@ +import { Button } from '@/components/ui/button' +import { Progress } from '@/components/ui/progress' +import { useDownloadStore } from '@/hooks/useDownloadStore' +import { useGeneralSetting } from '@/hooks/useGeneralSetting' +import { useModelProvider } from '@/hooks/useModelProvider' +import { useServiceHub } from '@/hooks/useServiceHub' +import { useTranslation } from '@/i18n' +import { extractModelName } from '@/lib/models' +import { cn, sanitizeModelId } from '@/lib/utils' +import { CatalogModel } from '@/services/models/types' +import { useCallback, useMemo } from 'react' +import { useShallow } from 'zustand/shallow' + +type ModelProps = { + model: CatalogModel + handleUseModel: (modelId: string) => void +} +const defaultModelQuantizations = ['iq4_xs', 'q4_k_m'] + +export function DownloadButtonPlaceholder({ + model, + handleUseModel, +}: ModelProps) { + const { downloads, localDownloadingModels, addLocalDownloadingModel } = + useDownloadStore( + useShallow((state) => ({ + downloads: state.downloads, + localDownloadingModels: state.localDownloadingModels, + addLocalDownloadingModel: state.addLocalDownloadingModel, + })) + ) + const { t } = useTranslation() + const getProviderByName = useModelProvider((state) => state.getProviderByName) + const llamaProvider = getProviderByName('llamacpp') + + const serviceHub = useServiceHub() + const huggingfaceToken = useGeneralSetting((state) => state.huggingfaceToken) + + const quant = + model.quants.find((e) => + defaultModelQuantizations.some((m) => + e.model_id.toLowerCase().includes(m) + ) + ) ?? model.quants[0] + + const modelId = quant?.model_id || model.model_name + + const downloadProcesses = useMemo( + () => + Object.values(downloads).map((download) => ({ + id: download.name, + name: download.name, + progress: download.progress, + current: download.current, + total: download.total, + })), + [downloads] + ) + + const isRecommendedModel = useCallback((modelId: string) => { + return (extractModelName(modelId)?.toLowerCase() === + 'jan-nano-gguf') as boolean + }, []) + + if (model.quants.length === 0) { + return ( +
+ +
+ ) + } + + const modelUrl = quant?.path || modelId + const isDownloading = + localDownloadingModels.has(modelId) || + downloadProcesses.some((e) => e.id === modelId) + + const downloadProgress = + downloadProcesses.find((e) => e.id === modelId)?.progress || 0 + const isDownloaded = llamaProvider?.models.some( + (m: { id: string }) => + m.id === modelId || + m.id === `${model.developer}/${sanitizeModelId(modelId)}` + ) + const isRecommended = isRecommendedModel(model.model_name) + + const handleDownload = () => { + // Immediately set local downloading state + addLocalDownloadingModel(modelId) + const mmprojPath = ( + model.mmproj_models?.find( + (e) => e.model_id.toLowerCase() === 'mmproj-f16' + ) || model.mmproj_models?.[0] + )?.path + serviceHub + .models() + .pullModelWithMetadata(modelId, modelUrl, mmprojPath, huggingfaceToken) + } + + return ( +
+ {isDownloading && !isDownloaded && ( +
+ + + {Math.round(downloadProgress * 100)}% + +
+ )} + {isDownloaded ? ( + + ) : ( + + )} +
+ ) +} diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index da702eff6..31d08cf10 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -89,6 +89,7 @@ const CodeComponent = memo( onCopy, copiedId, ...props + // eslint-disable-next-line @typescript-eslint/no-explicit-any }: any) => { const { t } = useTranslation() const match = /language-(\w+)/.exec(className || '') diff --git a/web-app/src/routes/hub/$modelId.tsx b/web-app/src/routes/hub/$modelId.tsx index 75ccc58bf..102b5cece 100644 --- a/web-app/src/routes/hub/$modelId.tsx +++ b/web-app/src/routes/hub/$modelId.tsx @@ -21,10 +21,7 @@ import { useEffect, useMemo, useCallback, useState } from 'react' import { useModelProvider } from '@/hooks/useModelProvider' import { useDownloadStore } from '@/hooks/useDownloadStore' import { useServiceHub } from '@/hooks/useServiceHub' -import type { - CatalogModel, - ModelQuant, -} from '@/services/models/types' +import type { CatalogModel, ModelQuant } from '@/services/models/types' import { Progress } from '@/components/ui/progress' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' @@ -80,12 +77,13 @@ function HubModelDetailContent() { }, [fetchSources]) const fetchRepo = useCallback(async () => { - const repoInfo = await serviceHub.models().fetchHuggingFaceRepo( - search.repo || modelId, - huggingfaceToken - ) + const repoInfo = await serviceHub + .models() + .fetchHuggingFaceRepo(search.repo || modelId, huggingfaceToken) if (repoInfo) { - const repoDetail = serviceHub.models().convertHfRepoToCatalogModel(repoInfo) + const repoDetail = serviceHub + .models() + .convertHfRepoToCatalogModel(repoInfo) setRepoData(repoDetail || undefined) } }, [serviceHub, modelId, search, huggingfaceToken]) @@ -168,7 +166,9 @@ function HubModelDetailContent() { try { // Use the HuggingFace path for the model const modelPath = variant.path - const supported = await serviceHub.models().isModelSupported(modelPath, 8192) + const supported = await serviceHub + .models() + .isModelSupported(modelPath, 8192) setModelSupportStatus((prev) => ({ ...prev, [modelKey]: supported, @@ -473,12 +473,20 @@ function HubModelDetailContent() { addLocalDownloadingModel( variant.model_id ) - serviceHub.models().pullModelWithMetadata( - variant.model_id, - variant.path, - modelData.mmproj_models?.[0]?.path, - huggingfaceToken - ) + serviceHub + .models() + .pullModelWithMetadata( + variant.model_id, + variant.path, + ( + modelData.mmproj_models?.find( + (e) => + e.model_id.toLowerCase() === + 'mmproj-f16' + ) || modelData.mmproj_models?.[0] + )?.path, + huggingfaceToken + ) }} className={cn(isDownloading && 'hidden')} > diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 2a53a848f..be63c49b6 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useVirtualizer } from '@tanstack/react-virtual' -import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' +import { createFileRoute, useNavigate } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' import { cn } from '@/lib/utils' @@ -34,8 +34,6 @@ import { TooltipTrigger, } from '@/components/ui/tooltip' import { ModelInfoHoverCard } from '@/containers/ModelInfoHoverCard' -import Joyride, { CallBackProps, STATUS } from 'react-joyride' -import { CustomTooltipJoyRide } from '@/containers/CustomeTooltipJoyRide' import { DropdownMenu, DropdownMenuContent, @@ -51,10 +49,9 @@ import { Loader } from 'lucide-react' import { useTranslation } from '@/i18n/react-i18next-compat' import Fuse from 'fuse.js' import { useGeneralSetting } from '@/hooks/useGeneralSetting' +import { DownloadButtonPlaceholder } from '@/containers/DownloadButton' +import { useShallow } from 'zustand/shallow' -type ModelProps = { - model: CatalogModel -} type SearchParams = { repo: string } @@ -77,7 +74,7 @@ function Hub() { function HubContent() { const parentRef = useRef(null) - const { huggingfaceToken } = useGeneralSetting() + const huggingfaceToken = useGeneralSetting((state) => state.huggingfaceToken) const serviceHub = useServiceHub() const { t } = useTranslation() @@ -93,7 +90,13 @@ function HubContent() { } }, []) - const { sources, fetchSources, loading } = useModelSources() + const { sources, fetchSources, loading } = useModelSources( + useShallow((state) => ({ + sources: state.sources, + fetchSources: state.fetchSources, + loading: state.loading, + })) + ) const [searchValue, setSearchValue] = useState('') const [sortSelected, setSortSelected] = useState('newest') @@ -108,16 +111,9 @@ function HubContent() { const [modelSupportStatus, setModelSupportStatus] = useState< Record >({}) - const [joyrideReady, setJoyrideReady] = useState(false) - const [currentStepIndex, setCurrentStepIndex] = useState(0) const addModelSourceTimeoutRef = useRef | null>( null ) - const downloadButtonRef = useRef(null) - const hasTriggeredDownload = useRef(false) - - const { getProviderByName } = useModelProvider() - const llamaProvider = getProviderByName('llamacpp') const toggleModelExpansion = (modelId: string) => { setExpandedModels((prev) => ({ @@ -168,9 +164,10 @@ function HubContent() { ?.map((model) => ({ ...model, quants: model.quants.filter((variant) => - llamaProvider?.models.some( - (m: { id: string }) => m.id === variant.model_id - ) + useModelProvider + .getState() + .getProviderByName('llamacpp') + ?.models.some((m: { id: string }) => m.id === variant.model_id) ), })) .filter((model) => model.quants.length > 0) @@ -186,7 +183,6 @@ function HubContent() { showOnlyDownloaded, huggingFaceRepo, searchOptions, - llamaProvider?.models, ]) // The virtualizer @@ -215,9 +211,13 @@ function HubContent() { addModelSourceTimeoutRef.current = setTimeout(async () => { try { - const repoInfo = await serviceHub.models().fetchHuggingFaceRepo(searchValue, huggingfaceToken) + const repoInfo = await serviceHub + .models() + .fetchHuggingFaceRepo(searchValue, huggingfaceToken) if (repoInfo) { - const catalogModel = serviceHub.models().convertHfRepoToCatalogModel(repoInfo) + const catalogModel = serviceHub + .models() + .convertHfRepoToCatalogModel(repoInfo) if ( !sources.some( (s) => @@ -303,7 +303,9 @@ function HubContent() { try { // Use the HuggingFace path for the model const modelPath = variant.path - const supportStatus = await serviceHub.models().isModelSupported(modelPath, 8192) + const supportStatus = await serviceHub + .models() + .isModelSupported(modelPath, 8192) setModelSupportStatus((prev) => ({ ...prev, @@ -320,178 +322,7 @@ function HubContent() { [modelSupportStatus, serviceHub] ) - const DownloadButtonPlaceholder = useMemo(() => { - return ({ model }: ModelProps) => { - // Check if this is a HuggingFace repository (no quants) - if (model.quants.length === 0) { - return ( -
- -
- ) - } - - const quant = - model.quants.find((e) => - defaultModelQuantizations.some((m) => - e.model_id.toLowerCase().includes(m) - ) - ) ?? model.quants[0] - const modelId = quant?.model_id || model.model_name - const modelUrl = quant?.path || modelId - const isDownloading = - localDownloadingModels.has(modelId) || - downloadProcesses.some((e) => e.id === modelId) - const downloadProgress = - downloadProcesses.find((e) => e.id === modelId)?.progress || 0 - const isDownloaded = llamaProvider?.models.some( - (m: { id: string }) => m.id === modelId - ) - const isRecommended = isRecommendedModel(model.model_name) - - const handleDownload = () => { - // Immediately set local downloading state - addLocalDownloadingModel(modelId) - const mmprojPath = model.mmproj_models?.[0]?.path - serviceHub.models().pullModelWithMetadata( - modelId, - modelUrl, - mmprojPath, - huggingfaceToken - ) - } - - return ( -
- {isDownloading && !isDownloaded && ( -
- - - {Math.round(downloadProgress * 100)}% - -
- )} - {isDownloaded ? ( - - ) : ( - - )} -
- ) - } - }, [ - localDownloadingModels, - downloadProcesses, - llamaProvider?.models, - isRecommendedModel, - t, - addLocalDownloadingModel, - huggingfaceToken, - handleUseModel, - serviceHub, - ]) - - const { step } = useSearch({ from: Route.id }) - const isSetup = step === 'setup_local_provider' - - // Wait for DOM to be ready before starting Joyride - useEffect(() => { - if (!loading && filteredModels.length > 0 && isSetup) { - const timer = setTimeout(() => { - setJoyrideReady(true) - }, 100) - return () => clearTimeout(timer) - } else { - setJoyrideReady(false) - } - }, [loading, filteredModels.length, isSetup]) - - const handleJoyrideCallback = (data: CallBackProps) => { - const { status, index } = data - - if ( - status === STATUS.FINISHED && - !isDownloading && - isLastStep && - !hasTriggeredDownload.current - ) { - const recommendedModel = filteredModels.find((model) => - isRecommendedModel(model.model_name) - ) - if (recommendedModel && recommendedModel.quants[0]?.model_id) { - if (downloadButtonRef.current) { - hasTriggeredDownload.current = true - downloadButtonRef.current.click() - } - return - } - } - - if (status === STATUS.FINISHED) { - navigate({ - to: route.hub.index, - }) - } - - // Track current step index - setCurrentStepIndex(index) - } - - // Check if any model is currently downloading - const isDownloading = - localDownloadingModels.size > 0 || downloadProcesses.length > 0 - - const steps = [ - { - target: '.hub-model-card-step', - title: t('hub:joyride.recommendedModelTitle'), - disableBeacon: true, - content: t('hub:joyride.recommendedModelContent'), - }, - { - target: '.hub-download-button-step', - title: isDownloading - ? t('hub:joyride.downloadInProgressTitle') - : t('hub:joyride.downloadModelTitle'), - disableBeacon: true, - content: isDownloading - ? t('hub:joyride.downloadInProgressContent') - : t('hub:joyride.downloadModelContent'), - }, - ] - // Check if we're on the last step - const isLastStep = currentStepIndex === steps.length - 1 - const renderFilter = () => { return ( <> @@ -544,31 +375,6 @@ function HubContent() { return ( <> -
@@ -698,6 +504,7 @@ function HubContent() { />
@@ -908,10 +715,13 @@ function HubContent() { (e) => e.id === variant.model_id )?.progress || 0 const isDownloaded = - llamaProvider?.models.some( - (m: { id: string }) => - m.id === variant.model_id - ) + useModelProvider + .getState() + .getProviderByName('llamacpp') + ?.models.some( + (m: { id: string }) => + m.id === variant.model_id + ) if (isDownloading) { return ( @@ -962,14 +772,26 @@ function HubContent() { addLocalDownloadingModel( variant.model_id ) - serviceHub.models().pullModelWithMetadata( - variant.model_id, - variant.path, - filteredModels[ - virtualItem.index - ].mmproj_models?.[0]?.path, - huggingfaceToken - ) + serviceHub + .models() + .pullModelWithMetadata( + variant.model_id, + variant.path, + + ( + filteredModels[ + virtualItem.index + ].mmproj_models?.find( + (e) => + e.model_id.toLowerCase() === + 'mmproj-f16' + ) || + filteredModels[ + virtualItem.index + ].mmproj_models?.[0] + )?.path, + huggingfaceToken + ) }} > { let modelsService: DefaultModelsService - + const mockEngine = { list: vi.fn(), updateSettings: vi.fn(), @@ -246,7 +246,9 @@ describe('DefaultModelsService', () => { }) mockEngine.load.mockRejectedValue(error) - await expect(modelsService.startModel(provider, model)).rejects.toThrow(error) + await expect(modelsService.startModel(provider, model)).rejects.toThrow( + error + ) }) it('should not load model again', async () => { const mockSettings = { @@ -263,7 +265,9 @@ describe('DefaultModelsService', () => { includes: () => true, }) expect(mockEngine.load).toBeCalledTimes(0) - await expect(modelsService.startModel(provider, model)).resolves.toBe(undefined) + await expect(modelsService.startModel(provider, model)).resolves.toBe( + undefined + ) }) }) @@ -312,7 +316,9 @@ describe('DefaultModelsService', () => { json: vi.fn().mockResolvedValue(mockRepoData), }) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toEqual(mockRepoData) expect(fetch).toHaveBeenCalledWith( @@ -342,7 +348,9 @@ describe('DefaultModelsService', () => { ) // Test with domain prefix - await modelsService.fetchHuggingFaceRepo('huggingface.co/microsoft/DialoGPT-medium') + await modelsService.fetchHuggingFaceRepo( + 'huggingface.co/microsoft/DialoGPT-medium' + ) expect(fetch).toHaveBeenCalledWith( 'https://huggingface.co/api/models/microsoft/DialoGPT-medium?blobs=true&files_metadata=true', { @@ -365,7 +373,9 @@ describe('DefaultModelsService', () => { expect(await modelsService.fetchHuggingFaceRepo('')).toBeNull() // Test string without slash - expect(await modelsService.fetchHuggingFaceRepo('invalid-repo')).toBeNull() + expect( + await modelsService.fetchHuggingFaceRepo('invalid-repo') + ).toBeNull() // Test whitespace only expect(await modelsService.fetchHuggingFaceRepo(' ')).toBeNull() @@ -378,7 +388,8 @@ describe('DefaultModelsService', () => { statusText: 'Not Found', }) - const result = await modelsService.fetchHuggingFaceRepo('nonexistent/model') + const result = + await modelsService.fetchHuggingFaceRepo('nonexistent/model') expect(result).toBeNull() expect(fetch).toHaveBeenCalledWith( @@ -398,7 +409,9 @@ describe('DefaultModelsService', () => { statusText: 'Internal Server Error', }) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toBeNull() expect(consoleSpy).toHaveBeenCalledWith( @@ -414,7 +427,9 @@ describe('DefaultModelsService', () => { ;(fetch as any).mockRejectedValue(new Error('Network error')) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toBeNull() expect(consoleSpy).toHaveBeenCalledWith( @@ -448,7 +463,9 @@ describe('DefaultModelsService', () => { json: vi.fn().mockResolvedValue(mockRepoData), }) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toEqual(mockRepoData) }) @@ -487,7 +504,9 @@ describe('DefaultModelsService', () => { json: vi.fn().mockResolvedValue(mockRepoData), }) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toEqual(mockRepoData) }) @@ -531,7 +550,9 @@ describe('DefaultModelsService', () => { json: vi.fn().mockResolvedValue(mockRepoData), }) - const result = await modelsService.fetchHuggingFaceRepo('microsoft/DialoGPT-medium') + const result = await modelsService.fetchHuggingFaceRepo( + 'microsoft/DialoGPT-medium' + ) expect(result).toEqual(mockRepoData) // Verify the GGUF file is present in siblings @@ -576,7 +597,8 @@ describe('DefaultModelsService', () => { } it('should convert HuggingFace repo to catalog model format', () => { - const result = modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) + const result = + modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) const expected: CatalogModel = { model_name: 'microsoft/DialoGPT-medium', @@ -586,12 +608,12 @@ describe('DefaultModelsService', () => { num_quants: 2, quants: [ { - model_id: 'model-q4_0', + model_id: 'microsoft/model-q4_0', path: 'https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/model-q4_0.gguf', file_size: '2.0 GB', }, { - model_id: 'model-q8_0', + model_id: 'microsoft/model-q8_0', path: 'https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/model-q8_0.GGUF', file_size: '4.0 GB', }, @@ -635,7 +657,8 @@ describe('DefaultModelsService', () => { siblings: undefined, } - const result = modelsService.convertHfRepoToCatalogModel(repoWithoutSiblings) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithoutSiblings) expect(result.num_quants).toBe(0) expect(result.quants).toEqual([]) @@ -663,7 +686,9 @@ describe('DefaultModelsService', () => { ], } - const result = modelsService.convertHfRepoToCatalogModel(repoWithVariousFileSizes) + const result = modelsService.convertHfRepoToCatalogModel( + repoWithVariousFileSizes + ) expect(result.quants[0].file_size).toBe('500.0 MB') expect(result.quants[1].file_size).toBe('3.5 GB') @@ -676,7 +701,8 @@ describe('DefaultModelsService', () => { tags: [], } - const result = modelsService.convertHfRepoToCatalogModel(repoWithEmptyTags) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithEmptyTags) expect(result.description).toBe('**Tags**: ') }) @@ -687,7 +713,8 @@ describe('DefaultModelsService', () => { downloads: undefined as any, } - const result = modelsService.convertHfRepoToCatalogModel(repoWithoutDownloads) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithoutDownloads) expect(result.downloads).toBe(0) }) @@ -714,15 +741,17 @@ describe('DefaultModelsService', () => { ], } - const result = modelsService.convertHfRepoToCatalogModel(repoWithVariousGGUF) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithVariousGGUF) - expect(result.quants[0].model_id).toBe('model') - expect(result.quants[1].model_id).toBe('MODEL') - expect(result.quants[2].model_id).toBe('complex-model-name') + expect(result.quants[0].model_id).toBe('microsoft/model') + expect(result.quants[1].model_id).toBe('microsoft/MODEL') + expect(result.quants[2].model_id).toBe('microsoft/complex-model-name') }) it('should generate correct download paths', () => { - const result = modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) + const result = + modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) expect(result.quants[0].path).toBe( 'https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/model-q4_0.gguf' @@ -733,7 +762,8 @@ describe('DefaultModelsService', () => { }) it('should generate correct readme URL', () => { - const result = modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) + const result = + modelsService.convertHfRepoToCatalogModel(mockHuggingFaceRepo) expect(result.readme).toBe( 'https://huggingface.co/microsoft/DialoGPT-medium/resolve/main/README.md' @@ -767,13 +797,14 @@ describe('DefaultModelsService', () => { ], } - const result = modelsService.convertHfRepoToCatalogModel(repoWithMixedCase) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithMixedCase) expect(result.num_quants).toBe(3) expect(result.quants).toHaveLength(3) - expect(result.quants[0].model_id).toBe('model-1') - expect(result.quants[1].model_id).toBe('model-2') - expect(result.quants[2].model_id).toBe('model-3') + expect(result.quants[0].model_id).toBe('microsoft/model-1') + expect(result.quants[1].model_id).toBe('microsoft/model-2') + expect(result.quants[2].model_id).toBe('microsoft/model-3') }) it('should handle edge cases with file size formatting', () => { @@ -798,7 +829,8 @@ describe('DefaultModelsService', () => { ], } - const result = modelsService.convertHfRepoToCatalogModel(repoWithEdgeCases) + const result = + modelsService.convertHfRepoToCatalogModel(repoWithEdgeCases) expect(result.quants[0].file_size).toBe('0.0 MB') expect(result.quants[1].file_size).toBe('1.0 GB') @@ -850,7 +882,10 @@ describe('DefaultModelsService', () => { mockEngineManager.get.mockReturnValue(mockEngineWithSupport) - const result = await modelsService.isModelSupported('/path/to/model.gguf', 4096) + const result = await modelsService.isModelSupported( + '/path/to/model.gguf', + 4096 + ) expect(result).toBe('GREEN') expect(mockEngineWithSupport.isModelSupported).toHaveBeenCalledWith( @@ -867,7 +902,10 @@ describe('DefaultModelsService', () => { mockEngineManager.get.mockReturnValue(mockEngineWithSupport) - const result = await modelsService.isModelSupported('/path/to/model.gguf', 8192) + const result = await modelsService.isModelSupported( + '/path/to/model.gguf', + 8192 + ) expect(result).toBe('YELLOW') expect(mockEngineWithSupport.isModelSupported).toHaveBeenCalledWith( @@ -884,7 +922,9 @@ describe('DefaultModelsService', () => { mockEngineManager.get.mockReturnValue(mockEngineWithSupport) - const result = await modelsService.isModelSupported('/path/to/large-model.gguf') + const result = await modelsService.isModelSupported( + '/path/to/large-model.gguf' + ) expect(result).toBe('RED') expect(mockEngineWithSupport.isModelSupported).toHaveBeenCalledWith( diff --git a/web-app/src/services/models/default.ts b/web-app/src/services/models/default.ts index 5a31f3993..186706334 100644 --- a/web-app/src/services/models/default.ts +++ b/web-app/src/services/models/default.ts @@ -30,6 +30,10 @@ export class DefaultModelsService implements ModelsService { return EngineManager.instance().get(provider) as AIEngine | undefined } + async getModel(modelId: string): Promise { + return this.getEngine()?.get(modelId) + } + async fetchModels(): Promise { return this.getEngine()?.list() ?? [] } @@ -127,7 +131,7 @@ export class DefaultModelsService implements ModelsService { const modelId = file.rfilename.replace(/\.gguf$/i, '') return { - model_id: sanitizeModelId(modelId), + model_id: `${repo.author}/${sanitizeModelId(modelId)}`, path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`, file_size: formatFileSize(file.size), } diff --git a/web-app/src/services/models/types.ts b/web-app/src/services/models/types.ts index 5bf66b8bf..d92dae38a 100644 --- a/web-app/src/services/models/types.ts +++ b/web-app/src/services/models/types.ts @@ -90,6 +90,7 @@ export interface ModelPlan { } export interface ModelsService { + getModel(modelId: string): Promise fetchModels(): Promise fetchModelCatalog(): Promise fetchHuggingFaceRepo( From e322e46e4b2f146ecf42c0fd1c0ad276deb21106 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 18:29:03 +0700 Subject: [PATCH 24/56] chore: separate windows install script --- package.json | 3 +- scripts/download-lib.mjs | 19 ------ scripts/download-win-installer-deps.mjs | 83 +++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 scripts/download-win-installer-deps.mjs diff --git a/package.json b/package.json index 2ec212088..50eb8ecaf 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"", "download:lib": "node ./scripts/download-lib.mjs", "download:bin": "node ./scripts/download-bin.mjs", - "build:tauri:win32": "yarn download:bin && yarn download:lib && yarn tauri build", + "download:windows-installer": "node ./scripts/download-win-installer-deps.mjs", + "build:tauri:win32": "yarn download:bin && yarn download:lib && yarn download:windows-installer && yarn tauri build", "build:tauri:linux": "yarn download:bin && yarn download:lib && NO_STRIP=1 ./src-tauri/build-utils/shim-linuxdeploy.sh yarn tauri build && ./src-tauri/build-utils/buildAppImage.sh", "build:tauri:darwin": "yarn download:bin && yarn tauri build --target universal-apple-darwin", "build:tauri": "yarn build:icon && yarn copy:assets:tauri && run-script-os", diff --git a/scripts/download-lib.mjs b/scripts/download-lib.mjs index 6075a18d1..d2086b36e 100644 --- a/scripts/download-lib.mjs +++ b/scripts/download-lib.mjs @@ -77,25 +77,6 @@ async function main() { // Expect EEXIST error } - // Download VC++ Redistributable 17 - if (platform == 'win32') { - const vcFilename = 'vc_redist.x64.exe' - const vcUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe' - - console.log(`Downloading VC++ Redistributable...`) - const vcSavePath = path.join(tempDir, vcFilename) - if (!fs.existsSync(vcSavePath)) { - await download(vcUrl, vcSavePath) - } - - // copy to tauri resources - try { - copySync(vcSavePath, libDir) - } catch (err) { - // Expect EEXIST error - } - } - console.log('Downloads completed.') } diff --git a/scripts/download-win-installer-deps.mjs b/scripts/download-win-installer-deps.mjs new file mode 100644 index 000000000..33bbbe04b --- /dev/null +++ b/scripts/download-win-installer-deps.mjs @@ -0,0 +1,83 @@ +console.log('Downloading Windows installer dependencies...') +// scripts/download-win-installer-deps.mjs +import https from 'https' +import fs, { mkdirSync } from 'fs' +import os from 'os' +import path from 'path' +import { copySync } from 'cpx' + +function download(url, dest) { + return new Promise((resolve, reject) => { + console.log(`Downloading ${url} to ${dest}`) + const file = fs.createWriteStream(dest) + https + .get(url, (response) => { + console.log(`Response status code: ${response.statusCode}`) + if ( + response.statusCode >= 300 && + response.statusCode < 400 && + response.headers.location + ) { + // Handle redirect + const redirectURL = response.headers.location + console.log(`Redirecting to ${redirectURL}`) + download(redirectURL, dest).then(resolve, reject) // Recursive call + return + } else if (response.statusCode !== 200) { + reject(`Failed to get '${url}' (${response.statusCode})`) + return + } + response.pipe(file) + file.on('finish', () => { + file.close(resolve) + }) + }) + .on('error', (err) => { + fs.unlink(dest, () => reject(err.message)) + }) + }) +} + +async function main() { + console.log('Starting Windows installer dependencies download') + const platform = os.platform() // 'darwin', 'linux', 'win32' + const arch = os.arch() // 'x64', 'arm64', etc. + + if (arch != 'x64') return + + + const libDir = 'src-tauri/resources/lib' + const tempDir = 'scripts/dist' + + try { + mkdirSync('scripts/dist') + } catch (err) { + // Expect EEXIST error if the directory already exists + } + + // Download VC++ Redistributable 17 + if (platform == 'win32') { + const vcFilename = 'vc_redist.x64.exe' + const vcUrl = 'https://aka.ms/vs/17/release/vc_redist.x64.exe' + + console.log(`Downloading VC++ Redistributable...`) + const vcSavePath = path.join(tempDir, vcFilename) + if (!fs.existsSync(vcSavePath)) { + await download(vcUrl, vcSavePath) + } + + // copy to tauri resources + try { + copySync(vcSavePath, libDir) + } catch (err) { + // Expect EEXIST error + } + } + + console.log('Windows installer dependencies downloads completed.') +} + +main().catch((err) => { + console.error('Error:', err) + process.exit(1) +}) From 23f14ebbb71fb14fe00f4524a3af05c0f2f48732 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Wed, 24 Sep 2025 19:02:18 +0700 Subject: [PATCH 25/56] fix: window dependencies not downloaded during tests --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 085e42e74..9a03ddaad 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,9 @@ lint: install-and-build test: lint yarn download:bin yarn download:lib +ifeq ($(OS),Windows_NT) + yarn download:windows-installer +endif yarn test yarn copy:assets:tauri yarn build:icon From e7a1a06395c5ba0d224d11efe2f6678bf91facb5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 25 Sep 2025 10:12:08 +0700 Subject: [PATCH 26/56] feat: thread organization folder --- web-app/src/constants/localStorage.ts | 1 + web-app/src/constants/routes.ts | 2 + web-app/src/containers/ChatInput.tsx | 33 +- web-app/src/containers/LeftPanel.tsx | 318 ++++++++++++---- web-app/src/containers/ThreadList.tsx | 355 ++++++++++++------ .../containers/dialogs/AddProjectDialog.tsx | 125 ++++++ .../dialogs/DeleteProjectDialog.tsx | 85 +++++ web-app/src/containers/dialogs/index.ts | 3 +- web-app/src/hooks/useThreadManagement.ts | 82 ++++ web-app/src/hooks/useThreads.ts | 59 ++- web-app/src/locales/de-DE/common.json | 162 +++++++- web-app/src/locales/en/common.json | 75 ++++ web-app/src/locales/id/common.json | 83 ++++ web-app/src/locales/pl/common.json | 47 +++ web-app/src/locales/vn/common.json | 1 + web-app/src/locales/zh-CN/common.json | 1 + web-app/src/locales/zh-TW/common.json | 1 + web-app/src/routeTree.gen.ts | 54 ++- web-app/src/routes/assistant.tsx | 106 +++--- web-app/src/routes/project/$projectId.tsx | 143 +++++++ web-app/src/routes/project/index.tsx | 244 ++++++++++++ web-app/src/services/threads/default.ts | 9 +- web-app/src/types/threads.d.ts | 10 +- 23 files changed, 1735 insertions(+), 264 deletions(-) create mode 100644 web-app/src/containers/dialogs/AddProjectDialog.tsx create mode 100644 web-app/src/containers/dialogs/DeleteProjectDialog.tsx create mode 100644 web-app/src/hooks/useThreadManagement.ts create mode 100644 web-app/src/routes/project/$projectId.tsx create mode 100644 web-app/src/routes/project/index.tsx diff --git a/web-app/src/constants/localStorage.ts b/web-app/src/constants/localStorage.ts index ae744837b..f13f5fcab 100644 --- a/web-app/src/constants/localStorage.ts +++ b/web-app/src/constants/localStorage.ts @@ -21,4 +21,5 @@ export const localStorageKey = { lastUsedAssistant: 'last-used-assistant', favoriteModels: 'favorite-models', setupCompleted: 'setup-completed', + threadManagement: 'thread-management', } diff --git a/web-app/src/constants/routes.ts b/web-app/src/constants/routes.ts index 97f95631d..f1f870dd5 100644 --- a/web-app/src/constants/routes.ts +++ b/web-app/src/constants/routes.ts @@ -3,6 +3,8 @@ export const route = { home: '/', appLogs: '/logs', assistant: '/assistant', + project: '/project', + projectDetail: '/project/$projectId', settings: { index: '/settings', model_providers: '/settings/providers', diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index c5743647b..cba580ebd 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -4,6 +4,7 @@ import TextareaAutosize from 'react-textarea-autosize' import { cn } from '@/lib/utils' import { usePrompt } from '@/hooks/usePrompt' import { useThreads } from '@/hooks/useThreads' +import { useThreadManagement } from '@/hooks/useThreadManagement' import { useCallback, useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { @@ -43,9 +44,15 @@ type ChatInputProps = { showSpeedToken?: boolean model?: ThreadModel initialMessage?: boolean + projectId?: string } -const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { +const ChatInput = ({ + model, + className, + initialMessage, + projectId, +}: ChatInputProps) => { const textareaRef = useRef(null) const [isFocused, setIsFocused] = useState(false) const [rows, setRows] = useState(1) @@ -58,6 +65,8 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { const prompt = usePrompt((state) => state.prompt) const setPrompt = usePrompt((state) => state.setPrompt) const currentThreadId = useThreads((state) => state.currentThreadId) + const updateThread = useThreads((state) => state.updateThread) + const { getFolderById } = useThreadManagement() const { t } = useTranslation() const spellCheckChatInput = useGeneralSetting( (state) => state.spellCheckChatInput @@ -177,6 +186,28 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { uploadedFiles.length > 0 ? uploadedFiles : undefined ) setUploadedFiles([]) + + // Handle project assignment for new threads + if (projectId && !currentThreadId) { + const project = getFolderById(projectId) + if (project) { + // Use setTimeout to ensure the thread is created first + setTimeout(() => { + const newCurrentThreadId = useThreads.getState().currentThreadId + if (newCurrentThreadId) { + updateThread(newCurrentThreadId, { + metadata: { + project: { + id: project.id, + name: project.name, + updated_at: project.updated_at, + }, + }, + }) + } + }, 100) + } + } } useEffect(() => { diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 67e35fab2..cef872119 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -4,14 +4,18 @@ import { cn } from '@/lib/utils' import { IconLayoutSidebar, IconDots, - IconCirclePlusFilled, - IconSettingsFilled, + IconCirclePlus, + IconSettings, IconStar, - IconMessageFilled, - IconAppsFilled, + IconFolderPlus, + IconMessage, + IconApps, IconX, IconSearch, - IconClipboardSmileFilled, + IconClipboardSmile, + IconFolder, + IconPencil, + IconTrash, } from '@tabler/icons-react' import { route } from '@/constants/routes' import ThreadList from './ThreadList' @@ -28,6 +32,7 @@ import { UserProfileMenu } from '@/containers/auth/UserProfileMenu' import { useAuth } from '@/hooks/useAuth' import { useThreads } from '@/hooks/useThreads' +import { useThreadManagement } from '@/hooks/useThreadManagement' import { useTranslation } from '@/i18n/react-i18next-compat' import { useMemo, useState, useEffect, useRef } from 'react' @@ -37,38 +42,40 @@ import { useSmallScreen } from '@/hooks/useMediaQuery' import { useClickOutside } from '@/hooks/useClickOutside' import { DeleteAllThreadsDialog } from '@/containers/dialogs' +import AddProjectDialog from '@/containers/dialogs/AddProjectDialog' +import { DeleteProjectDialog } from '@/containers/dialogs/DeleteProjectDialog' const mainMenus = [ { title: 'common:newChat', - icon: IconCirclePlusFilled, + icon: IconCirclePlus, route: route.home, isEnabled: true, }, + { + title: 'Projects', + icon: IconFolderPlus, + route: route.project, + isEnabled: true, + }, { title: 'common:assistants', - icon: IconClipboardSmileFilled, + icon: IconClipboardSmile, route: route.assistant, isEnabled: PlatformFeatures[PlatformFeature.ASSISTANTS], }, { title: 'common:hub', - icon: IconAppsFilled, + icon: IconApps, route: route.hub.index, isEnabled: PlatformFeatures[PlatformFeature.MODEL_HUB], }, { title: 'common:settings', - icon: IconSettingsFilled, + icon: IconSettings, route: route.settings.general, isEnabled: true, }, - { - title: 'common:authentication', - icon: null, - route: null, - isEnabled: PlatformFeatures[PlatformFeature.AUTHENTICATION], - }, ] const LeftPanel = () => { @@ -152,20 +159,65 @@ const LeftPanel = () => { const getFilteredThreads = useThreads((state) => state.getFilteredThreads) const threads = useThreads((state) => state.threads) + const { folders, addFolder, updateFolder, deleteFolder, getFolderById } = + useThreadManagement() + + // Project dialog states + const [projectDialogOpen, setProjectDialogOpen] = useState(false) + const [editingProjectKey, setEditingProjectKey] = useState( + null + ) + const [deleteProjectConfirmOpen, setDeleteProjectConfirmOpen] = + useState(false) + const [deletingProjectId, setDeletingProjectId] = useState( + null + ) + const filteredThreads = useMemo(() => { return getFilteredThreads(searchTerm) // eslint-disable-next-line react-hooks/exhaustive-deps }, [getFilteredThreads, searchTerm, threads]) + const filteredProjects = useMemo(() => { + if (!searchTerm) return folders + return folders.filter((folder) => + folder.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + }, [folders, searchTerm]) + // Memoize categorized threads based on filteredThreads const favoritedThreads = useMemo(() => { return filteredThreads.filter((t) => t.isFavorite) }, [filteredThreads]) const unFavoritedThreads = useMemo(() => { - return filteredThreads.filter((t) => !t.isFavorite) + return filteredThreads.filter((t) => !t.isFavorite && !t.metadata?.project) }, [filteredThreads]) + // Project handlers + const handleProjectDelete = (id: string) => { + setDeletingProjectId(id) + setDeleteProjectConfirmOpen(true) + } + + const confirmProjectDelete = () => { + if (deletingProjectId) { + deleteFolder(deletingProjectId) + setDeleteProjectConfirmOpen(false) + setDeletingProjectId(null) + } + } + + const handleProjectSave = (name: string) => { + if (editingProjectKey) { + updateFolder(editingProjectKey, name) + } else { + addFolder(name) + } + setProjectDialogOpen(false) + setEditingProjectKey(null) + } + // Disable body scroll when panel is open on small screens useEffect(() => { if (isSmallScreen && open) { @@ -260,15 +312,12 @@ const LeftPanel = () => { )} -
-
+
+
{IS_MACOS && (
@@ -294,7 +343,151 @@ const LeftPanel = () => { )}
)} -
+ + {mainMenus.map((menu) => { + if (!menu.isEnabled) { + return null + } + + // Handle authentication menu specially + if (menu.title === 'common:authentication') { + return ( +
+
+ {isAuthenticated ? ( + + ) : ( + + )} +
+ ) + } + + // Regular menu items must have route and icon + if (!menu.route || !menu.icon) return null + + const isActive = (() => { + // Settings routes + if (menu.route.includes(route.settings.index)) { + return currentPath.includes(route.settings.index) + } + + // Default exact match for other routes + return currentPath === menu.route + })() + return ( + isSmallScreen && setLeftPanel(false)} + data-test-id={`menu-${menu.title}`} + activeOptions={{ exact: true }} + className={cn( + 'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded', + isActive && 'bg-left-panel-fg/10' + )} + > + + + {t(menu.title)} + + + ) + })} +
+ + {filteredProjects.length > 0 && ( +
+
+ + Projects + +
+
+ {filteredProjects + .slice() + .sort((a, b) => b.updated_at - a.updated_at) + .map((folder) => { + const ProjectItem = () => { + const [openDropdown, setOpenDropdown] = useState(false) + const isProjectActive = + currentPath === `/project/${folder.id}` + + return ( +
+
+ + isSmallScreen && setLeftPanel(false) + } + className="py-1 pr-2 truncate flex items-center gap-2 flex-1" + > + + + {folder.name} + + +
+ setOpenDropdown(open)} + > + + { + e.preventDefault() + e.stopPropagation() + }} + /> + + + { + e.stopPropagation() + setEditingProjectKey(folder.id) + setProjectDialogOpen(true) + }} + > + + Edit + + { + e.stopPropagation() + handleProjectDelete(folder.id) + }} + > + + Delete + + + +
+
+
+ ) + } + + return + })} +
+
+ )} + +
+
{favoritedThreads.length > 0 && ( <> @@ -397,7 +590,7 @@ const LeftPanel = () => { <>
- +
{t('common:noThreadsYet')}
@@ -414,59 +607,36 @@ const LeftPanel = () => {
+ {PlatformFeatures[PlatformFeature.AUTHENTICATION] && ( +
+
+
+ {isAuthenticated ? : } +
+
+ )} + +
- -
- {mainMenus.map((menu) => { - if (!menu.isEnabled) { - return null - } - - // Handle authentication menu specially - if (menu.title === 'common:authentication') { - return ( -
-
- {isAuthenticated ? ( - - ) : ( - - )} -
- ) - } - - // Regular menu items must have route and icon - if (!menu.route || !menu.icon) return null - - const isActive = - currentPath.includes(route.settings.index) && - menu.route.includes(route.settings.index) - return ( - isSmallScreen && setLeftPanel(false)} - data-test-id={`menu-${menu.title}`} - className={cn( - 'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded', - isActive - ? 'bg-left-panel-fg/10' - : '[&.active]:bg-left-panel-fg/10' - )} - > - - - {t(menu.title)} - - - ) - })} -
- -
+ + {/* Project Dialogs */} + + ) } diff --git a/web-app/src/containers/ThreadList.tsx b/web-app/src/containers/ThreadList.tsx index 672fc3ebc..40f0e0216 100644 --- a/web-app/src/containers/ThreadList.tsx +++ b/web-app/src/containers/ThreadList.tsx @@ -16,9 +16,13 @@ import { IconDots, IconStarFilled, IconStar, + IconFolder, + IconX, } from '@tabler/icons-react' import { useThreads } from '@/hooks/useThreads' +import { useThreadManagement } from '@/hooks/useThreadManagement' import { useLeftPanel } from '@/hooks/useLeftPanel' +import { useMessages } from '@/hooks/useMessages' import { cn } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' @@ -28,147 +32,268 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, } from '@/components/ui/dropdown-menu' import { useTranslation } from '@/i18n/react-i18next-compat' import { memo, useMemo, useState } from 'react' import { useNavigate, useMatches } from '@tanstack/react-router' import { RenameThreadDialog, DeleteThreadDialog } from '@/containers/dialogs' import { route } from '@/constants/routes' +import { toast } from 'sonner' -const SortableItem = memo(({ thread }: { thread: Thread }) => { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: thread.id, disabled: true }) +const SortableItem = memo( + ({ + thread, + variant, + }: { + thread: Thread + variant?: 'default' | 'project' + }) => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: thread.id, disabled: true }) - const isSmallScreen = useSmallScreen() - const setLeftPanel = useLeftPanel(state => state.setLeftPanel) + const isSmallScreen = useSmallScreen() + const setLeftPanel = useLeftPanel((state) => state.setLeftPanel) - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - } - const toggleFavorite = useThreads((state) => state.toggleFavorite) - const deleteThread = useThreads((state) => state.deleteThread) - const renameThread = useThreads((state) => state.renameThread) - const { t } = useTranslation() - const [openDropdown, setOpenDropdown] = useState(false) - const navigate = useNavigate() - // Check if current route matches this thread's detail page - const matches = useMatches() - const isActive = matches.some( - (match) => - match.routeId === '/threads/$threadId' && - 'threadId' in match.params && - match.params.threadId === thread.id - ) + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + } + const toggleFavorite = useThreads((state) => state.toggleFavorite) + const deleteThread = useThreads((state) => state.deleteThread) + const renameThread = useThreads((state) => state.renameThread) + const updateThread = useThreads((state) => state.updateThread) + const getFolderById = useThreadManagement().getFolderById + const { folders } = useThreadManagement() + const getMessages = useMessages((state) => state.getMessages) + const { t } = useTranslation() + const [openDropdown, setOpenDropdown] = useState(false) + const navigate = useNavigate() + // Check if current route matches this thread's detail page + const matches = useMatches() + const isActive = matches.some( + (match) => + match.routeId === '/threads/$threadId' && + 'threadId' in match.params && + match.params.threadId === thread.id + ) - const handleClick = () => { - if (!isDragging) { - // Only close panel and navigate if the thread is not already active - if (!isActive) { - if (isSmallScreen) setLeftPanel(false) - navigate({ to: route.threadsDetail, params: { threadId: thread.id } }) + const handleClick = () => { + if (!isDragging) { + // Only close panel and navigate if the thread is not already active + if (!isActive) { + if (isSmallScreen) setLeftPanel(false) + navigate({ to: route.threadsDetail, params: { threadId: thread.id } }) + } } } - } - const plainTitleForRename = useMemo(() => { - // Basic HTML stripping for simple span tags. - // If thread.title is undefined or null, treat as empty string before replace. - return (thread.title || '').replace(/]*>|<\/span>/g, '') - }, [thread.title]) + const plainTitleForRename = useMemo(() => { + // Basic HTML stripping for simple span tags. + // If thread.title is undefined or null, treat as empty string before replace. + return (thread.title || '').replace(/]*>|<\/span>/g, '') + }, [thread.title]) + const assignThreadToProject = (threadId: string, projectId: string) => { + const project = getFolderById(projectId) + if (project && updateThread) { + const projectMetadata = { + id: project.id, + name: project.name, + updated_at: project.updated_at, + } - return ( -
{ - e.preventDefault() - e.stopPropagation() - setOpenDropdown(true) - }} - className={cn( - 'mb-1 rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all', - isDragging ? 'cursor-move' : 'cursor-pointer', - isActive && 'bg-left-panel-fg/10' - )} - > -
- {thread.title || t('common:newThread')} -
-
- setOpenDropdown(open)} + updateThread(threadId, { + metadata: { + ...thread.metadata, + project: projectMetadata, + }, + }) + + toast.success(`Thread assigned to "${project.name}" successfully`) + } + } + + const getLastMessageInfo = useMemo(() => { + const messages = getMessages(thread.id) + if (messages.length === 0) return null + + const lastMessage = messages[messages.length - 1] + return { + date: new Date(lastMessage.created_at || 0), + content: lastMessage.content?.[0]?.text?.value || '', + } + }, [getMessages, thread.id]) + + return ( +
{ + e.preventDefault() + e.stopPropagation() + setOpenDropdown(true) + }} + className={cn( + 'rounded hover:bg-left-panel-fg/10 flex items-center justify-between gap-2 px-1.5 group/thread-list transition-all', + variant === 'project' + ? 'mb-2 rounded-lg px-4 border border-main-view-fg/10 bg-main-view-fg/5' + : 'mb-1', + isDragging ? 'cursor-move' : 'cursor-pointer', + isActive && 'bg-left-panel-fg/10' + )} + > +
- - { - e.preventDefault() - e.stopPropagation() - }} - /> - - - {thread.isFavorite ? ( - {thread.title || t('common:newThread')} + {variant === 'project' && ( + <> + {variant === 'project' && getLastMessageInfo?.content && ( +
+ {getLastMessageInfo.content} +
+ )} + + )} +
+
+ setOpenDropdown(open)} + > + + { + e.preventDefault() e.stopPropagation() - toggleFavorite(thread.id) }} - > - - {t('common:unstar')} - - ) : ( - { - e.stopPropagation() - toggleFavorite(thread.id) - }} - > - - {t('common:star')} - - )} - setOpenDropdown(false)} - /> + /> + + + {thread.isFavorite ? ( + { + e.stopPropagation() + toggleFavorite(thread.id) + }} + > + + {t('common:unstar')} + + ) : ( + { + e.stopPropagation() + toggleFavorite(thread.id) + }} + > + + {t('common:star')} + + )} + setOpenDropdown(false)} + /> - - setOpenDropdown(false)} - /> - - + + + + Add to project + + + {folders.length === 0 ? ( + + + No projects available + + + ) : ( + folders + .sort((a, b) => b.updated_at - a.updated_at) + .map((folder) => ( + { + e.stopPropagation() + assignThreadToProject(thread.id, folder.id) + }} + > + + + {folder.name} + + + )) + )} + {thread.metadata?.project && ( + <> + + { + e.stopPropagation() + // Remove project from metadata + const projectName = thread.metadata?.project?.name + updateThread(thread.id, { + metadata: { + ...thread.metadata, + project: undefined, + }, + }) + toast.success( + `Thread removed from "${projectName}" successfully` + ) + }} + > + + Remove from project + + + )} + + + + setOpenDropdown(false)} + /> + + +
-
- ) -}) + ) + } +) type ThreadListProps = { threads: Thread[] isFavoriteSection?: boolean + variant?: 'default' | 'project' + showDate?: boolean } -function ThreadList({ threads }: ThreadListProps) { +function ThreadList({ threads, variant = 'default' }: ThreadListProps) { const sortedThreads = useMemo(() => { return threads.sort((a, b) => { return (b.updated || 0) - (a.updated || 0) @@ -192,7 +317,7 @@ function ThreadList({ threads }: ThreadListProps) { strategy={verticalListSortingStrategy} > {sortedThreads.map((thread, index) => ( - + ))} diff --git a/web-app/src/containers/dialogs/AddProjectDialog.tsx b/web-app/src/containers/dialogs/AddProjectDialog.tsx new file mode 100644 index 000000000..f0fda648c --- /dev/null +++ b/web-app/src/containers/dialogs/AddProjectDialog.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { useThreadManagement } from '@/hooks/useThreadManagement' +import { toast } from 'sonner' +import { useTranslation } from '@/i18n/react-i18next-compat' + +interface AddProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + editingKey: string | null + initialData?: { + id: string + name: string + updated_at: number + } + onSave: (name: string) => void +} + +export default function AddProjectDialog({ + open, + onOpenChange, + editingKey, + initialData, + onSave, +}: AddProjectDialogProps) { + const { t } = useTranslation() + const [name, setName] = useState(initialData?.name || '') + const { folders } = useThreadManagement() + + useEffect(() => { + if (open) { + setName(initialData?.name || '') + } + }, [open, initialData]) + + const handleSave = () => { + if (!name.trim()) return + + const trimmedName = name.trim() + + // Check for duplicate names (excluding current project when editing) + const isDuplicate = folders.some( + (folder) => + folder.name.toLowerCase() === trimmedName.toLowerCase() && + folder.id !== editingKey + ) + + if (isDuplicate) { + toast.warning(t('projects.addProjectDialog.alreadyExists', { projectName: trimmedName })) + return + } + + onSave(trimmedName) + + // Show detailed success message + if (editingKey && initialData) { + toast.success( + t('projects.addProjectDialog.renameSuccess', { + oldName: initialData.name, + newName: trimmedName + }) + ) + } else { + toast.success(t('projects.addProjectDialog.createSuccess', { projectName: trimmedName })) + } + + setName('') + } + + const handleCancel = () => { + onOpenChange(false) + setName('') + } + + // Check if the button should be disabled + const isButtonDisabled = + !name.trim() || (editingKey && name.trim() === initialData?.name) + + return ( + + + + + {editingKey ? t('projects.addProjectDialog.editTitle') : t('projects.addProjectDialog.createTitle')} + + +
+
+ + setName(e.target.value)} + placeholder={t('projects.addProjectDialog.namePlaceholder')} + className="mt-1" + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter' && !isButtonDisabled) { + handleSave() + } + }} + /> +
+
+ + + + +
+
+ ) +} diff --git a/web-app/src/containers/dialogs/DeleteProjectDialog.tsx b/web-app/src/containers/dialogs/DeleteProjectDialog.tsx new file mode 100644 index 000000000..f8c86a3b4 --- /dev/null +++ b/web-app/src/containers/dialogs/DeleteProjectDialog.tsx @@ -0,0 +1,85 @@ +import { useRef } from 'react' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { toast } from 'sonner' +import { useTranslation } from '@/i18n/react-i18next-compat' + +interface DeleteProjectDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + onConfirm: () => void + projectName?: string +} + +export function DeleteProjectDialog({ + open, + onOpenChange, + onConfirm, + projectName, +}: DeleteProjectDialogProps) { + const { t } = useTranslation() + const deleteButtonRef = useRef(null) + + const handleConfirm = () => { + try { + onConfirm() + toast.success( + projectName + ? t('projects.deleteProjectDialog.successWithName', { projectName }) + : t('projects.deleteProjectDialog.successWithoutName') + ) + onOpenChange(false) + } catch (error) { + toast.error(t('projects.deleteProjectDialog.error')) + console.error('Delete project error:', error) + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleConfirm() + } + } + + return ( + + { + e.preventDefault() + deleteButtonRef.current?.focus() + }} + > + + {t('projects.deleteProjectDialog.title')} + + {t('projects.deleteProjectDialog.description')} + + + + + + + + + ) +} diff --git a/web-app/src/containers/dialogs/index.ts b/web-app/src/containers/dialogs/index.ts index b3c640200..3f96e5d17 100644 --- a/web-app/src/containers/dialogs/index.ts +++ b/web-app/src/containers/dialogs/index.ts @@ -6,4 +6,5 @@ export { MessageMetadataDialog } from './MessageMetadataDialog' export { DeleteMessageDialog } from './DeleteMessageDialog' export { FactoryResetDialog } from './FactoryResetDialog' export { DeleteAssistantDialog } from './DeleteAssistantDialog' -export { AddProviderDialog } from './AddProviderDialog' \ No newline at end of file +export { DeleteProjectDialog } from './DeleteProjectDialog' +export { AddProviderDialog } from './AddProviderDialog' diff --git a/web-app/src/hooks/useThreadManagement.ts b/web-app/src/hooks/useThreadManagement.ts new file mode 100644 index 000000000..84e5b0e34 --- /dev/null +++ b/web-app/src/hooks/useThreadManagement.ts @@ -0,0 +1,82 @@ +import { create } from 'zustand' +import { persist, createJSONStorage } from 'zustand/middleware' +import { ulid } from 'ulidx' +import { localStorageKey } from '@/constants/localStorage' +import { useThreads } from '@/hooks/useThreads' + +type ThreadFolder = { + id: string + name: string + updated_at: number +} + +type ThreadManagementState = { + folders: ThreadFolder[] + setFolders: (folders: ThreadFolder[]) => void + addFolder: (name: string) => void + updateFolder: (id: string, name: string) => void + deleteFolder: (id: string) => void + getFolderById: (id: string) => ThreadFolder | undefined +} + +export const useThreadManagement = create()( + persist( + (set, get) => ({ + folders: [], + + setFolders: (folders) => { + set({ folders }) + }, + + addFolder: (name) => { + const newFolder: ThreadFolder = { + id: ulid(), + name, + updated_at: Date.now(), + } + set((state) => ({ + folders: [...state.folders, newFolder], + })) + }, + + updateFolder: (id, name) => { + set((state) => ({ + folders: state.folders.map((folder) => + folder.id === id + ? { ...folder, name, updated_at: Date.now() } + : folder + ), + })) + }, + + deleteFolder: (id) => { + // Remove project metadata from all threads that belong to this project + const threadsState = useThreads.getState() + const threadsToUpdate = Object.values(threadsState.threads).filter( + (thread) => thread.metadata?.project?.id === id + ) + + threadsToUpdate.forEach((thread) => { + threadsState.updateThread(thread.id, { + metadata: { + ...thread.metadata, + project: undefined, + }, + }) + }) + + set((state) => ({ + folders: state.folders.filter((folder) => folder.id !== id), + })) + }, + + getFolderById: (id) => { + return get().folders.find((folder) => folder.id === id) + }, + }), + { + name: localStorageKey.threadManagement, + storage: createJSONStorage(() => localStorage), + } + ) +) diff --git a/web-app/src/hooks/useThreads.ts b/web-app/src/hooks/useThreads.ts index cce11c027..b450874cd 100644 --- a/web-app/src/hooks/useThreads.ts +++ b/web-app/src/hooks/useThreads.ts @@ -20,12 +20,14 @@ type ThreadState = { createThread: ( model: ThreadModel, title?: string, - assistant?: Assistant + assistant?: Assistant, + projectMetadata?: { id: string; name: string; updated_at: number } ) => Promise updateCurrentThreadModel: (model: ThreadModel) => void getFilteredThreads: (searchTerm: string) => Thread[] updateCurrentThreadAssistant: (assistant: Assistant) => void updateThreadTimestamp: (threadId: string) => void + updateThread: (threadId: string, updates: Partial) => void searchIndex: Fzf | null } @@ -132,20 +134,28 @@ export const useThreads = create()((set, get) => ({ deleteAllThreads: () => { set((state) => { const allThreadIds = Object.keys(state.threads) - const favoriteThreadIds = allThreadIds.filter( - (threadId) => state.threads[threadId].isFavorite - ) - const nonFavoriteThreadIds = allThreadIds.filter( - (threadId) => !state.threads[threadId].isFavorite + + // Identify threads to keep (favorites OR have project metadata) + const threadsToKeepIds = allThreadIds.filter( + (threadId) => + state.threads[threadId].isFavorite || + state.threads[threadId].metadata?.project ) - // Only delete non-favorite threads - nonFavoriteThreadIds.forEach((threadId) => { + // Identify threads to delete (non-favorites AND no project metadata) + const threadsToDeleteIds = allThreadIds.filter( + (threadId) => + !state.threads[threadId].isFavorite && + !state.threads[threadId].metadata?.project + ) + + // Delete threads that are not favorites and not in projects + threadsToDeleteIds.forEach((threadId) => { getServiceHub().threads().deleteThread(threadId) }) - // Keep only favorite threads - const remainingThreads = favoriteThreadIds.reduce( + // Keep favorite threads and threads with project metadata + const remainingThreads = threadsToKeepIds.reduce( (acc, threadId) => { acc[threadId] = state.threads[threadId] return acc @@ -208,13 +218,18 @@ export const useThreads = create()((set, get) => ({ setCurrentThreadId: (threadId) => { if (threadId !== get().currentThreadId) set({ currentThreadId: threadId }) }, - createThread: async (model, title, assistant) => { + createThread: async (model, title, assistant, projectMetadata) => { const newThread: Thread = { id: ulid(), title: title ?? 'New Thread', model, updated: Date.now() / 1000, assistants: assistant ? [assistant] : [], + ...(projectMetadata && { + metadata: { + project: projectMetadata, + }, + }), } return await getServiceHub() .threads() @@ -328,4 +343,26 @@ export const useThreads = create()((set, get) => ({ } }) }, + updateThread: (threadId, updates) => { + set((state) => { + const thread = state.threads[threadId] + if (!thread) return state + + const updatedThread = { + ...thread, + ...updates, + updated: Date.now() / 1000, + } + + getServiceHub().threads().updateThread(updatedThread) + + const newThreads = { ...state.threads, [threadId]: updatedThread } + return { + threads: newThreads, + searchIndex: new Fzf(Object.values(newThreads), { + selector: (item: Thread) => item.title, + }), + } + }) + }, })) diff --git a/web-app/src/locales/de-DE/common.json b/web-app/src/locales/de-DE/common.json index c0a55e1d9..4ce743b46 100644 --- a/web-app/src/locales/de-DE/common.json +++ b/web-app/src/locales/de-DE/common.json @@ -117,6 +117,7 @@ "chatInput": "Frage mich etwas..." }, "confirm": "Bestätige", + "continue": "Weiter", "loading": "Lade...", "error": "Fehler", "success": "Erfolg", @@ -127,6 +128,7 @@ "createAssistant": "Assistenten anlegen", "enterApiKey": "API Key eingeben", "scrollToBottom": "Zum Ende scrollen", + "generateAiResponse": "KI-Antwort generieren", "addModel": { "title": "Modell hinzufügen", "modelId": "Modell ID", @@ -154,12 +156,12 @@ "delete": "Löschen" }, "editJson": { - "errorParse": "Failed to parse JSON", - "errorPaste": "Failed to paste JSON", - "errorFormat": "Invalid JSON format", - "titleAll": "Edit All Servers Configuration", - "placeholder": "Enter JSON configuration...", - "save": "Save" + "errorParse": "JSON-Parsing fehlgeschlagen", + "errorPaste": "JSON-Einfügen fehlgeschlagen", + "errorFormat": "Ungültiges JSON-Format", + "titleAll": "Alle Serverkonfigurationen bearbeiten", + "placeholder": "JSON-Konfiguration eingeben...", + "save": "Speichern" }, "editModel": { "title": "Modell bearbeiten: {{modelId}}", @@ -228,11 +230,85 @@ "title": "Nachricht Metadaten" } }, + "projects": { + "title": "Projekte", + "addProject": "Projekt hinzufügen", + "addToProject": "Zum Projekt hinzufügen", + "removeFromProject": "Vom Projekt entfernen", + "createNewProject": "Neues Projekt erstellen", + "editProject": "Projekt bearbeiten", + "deleteProject": "Projekt löschen", + "projectName": "Projektname", + "enterProjectName": "Projektname eingeben...", + "noProjectsAvailable": "Keine Projekte verfügbar", + "noProjectsYet": "Noch keine Projekte", + "noProjectsYetDesc": "Starten Sie ein neues Projekt, indem Sie auf die Schaltfläche Projekt hinzufügen klicken.", + "projectNotFound": "Projekt nicht gefunden", + "projectNotFoundDesc": "Das gesuchte Projekt existiert nicht oder wurde gelöscht.", + "deleteProjectDialog": { + "title": "Projekt löschen", + "description": "Sind Sie sicher, dass Sie dieses Projekt löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteButton": "Löschen", + "successWithName": "Projekt \"{{projectName}}\" erfolgreich gelöscht", + "successWithoutName": "Projekt erfolgreich gelöscht", + "error": "Projekt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", + "ariaLabel": "{{projectName}} löschen" + }, + "addProjectDialog": { + "createTitle": "Neues Projekt erstellen", + "editTitle": "Projekt bearbeiten", + "nameLabel": "Projektname", + "namePlaceholder": "Projektname eingeben...", + "createButton": "Erstellen", + "updateButton": "Aktualisieren", + "alreadyExists": "Projekt \"{{projectName}}\" existiert bereits", + "createSuccess": "Projekt \"{{projectName}}\" erfolgreich erstellt", + "renameSuccess": "Projekt von \"{{oldName}}\" zu \"{{newName}}\" umbenannt" + }, + "noConversationsIn": "Keine Gespräche in {{projectName}}", + "startNewConversation": "Starten Sie ein neues Gespräch mit {{projectName}} unten", + "conversationsIn": "Gespräche in {{projectName}}", + "conversationsDescription": "Klicken Sie auf ein Gespräch, um weiterzuchatten, oder starten Sie unten ein neues.", + "thread": "Thread", + "threads": "Threads", + "updated": "Aktualisiert:", + "collapseThreads": "Threads einklappen", + "expandThreads": "Threads ausklappen", + "update": "Aktualisieren" + }, "toast": { "allThreadsUnfavorited": { "title": "Alle Threads De-Favorisieren ", "description": "Alle deine Threads wurden defavorisiert." }, + "projectCreated": { + "title": "Projekt erstellt", + "description": "Projekt \"{{projectName}}\" erfolgreich erstellt" + }, + "projectRenamed": { + "title": "Projekt umbenannt", + "description": "Projekt von \"{{oldName}}\" zu \"{{newName}}\" umbenannt" + }, + "projectDeleted": { + "title": "Projekt gelöscht", + "description": "Projekt \"{{projectName}}\" erfolgreich gelöscht" + }, + "projectAlreadyExists": { + "title": "Projekt existiert bereits", + "description": "Projekt \"{{projectName}}\" existiert bereits" + }, + "projectDeleteFailed": { + "title": "Löschen fehlgeschlagen", + "description": "Projekt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut." + }, + "threadAssignedToProject": { + "title": "Thread zugewiesen", + "description": "Thread erfolgreich zu \"{{projectName}}\" hinzugefügt" + }, + "threadRemovedFromProject": { + "title": "Thread entfernt", + "description": "Thread erfolgreich von \"{{projectName}}\" entfernt" + }, "deleteAllThreads": { "title": "Alle Threads löschen", "description": "Alle deine Threads wurden permanent gelöscht." @@ -280,6 +356,80 @@ "downloadAndVerificationComplete": { "title": "Download abgeschlossen", "description": "Modell \"{{item}}\" erfolgreich heruntergeladen und verifiziert" + }, + "projectCreated": { + "title": "Projekt erstellt", + "description": "Projekt \"{{projectName}}\" erfolgreich erstellt" + }, + "projectRenamed": { + "title": "Projekt umbenannt", + "description": "Projekt von \"{{oldName}}\" zu \"{{newName}}\" umbenannt" + }, + "projectDeleted": { + "title": "Projekt gelöscht", + "description": "Projekt \"{{projectName}}\" erfolgreich gelöscht" + }, + "projectAlreadyExists": { + "title": "Projekt existiert bereits", + "description": "Projekt \"{{projectName}}\" existiert bereits" + }, + "projectDeleteFailed": { + "title": "Löschen fehlgeschlagen", + "description": "Projekt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut." + }, + "threadAssignedToProject": { + "title": "Thread zugewiesen", + "description": "Thread erfolgreich zu \"{{projectName}}\" hinzugefügt" + }, + "threadRemovedFromProject": { + "title": "Thread entfernt", + "description": "Thread erfolgreich von \"{{projectName}}\" entfernt" } + }, + "projects": { + "title": "Projekte", + "addProject": "Projekt hinzufügen", + "addToProject": "Zu Projekt hinzufügen", + "removeFromProject": "Von Projekt entfernen", + "createNewProject": "Neues Projekt erstellen", + "editProject": "Projekt bearbeiten", + "deleteProject": "Projekt löschen", + "projectName": "Projektname", + "enterProjectName": "Projektname eingeben...", + "noProjectsAvailable": "Keine Projekte verfügbar", + "noProjectsYet": "Noch keine Projekte", + "noProjectsYetDesc": "Starten Sie ein neues Projekt, indem Sie auf die Schaltfläche Projekt hinzufügen klicken.", + "projectNotFound": "Projekt nicht gefunden", + "projectNotFoundDesc": "Das gesuchte Projekt existiert nicht oder wurde gelöscht.", + "deleteProjectDialog": { + "title": "Projekt löschen", + "description": "Sind Sie sicher, dass Sie dieses Projekt löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "deleteButton": "Löschen", + "successWithName": "Projekt \"{{projectName}}\" erfolgreich gelöscht", + "successWithoutName": "Projekt erfolgreich gelöscht", + "error": "Projekt konnte nicht gelöscht werden. Bitte versuchen Sie es erneut.", + "ariaLabel": "{{projectName}} löschen" + }, + "addProjectDialog": { + "createTitle": "Neues Projekt erstellen", + "editTitle": "Projekt bearbeiten", + "nameLabel": "Projektname", + "namePlaceholder": "Projektname eingeben...", + "createButton": "Erstellen", + "updateButton": "Aktualisieren", + "alreadyExists": "Projekt \"{{projectName}}\" existiert bereits", + "createSuccess": "Projekt \"{{projectName}}\" erfolgreich erstellt", + "renameSuccess": "Projekt von \"{{oldName}}\" zu \"{{newName}}\" umbenannt" + }, + "noConversationsIn": "Keine Gespräche in {{projectName}}", + "startNewConversation": "Starten Sie ein neues Gespräch mit {{projectName}} unten", + "conversationsIn": "Gespräche in {{projectName}}", + "conversationsDescription": "Klicken Sie auf ein Gespräch, um weiterzuchatten, oder starten Sie unten ein neues.", + "thread": "Thread", + "threads": "Threads", + "updated": "Aktualisiert:", + "collapseThreads": "Threads einklappen", + "expandThreads": "Threads ausklappen", + "update": "Aktualisieren" } } diff --git a/web-app/src/locales/en/common.json b/web-app/src/locales/en/common.json index ce6628000..c829dbdf8 100644 --- a/web-app/src/locales/en/common.json +++ b/web-app/src/locales/en/common.json @@ -119,6 +119,7 @@ "chatInput": "Ask me anything..." }, "confirm": "Confirm", + "continue": "Continue", "loading": "Loading...", "error": "Error", "success": "Success", @@ -231,6 +232,52 @@ "title": "Message Metadata" } }, + "projects": { + "title": "Projects", + "addProject": "Add Project", + "addToProject": "Add to project", + "removeFromProject": "Remove from project", + "createNewProject": "Create New Project", + "editProject": "Edit Project", + "deleteProject": "Delete Project", + "projectName": "Project Name", + "enterProjectName": "Enter project name...", + "noProjectsAvailable": "No projects available", + "noProjectsYet": "No Projects Yet", + "noProjectsYetDesc": "Start a new project by clicking the Add Project button.", + "projectNotFound": "Project Not Found", + "projectNotFoundDesc": "The project you're looking for doesn't exist or has been deleted.", + "deleteProjectDialog": { + "title": "Delete Project", + "description": "Are you sure you want to delete this project? This action cannot be undone.", + "deleteButton": "Delete", + "successWithName": "Project \"{{projectName}}\" deleted successfully", + "successWithoutName": "Project deleted successfully", + "error": "Failed to delete project. Please try again.", + "ariaLabel": "Delete {{projectName}}" + }, + "addProjectDialog": { + "createTitle": "Create New Project", + "editTitle": "Edit Project", + "nameLabel": "Project Name", + "namePlaceholder": "Enter project name...", + "createButton": "Create", + "updateButton": "Update", + "alreadyExists": "Project \"{{projectName}}\" already exists", + "createSuccess": "Project \"{{projectName}}\" created successfully", + "renameSuccess": "Project renamed from \"{{oldName}}\" to \"{{newName}}\"" + }, + "noConversationsIn": "No Conversations in {{projectName}}", + "startNewConversation": "Start a new conversation with {{projectName}} below", + "conversationsIn": "Conversations in {{projectName}}", + "conversationsDescription": "Click on any conversation to continue chatting, or start a new one below.", + "thread": "thread", + "threads": "threads", + "updated": "Updated:", + "collapseThreads": "Collapse threads", + "expandThreads": "Expand threads", + "update": "Update" + }, "toast": { "allThreadsUnfavorited": { "title": "All Threads Unfavorited", @@ -283,6 +330,34 @@ "downloadAndVerificationComplete": { "title": "Download Complete", "description": "Model \"{{item}}\" downloaded and verified successfully" + }, + "projectCreated": { + "title": "Project Created", + "description": "Project \"{{projectName}}\" created successfully" + }, + "projectRenamed": { + "title": "Project Renamed", + "description": "Project renamed from \"{{oldName}}\" to \"{{newName}}\"" + }, + "projectDeleted": { + "title": "Project Deleted", + "description": "Project \"{{projectName}}\" deleted successfully" + }, + "projectAlreadyExists": { + "title": "Project Already Exists", + "description": "Project \"{{projectName}}\" already exists" + }, + "projectDeleteFailed": { + "title": "Delete Failed", + "description": "Failed to delete project. Please try again." + }, + "threadAssignedToProject": { + "title": "Thread Assigned", + "description": "Thread assigned to \"{{projectName}}\" successfully" + }, + "threadRemovedFromProject": { + "title": "Thread Removed", + "description": "Thread removed from \"{{projectName}}\" successfully" } } } \ No newline at end of file diff --git a/web-app/src/locales/id/common.json b/web-app/src/locales/id/common.json index c1f9838c6..aa0c83fd9 100644 --- a/web-app/src/locales/id/common.json +++ b/web-app/src/locales/id/common.json @@ -117,6 +117,7 @@ "chatInput": "Tanyakan apa saja padaku..." }, "confirm": "Konfirmasi", + "continue": "Lanjutkan", "loading": "Memuat...", "error": "Kesalahan", "success": "Sukses", @@ -127,6 +128,7 @@ "createAssistant": "Buat Asisten", "enterApiKey": "Masukkan Kunci API", "scrollToBottom": "Gulir ke bawah", + "generateAiResponse": "Hasilkan Respons AI", "addModel": { "title": "Tambah Model", "modelId": "ID Model", @@ -170,6 +172,13 @@ "embeddings": "Embedding", "notAvailable": "Belum tersedia" }, + "outOfContextError": { + "truncateInput": "Potong Input", + "title": "Kesalahan konteks habis", + "description": "Obrolan ini mencapai batas memori AI, seperti papan tulis yang penuh. Kami dapat memperluas jendela memori (disebut ukuran konteks) agar lebih mengingat, tetapi mungkin akan menggunakan lebih banyak memori komputer Anda. Kami juga dapat memotong input, yang berarti akan melupakan sebagian riwayat obrolan untuk memberi ruang bagi pesan baru.", + "increaseContextSizeDescription": "Apakah Anda ingin meningkatkan ukuran konteks?", + "increaseContextSize": "Tingkatkan Ukuran Konteks" + }, "toolApproval": { "title": "Permintaan Izin Alat", "description": "Asisten ingin menggunakan {{toolName}}", @@ -273,6 +282,80 @@ "downloadAndVerificationComplete": { "title": "Unduhan Selesai", "description": "Model \"{{item}}\" berhasil diunduh dan diverifikasi" + }, + "projectCreated": { + "title": "Proyek Dibuat", + "description": "Proyek \"{{projectName}}\" berhasil dibuat" + }, + "projectRenamed": { + "title": "Proyek Diganti Nama", + "description": "Proyek diganti nama dari \"{{oldName}}\" ke \"{{newName}}\"" + }, + "projectDeleted": { + "title": "Proyek Dihapus", + "description": "Proyek \"{{projectName}}\" berhasil dihapus" + }, + "projectAlreadyExists": { + "title": "Proyek Sudah Ada", + "description": "Proyek \"{{projectName}}\" sudah ada" + }, + "projectDeleteFailed": { + "title": "Penghapusan Gagal", + "description": "Gagal menghapus proyek. Silakan coba lagi." + }, + "threadAssignedToProject": { + "title": "Thread Ditugaskan", + "description": "Thread berhasil ditugaskan ke \"{{projectName}}\"" + }, + "threadRemovedFromProject": { + "title": "Thread Dihapus", + "description": "Thread berhasil dihapus dari \"{{projectName}}\"" } + }, + "projects": { + "title": "Proyek", + "addProject": "Tambah Proyek", + "addToProject": "Tambahkan ke proyek", + "removeFromProject": "Hapus dari proyek", + "createNewProject": "Buat Proyek Baru", + "editProject": "Edit Proyek", + "deleteProject": "Hapus Proyek", + "projectName": "Nama Proyek", + "enterProjectName": "Masukkan nama proyek...", + "noProjectsAvailable": "Tidak ada proyek tersedia", + "noProjectsYet": "Belum Ada Proyek", + "noProjectsYetDesc": "Mulai proyek baru dengan mengklik tombol Tambah Proyek.", + "projectNotFound": "Proyek Tidak Ditemukan", + "projectNotFoundDesc": "Proyek yang Anda cari tidak ada atau telah dihapus.", + "deleteProjectDialog": { + "title": "Hapus Proyek", + "description": "Apakah Anda yakin ingin menghapus proyek ini? Tindakan ini tidak dapat dibatalkan.", + "deleteButton": "Hapus", + "successWithName": "Proyek \"{{projectName}}\" berhasil dihapus", + "successWithoutName": "Proyek berhasil dihapus", + "error": "Gagal menghapus proyek. Silakan coba lagi.", + "ariaLabel": "Hapus {{projectName}}" + }, + "addProjectDialog": { + "createTitle": "Buat Proyek Baru", + "editTitle": "Edit Proyek", + "nameLabel": "Nama Proyek", + "namePlaceholder": "Masukkan nama proyek...", + "createButton": "Buat", + "updateButton": "Perbarui", + "alreadyExists": "Proyek \"{{projectName}}\" sudah ada", + "createSuccess": "Proyek \"{{projectName}}\" berhasil dibuat", + "renameSuccess": "Proyek diubah dari \"{{oldName}}\" menjadi \"{{newName}}\"" + }, + "noConversationsIn": "Tidak Ada Percakapan di {{projectName}}", + "startNewConversation": "Mulai percakapan baru dengan {{projectName}} di bawah", + "conversationsIn": "Percakapan di {{projectName}}", + "conversationsDescription": "Klik percakapan mana pun untuk melanjutkan chatting, atau mulai yang baru di bawah.", + "thread": "utas", + "threads": "utas", + "updated": "Diperbarui:", + "collapseThreads": "Tutup utas", + "expandThreads": "Buka utas", + "update": "Perbarui" } } diff --git a/web-app/src/locales/pl/common.json b/web-app/src/locales/pl/common.json index 14fd6519e..ca6f6b6b7 100644 --- a/web-app/src/locales/pl/common.json +++ b/web-app/src/locales/pl/common.json @@ -117,6 +117,7 @@ "chatInput": "Zapytaj mnie o cokolwiek…" }, "confirm": "Potwierdź", + "continue": "Kontynuuj", "loading": "Wczytywanie…", "error": "Błąd", "success": "Sukces", @@ -229,6 +230,52 @@ "title": "Metadane Wiadomości" } }, + "projects": { + "title": "Projekty", + "addProject": "Dodaj Projekt", + "addToProject": "Dodaj do projektu", + "removeFromProject": "Usuń z projektu", + "createNewProject": "Utwórz Nowy Projekt", + "editProject": "Edytuj Projekt", + "deleteProject": "Usuń Projekt", + "projectName": "Nazwa Projektu", + "enterProjectName": "Wprowadź nazwę projektu...", + "noProjectsAvailable": "Brak dostępnych projektów", + "noProjectsYet": "Brak Projektów", + "noProjectsYetDesc": "Rozpocznij nowy projekt klikając przycisk Dodaj Projekt.", + "projectNotFound": "Projekt Nie Znaleziony", + "projectNotFoundDesc": "Projekt, którego szukasz nie istnieje lub został usunięty.", + "deleteProjectDialog": { + "title": "Usuń Projekt", + "description": "Na pewno chcesz usunąć ten projekt? Tej operacji nie można cofnąć.", + "deleteButton": "Usuń", + "successWithName": "Projekt \"{{projectName}}\" został pomyślnie usunięty", + "successWithoutName": "Projekt został pomyślnie usunięty", + "error": "Nie udało się usunąć projektu. Spróbuj ponownie.", + "ariaLabel": "Usuń {{projectName}}" + }, + "addProjectDialog": { + "createTitle": "Utwórz Nowy Projekt", + "editTitle": "Edytuj Projekt", + "nameLabel": "Nazwa Projektu", + "namePlaceholder": "Wprowadź nazwę projektu...", + "createButton": "Utwórz", + "updateButton": "Aktualizuj", + "alreadyExists": "Projekt \"{{projectName}}\" już istnieje", + "createSuccess": "Projekt \"{{projectName}}\" został pomyślnie utworzony", + "renameSuccess": "Projekt zmieniono z \"{{oldName}}\" na \"{{newName}}\"" + }, + "noConversationsIn": "Brak Rozmów w {{projectName}}", + "startNewConversation": "Rozpocznij nową rozmowę z {{projectName}} poniżej", + "conversationsIn": "Rozmowy w {{projectName}}", + "conversationsDescription": "Kliknij na dowolną rozmowę aby kontynuować czat, lub rozpocznij nową poniżej.", + "thread": "wątek", + "threads": "wątki", + "updated": "Zaktualizowano:", + "collapseThreads": "Zwiń wątki", + "expandThreads": "Rozwiń wątki", + "update": "Aktualizuj" + }, "toast": { "allThreadsUnfavorited": { "title": "Wszystkie Wątki Usunięte z Ulubionych", diff --git a/web-app/src/locales/vn/common.json b/web-app/src/locales/vn/common.json index 8a107a9a2..4c2d95101 100644 --- a/web-app/src/locales/vn/common.json +++ b/web-app/src/locales/vn/common.json @@ -117,6 +117,7 @@ "chatInput": "Hỏi tôi bất cứ điều gì..." }, "confirm": "Xác nhận", + "continue": "Tiếp tục", "loading": "Đang tải...", "error": "Lỗi", "success": "Thành công", diff --git a/web-app/src/locales/zh-CN/common.json b/web-app/src/locales/zh-CN/common.json index ccabb6071..6da4a83fa 100644 --- a/web-app/src/locales/zh-CN/common.json +++ b/web-app/src/locales/zh-CN/common.json @@ -117,6 +117,7 @@ "chatInput": "随便问我什么..." }, "confirm": "确认", + "continue": "继续", "loading": "加载中...", "error": "错误", "success": "成功", diff --git a/web-app/src/locales/zh-TW/common.json b/web-app/src/locales/zh-TW/common.json index cb0a60510..4b9d1e7f6 100644 --- a/web-app/src/locales/zh-TW/common.json +++ b/web-app/src/locales/zh-TW/common.json @@ -117,6 +117,7 @@ "chatInput": "問我任何事..." }, "confirm": "確認", + "continue": "繼續", "loading": "載入中...", "error": "錯誤", "success": "成功", diff --git a/web-app/src/routeTree.gen.ts b/web-app/src/routeTree.gen.ts index 4322b0fd1..0eb2bbf13 100644 --- a/web-app/src/routeTree.gen.ts +++ b/web-app/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as SystemMonitorImport } from './routes/system-monitor' import { Route as LogsImport } from './routes/logs' import { Route as AssistantImport } from './routes/assistant' import { Route as IndexImport } from './routes/index' +import { Route as ProjectIndexImport } from './routes/project/index' import { Route as HubIndexImport } from './routes/hub/index' import { Route as ThreadsThreadIdImport } from './routes/threads/$threadId' import { Route as SettingsShortcutsImport } from './routes/settings/shortcuts' @@ -26,6 +27,7 @@ import { Route as SettingsHardwareImport } from './routes/settings/hardware' import { Route as SettingsGeneralImport } from './routes/settings/general' import { Route as SettingsExtensionsImport } from './routes/settings/extensions' import { Route as SettingsAppearanceImport } from './routes/settings/appearance' +import { Route as ProjectProjectIdImport } from './routes/project/$projectId' import { Route as LocalApiServerLogsImport } from './routes/local-api-server/logs' import { Route as HubModelIdImport } from './routes/hub/$modelId' import { Route as SettingsProvidersIndexImport } from './routes/settings/providers/index' @@ -58,6 +60,12 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const ProjectIndexRoute = ProjectIndexImport.update({ + id: '/project/', + path: '/project/', + getParentRoute: () => rootRoute, +} as any) + const HubIndexRoute = HubIndexImport.update({ id: '/hub/', path: '/hub/', @@ -124,6 +132,12 @@ const SettingsAppearanceRoute = SettingsAppearanceImport.update({ getParentRoute: () => rootRoute, } as any) +const ProjectProjectIdRoute = ProjectProjectIdImport.update({ + id: '/project/$projectId', + path: '/project/$projectId', + getParentRoute: () => rootRoute, +} as any) + const LocalApiServerLogsRoute = LocalApiServerLogsImport.update({ id: '/local-api-server/logs', path: '/local-api-server/logs', @@ -201,6 +215,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof LocalApiServerLogsImport parentRoute: typeof rootRoute } + '/project/$projectId': { + id: '/project/$projectId' + path: '/project/$projectId' + fullPath: '/project/$projectId' + preLoaderRoute: typeof ProjectProjectIdImport + parentRoute: typeof rootRoute + } '/settings/appearance': { id: '/settings/appearance' path: '/settings/appearance' @@ -278,6 +299,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HubIndexImport parentRoute: typeof rootRoute } + '/project/': { + id: '/project/' + path: '/project' + fullPath: '/project' + preLoaderRoute: typeof ProjectIndexImport + parentRoute: typeof rootRoute + } '/auth/google/callback': { id: '/auth/google/callback' path: '/auth/google/callback' @@ -311,6 +339,7 @@ export interface FileRoutesByFullPath { '/system-monitor': typeof SystemMonitorRoute '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute + '/project/$projectId': typeof ProjectProjectIdRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute @@ -322,6 +351,7 @@ export interface FileRoutesByFullPath { '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute '/hub': typeof HubIndexRoute + '/project': typeof ProjectIndexRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers': typeof SettingsProvidersIndexRoute @@ -334,6 +364,7 @@ export interface FileRoutesByTo { '/system-monitor': typeof SystemMonitorRoute '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute + '/project/$projectId': typeof ProjectProjectIdRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute @@ -345,19 +376,21 @@ export interface FileRoutesByTo { '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute '/hub': typeof HubIndexRoute + '/project': typeof ProjectIndexRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers': typeof SettingsProvidersIndexRoute } export interface FileRoutesById { - __root__: typeof rootRoute + '__root__': typeof rootRoute '/': typeof IndexRoute '/assistant': typeof AssistantRoute '/logs': typeof LogsRoute '/system-monitor': typeof SystemMonitorRoute '/hub/$modelId': typeof HubModelIdRoute '/local-api-server/logs': typeof LocalApiServerLogsRoute + '/project/$projectId': typeof ProjectProjectIdRoute '/settings/appearance': typeof SettingsAppearanceRoute '/settings/extensions': typeof SettingsExtensionsRoute '/settings/general': typeof SettingsGeneralRoute @@ -369,6 +402,7 @@ export interface FileRoutesById { '/settings/shortcuts': typeof SettingsShortcutsRoute '/threads/$threadId': typeof ThreadsThreadIdRoute '/hub/': typeof HubIndexRoute + '/project/': typeof ProjectIndexRoute '/auth/google/callback': typeof AuthGoogleCallbackRoute '/settings/providers/$providerName': typeof SettingsProvidersProviderNameRoute '/settings/providers/': typeof SettingsProvidersIndexRoute @@ -383,6 +417,7 @@ export interface FileRouteTypes { | '/system-monitor' | '/hub/$modelId' | '/local-api-server/logs' + | '/project/$projectId' | '/settings/appearance' | '/settings/extensions' | '/settings/general' @@ -394,6 +429,7 @@ export interface FileRouteTypes { | '/settings/shortcuts' | '/threads/$threadId' | '/hub' + | '/project' | '/auth/google/callback' | '/settings/providers/$providerName' | '/settings/providers' @@ -405,6 +441,7 @@ export interface FileRouteTypes { | '/system-monitor' | '/hub/$modelId' | '/local-api-server/logs' + | '/project/$projectId' | '/settings/appearance' | '/settings/extensions' | '/settings/general' @@ -416,6 +453,7 @@ export interface FileRouteTypes { | '/settings/shortcuts' | '/threads/$threadId' | '/hub' + | '/project' | '/auth/google/callback' | '/settings/providers/$providerName' | '/settings/providers' @@ -427,6 +465,7 @@ export interface FileRouteTypes { | '/system-monitor' | '/hub/$modelId' | '/local-api-server/logs' + | '/project/$projectId' | '/settings/appearance' | '/settings/extensions' | '/settings/general' @@ -438,6 +477,7 @@ export interface FileRouteTypes { | '/settings/shortcuts' | '/threads/$threadId' | '/hub/' + | '/project/' | '/auth/google/callback' | '/settings/providers/$providerName' | '/settings/providers/' @@ -451,6 +491,7 @@ export interface RootRouteChildren { SystemMonitorRoute: typeof SystemMonitorRoute HubModelIdRoute: typeof HubModelIdRoute LocalApiServerLogsRoute: typeof LocalApiServerLogsRoute + ProjectProjectIdRoute: typeof ProjectProjectIdRoute SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsExtensionsRoute: typeof SettingsExtensionsRoute SettingsGeneralRoute: typeof SettingsGeneralRoute @@ -462,6 +503,7 @@ export interface RootRouteChildren { SettingsShortcutsRoute: typeof SettingsShortcutsRoute ThreadsThreadIdRoute: typeof ThreadsThreadIdRoute HubIndexRoute: typeof HubIndexRoute + ProjectIndexRoute: typeof ProjectIndexRoute AuthGoogleCallbackRoute: typeof AuthGoogleCallbackRoute SettingsProvidersProviderNameRoute: typeof SettingsProvidersProviderNameRoute SettingsProvidersIndexRoute: typeof SettingsProvidersIndexRoute @@ -474,6 +516,7 @@ const rootRouteChildren: RootRouteChildren = { SystemMonitorRoute: SystemMonitorRoute, HubModelIdRoute: HubModelIdRoute, LocalApiServerLogsRoute: LocalApiServerLogsRoute, + ProjectProjectIdRoute: ProjectProjectIdRoute, SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsExtensionsRoute: SettingsExtensionsRoute, SettingsGeneralRoute: SettingsGeneralRoute, @@ -485,6 +528,7 @@ const rootRouteChildren: RootRouteChildren = { SettingsShortcutsRoute: SettingsShortcutsRoute, ThreadsThreadIdRoute: ThreadsThreadIdRoute, HubIndexRoute: HubIndexRoute, + ProjectIndexRoute: ProjectIndexRoute, AuthGoogleCallbackRoute: AuthGoogleCallbackRoute, SettingsProvidersProviderNameRoute: SettingsProvidersProviderNameRoute, SettingsProvidersIndexRoute: SettingsProvidersIndexRoute, @@ -506,6 +550,7 @@ export const routeTree = rootRoute "/system-monitor", "/hub/$modelId", "/local-api-server/logs", + "/project/$projectId", "/settings/appearance", "/settings/extensions", "/settings/general", @@ -517,6 +562,7 @@ export const routeTree = rootRoute "/settings/shortcuts", "/threads/$threadId", "/hub/", + "/project/", "/auth/google/callback", "/settings/providers/$providerName", "/settings/providers/" @@ -540,6 +586,9 @@ export const routeTree = rootRoute "/local-api-server/logs": { "filePath": "local-api-server/logs.tsx" }, + "/project/$projectId": { + "filePath": "project/$projectId.tsx" + }, "/settings/appearance": { "filePath": "settings/appearance.tsx" }, @@ -573,6 +622,9 @@ export const routeTree = rootRoute "/hub/": { "filePath": "hub/index.tsx" }, + "/project/": { + "filePath": "project/index.tsx" + }, "/auth/google/callback": { "filePath": "auth.google.callback.tsx" }, diff --git a/web-app/src/routes/assistant.tsx b/web-app/src/routes/assistant.tsx index bf4fd928c..d96080f7d 100644 --- a/web-app/src/routes/assistant.tsx +++ b/web-app/src/routes/assistant.tsx @@ -12,6 +12,7 @@ import { AvatarEmoji } from '@/containers/AvatarEmoji' import { useTranslation } from '@/i18n/react-i18next-compat' import { PlatformGuard } from '@/lib/platform/PlatformGuard' import { PlatformFeature } from '@/lib/platform/types' +import { Button } from '@/components/ui/button' // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Route = createFileRoute(route.assistant as any)({ @@ -61,72 +62,71 @@ function AssistantContent() { return (
- {t('assistants:title')} +
+ {t('assistants:title')} + +
-
-
+
+
{assistants .slice() .sort((a, b) => a.created_at - b.created_at) .map((assistant) => (
-
-

-
- {assistant?.avatar && ( - - - - )} - {assistant.name} -
-

-
-
{ - setEditingKey(assistant.id) - setOpen(true) - }} - > - -
-
handleDelete(assistant.id)} - > - +
+ {assistant?.avatar && ( +
+
+ )} +
+

+ {assistant.name} +

+

+ {assistant.description} +

-

- {assistant.description} -

+
+ + +
))} - -
{ - setEditingKey(null) - setOpen(true) - }} - > - -
state.threads) + + const chatWidth = useAppearance((state) => state.chatWidth) + const isSmallScreen = useSmallScreen() + + // Find the project + const project = getFolderById(projectId) + + // Get threads for this project + const projectThreads = useMemo(() => { + return Object.values(threads) + .filter((thread) => thread.metadata?.project?.id === projectId) + .sort((a, b) => (b.updated || 0) - (a.updated || 0)) + }, [threads, projectId]) + + // Conditional to check if there are any valid providers + const hasValidProviders = providers.some( + (provider) => + provider.api_key?.length || + (provider.provider === 'llamacpp' && provider.models.length) || + (provider.provider === 'jan' && provider.models.length) + ) + + if (!hasValidProviders) { + return + } + + if (!project) { + return ( +
+
+

+ {t('projects.projectNotFound')} +

+

+ {t('projects.projectNotFoundDesc')} +

+
+
+ ) + } + + return ( +
+ +
+ {PlatformFeatures[PlatformFeature.ASSISTANTS] && ( + + )} +
+
+ +
+
+
+
+ {projectThreads.length > 0 && ( + <> +

+ {t('projects.conversationsIn', { projectName: project.name })} +

+

+ {t('projects.conversationsDescription')} +

+ + )} +
+ + {/* Thread List or Empty State */} +
+ {projectThreads.length > 0 ? ( + + ) : ( +
+ +

+ {t('projects.noConversationsIn', { projectName: project.name })} +

+

+ {t('projects.startNewConversation', { projectName: project.name })} +

+
+ )} +
+
+
+
+ {/* New Chat Input */} +
+ +
+
+ ) +} diff --git a/web-app/src/routes/project/index.tsx b/web-app/src/routes/project/index.tsx new file mode 100644 index 000000000..b4cbb6618 --- /dev/null +++ b/web-app/src/routes/project/index.tsx @@ -0,0 +1,244 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState, useMemo } from 'react' + +import { useThreadManagement } from '@/hooks/useThreadManagement' +import { useThreads } from '@/hooks/useThreads' +import { useTranslation } from '@/i18n/react-i18next-compat' + +import HeaderPage from '@/containers/HeaderPage' +import ThreadList from '@/containers/ThreadList' +import { + IconCirclePlus, + IconPencil, + IconTrash, + IconFolder, + IconChevronDown, + IconChevronRight, +} from '@tabler/icons-react' +import AddProjectDialog from '@/containers/dialogs/AddProjectDialog' +import { DeleteProjectDialog } from '@/containers/dialogs/DeleteProjectDialog' +import { Button } from '@/components/ui/button' + +import { formatDate } from '@/utils/formatDate' + +export const Route = createFileRoute('/project/')({ + component: Project, +}) + +function Project() { + return +} + +function ProjectContent() { + const { t } = useTranslation() + const { folders, addFolder, updateFolder, deleteFolder, getFolderById } = + useThreadManagement() + const threads = useThreads((state) => state.threads) + const [open, setOpen] = useState(false) + const [editingKey, setEditingKey] = useState(null) + const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) + const [deletingId, setDeletingId] = useState(null) + const [expandedProjects, setExpandedProjects] = useState>( + new Set() + ) + + const handleDelete = (id: string) => { + setDeletingId(id) + setDeleteConfirmOpen(true) + } + + const confirmDelete = () => { + if (deletingId) { + deleteFolder(deletingId) + setDeleteConfirmOpen(false) + setDeletingId(null) + } + } + + const handleSave = (name: string) => { + if (editingKey) { + updateFolder(editingKey, name) + } else { + addFolder(name) + } + setOpen(false) + setEditingKey(null) + } + + const formatProjectDate = (timestamp: number) => { + return formatDate(new Date(timestamp), { includeTime: false }) + } + + // Get threads for a specific project + const getThreadsForProject = useMemo(() => { + return (projectId: string) => { + return Object.values(threads) + .filter((thread) => thread.metadata?.project?.id === projectId) + .sort((a, b) => (b.updated || 0) - (a.updated || 0)) + } + }, [threads]) + + const toggleProjectExpansion = (projectId: string) => { + setExpandedProjects((prev) => { + const newSet = new Set(prev) + if (newSet.has(projectId)) { + newSet.delete(projectId) + } else { + newSet.add(projectId) + } + return newSet + }) + } + + return ( +
+ +
+ {t('projects.title')} + +
+
+
+
+ {folders.length === 0 ? ( +
+ +

+ {t('projects.noProjectsYet')} +

+

+ {t('projects.noProjectsYetDesc')} +

+
+ ) : ( +
+ {folders + .slice() + .sort((a, b) => a.updated_at - b.updated_at) + .map((folder) => { + const projectThreads = getThreadsForProject(folder.id) + const isExpanded = expandedProjects.has(folder.id) + + return ( +
+
+
+
+ +
+
+
+

+ {folder.name} +

+ + {projectThreads.length}{' '} + {projectThreads.length === 1 + ? t('projects.thread') + : t('projects.threads')} + +
+

+ {t('projects.updated')}{' '} + {formatProjectDate(folder.updated_at)} +

+
+
+
+ {projectThreads.length > 0 && ( + + )} + + +
+
+ + {/* Thread List */} + {isExpanded && projectThreads.length > 0 && ( +
+ +
+ )} +
+ ) + })} +
+ )} +
+
+ + +
+ ) +} diff --git a/web-app/src/services/threads/default.ts b/web-app/src/services/threads/default.ts index af5f213d5..72c66841a 100644 --- a/web-app/src/services/threads/default.ts +++ b/web-app/src/services/threads/default.ts @@ -30,6 +30,12 @@ export class DefaultThreadsService implements ThreadsService { provider: e.assistants?.[0]?.model?.engine, }, assistants: e.assistants ?? [defaultAssistant], + metadata: { + ...e.metadata, + // Override extracted fields to avoid duplication + order: e.metadata?.order, + is_favorite: e.metadata?.is_favorite, + }, } as Thread }) }) @@ -101,6 +107,7 @@ export class DefaultThreadsService implements ThreadsService { }, ], metadata: { + ...thread.metadata, is_favorite: thread.isFavorite, order: thread.order, }, @@ -115,4 +122,4 @@ export class DefaultThreadsService implements ThreadsService { .get(ExtensionTypeEnum.Conversational) ?.deleteThread(threadId) } -} \ No newline at end of file +} diff --git a/web-app/src/types/threads.d.ts b/web-app/src/types/threads.d.ts index 657b7e651..35238687a 100644 --- a/web-app/src/types/threads.d.ts +++ b/web-app/src/types/threads.d.ts @@ -44,6 +44,14 @@ type Thread = { model?: ThreadModel updated: number order?: number + metadata?: { + project?: { + id: string + name: string + updated_at: number + } + [key: string]: unknown + } } type Assistant = { @@ -62,4 +70,4 @@ type TokenSpeed = { tokenSpeed: number tokenCount: number lastTimestamp: number -} \ No newline at end of file +} From d0f62fa634eb1deef68c888203610f5ae1c90ae9 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 25 Sep 2025 10:18:21 +0700 Subject: [PATCH 27/56] chore: fix missing classname --- web-app/src/containers/SetupScreen.tsx | 2 +- web-app/src/routes/assistant.tsx | 2 +- web-app/src/routes/index.tsx | 2 +- web-app/src/routes/project/index.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx index 812ed6493..bce474836 100644 --- a/web-app/src/containers/SetupScreen.tsx +++ b/web-app/src/containers/SetupScreen.tsx @@ -18,7 +18,7 @@ function SetupScreen() { localStorage.getItem(localStorageKey.setupCompleted) === 'true' return ( -
+
diff --git a/web-app/src/routes/assistant.tsx b/web-app/src/routes/assistant.tsx index d96080f7d..dca4c93ef 100644 --- a/web-app/src/routes/assistant.tsx +++ b/web-app/src/routes/assistant.tsx @@ -60,7 +60,7 @@ function AssistantContent() { } return ( -
+
{t('assistants:title')} diff --git a/web-app/src/routes/index.tsx b/web-app/src/routes/index.tsx index 80bf065f2..b4b208b9d 100644 --- a/web-app/src/routes/index.tsx +++ b/web-app/src/routes/index.tsx @@ -52,7 +52,7 @@ function Index() { } return ( -
+
{PlatformFeatures[PlatformFeature.ASSISTANTS] && } diff --git a/web-app/src/routes/project/index.tsx b/web-app/src/routes/project/index.tsx index b4cbb6618..f609f7943 100644 --- a/web-app/src/routes/project/index.tsx +++ b/web-app/src/routes/project/index.tsx @@ -91,7 +91,7 @@ function ProjectContent() { } return ( -
+
{t('projects.title')} From b0bca2ac1f5e33cd8463e6b3c885d4be3a522796 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 25 Sep 2025 10:19:18 +0700 Subject: [PATCH 28/56] Update web-app/src/routes/project/index.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- web-app/src/routes/project/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-app/src/routes/project/index.tsx b/web-app/src/routes/project/index.tsx index f609f7943..fa8e1cf3a 100644 --- a/web-app/src/routes/project/index.tsx +++ b/web-app/src/routes/project/index.tsx @@ -124,7 +124,7 @@ function ProjectContent() {
{folders .slice() - .sort((a, b) => a.updated_at - b.updated_at) + .sort((a, b) => b.updated_at - a.updated_at) .map((folder) => { const projectThreads = getThreadsForProject(folder.id) const isExpanded = expandedProjects.has(folder.id) From 8205c3317646cb339726790b4b0dd1d7d074d7b3 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Thu, 25 Sep 2025 10:55:10 +0700 Subject: [PATCH 29/56] ci: update package version for tauri plugin --- ...emplate-tauri-build-linux-x64-external.yml | 27 +++++++++- ...template-tauri-build-linux-x64-flatpak.yml | 29 +++++++++-- .../template-tauri-build-linux-x64.yml | 29 +++++++++-- .../template-tauri-build-macos-external.yml | 52 ++++++++++--------- .../workflows/template-tauri-build-macos.yml | 23 ++++++++ ...plate-tauri-build-windows-x64-external.yml | 25 ++++++++- .../template-tauri-build-windows-x64.yml | 24 ++++++++- 7 files changed, 175 insertions(+), 34 deletions(-) diff --git a/.github/workflows/template-tauri-build-linux-x64-external.yml b/.github/workflows/template-tauri-build-linux-x64-external.yml index 59c14a3d6..83c19879f 100644 --- a/.github/workflows/template-tauri-build-linux-x64-external.yml +++ b/.github/workflows/template-tauri-build-linux-x64-external.yml @@ -79,8 +79,33 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json - ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/Cargo.toml---------" + cat ./src-tauri/Cargo.toml + + ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" if [ "${{ inputs.channel }}" != "stable" ]; then jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json diff --git a/.github/workflows/template-tauri-build-linux-x64-flatpak.yml b/.github/workflows/template-tauri-build-linux-x64-flatpak.yml index 2807a74ae..d8b374cdf 100644 --- a/.github/workflows/template-tauri-build-linux-x64-flatpak.yml +++ b/.github/workflows/template-tauri-build-linux-x64-flatpak.yml @@ -100,13 +100,36 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json - # Temporarily enable devtool on prod build - ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" - cat ./src-tauri/Cargo.toml + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/Cargo.toml---------" cat ./src-tauri/Cargo.toml + # Temporarily enable devtool on prod build + ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" + cat ./src-tauri/Cargo.toml + # Change app name for beta and nightly builds if [ "${{ inputs.channel }}" != "stable" ]; then jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json diff --git a/.github/workflows/template-tauri-build-linux-x64.yml b/.github/workflows/template-tauri-build-linux-x64.yml index 3b9daebb5..7cebf389a 100644 --- a/.github/workflows/template-tauri-build-linux-x64.yml +++ b/.github/workflows/template-tauri-build-linux-x64.yml @@ -117,11 +117,34 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json - # Temporarily enable devtool on prod build - ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" - cat ./src-tauri/Cargo.toml + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/Cargo.toml---------" + cat ./src-tauri/Cargo.toml + + # Temporarily enable devtool on prod build + ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" cat ./src-tauri/Cargo.toml # Change app name for beta and nightly builds diff --git a/.github/workflows/template-tauri-build-macos-external.yml b/.github/workflows/template-tauri-build-macos-external.yml index 8f61b86fa..3ba92f263 100644 --- a/.github/workflows/template-tauri-build-macos-external.yml +++ b/.github/workflows/template-tauri-build-macos-external.yml @@ -42,31 +42,6 @@ jobs: run: | cargo install ctoml - - name: Create bun and uv universal - run: | - mkdir -p ./src-tauri/resources/bin/ - cd ./src-tauri/resources/bin/ - curl -L -o bun-darwin-x64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.10/bun-darwin-x64.zip - curl -L -o bun-darwin-aarch64.zip https://github.com/oven-sh/bun/releases/download/bun-v1.2.10/bun-darwin-aarch64.zip - unzip bun-darwin-x64.zip - unzip bun-darwin-aarch64.zip - lipo -create -output bun-universal-apple-darwin bun-darwin-x64/bun bun-darwin-aarch64/bun - cp -f bun-darwin-aarch64/bun bun-aarch64-apple-darwin - cp -f bun-darwin-x64/bun bun-x86_64-apple-darwin - cp -f bun-universal-apple-darwin bun - - curl -L -o uv-x86_64.tar.gz https://github.com/astral-sh/uv/releases/download/0.6.17/uv-x86_64-apple-darwin.tar.gz - curl -L -o uv-arm64.tar.gz https://github.com/astral-sh/uv/releases/download/0.6.17/uv-aarch64-apple-darwin.tar.gz - tar -xzf uv-x86_64.tar.gz - tar -xzf uv-arm64.tar.gz - mv uv-x86_64-apple-darwin uv-x86_64 - mv uv-aarch64-apple-darwin uv-aarch64 - lipo -create -output uv-universal-apple-darwin uv-x86_64/uv uv-aarch64/uv - cp -f uv-x86_64/uv uv-x86_64-apple-darwin - cp -f uv-aarch64/uv uv-aarch64-apple-darwin - cp -f uv-universal-apple-darwin uv - ls -la - - name: Update app version run: | echo "Version: ${{ inputs.new_version }}" @@ -74,8 +49,35 @@ jobs: mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json + + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/Cargo.toml---------" + cat ./src-tauri/Cargo.toml + ctoml ./src-tauri/Cargo.toml dependencies.tauri.features[] "devtools" + if [ "${{ inputs.channel }}" != "stable" ]; then jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json diff --git a/.github/workflows/template-tauri-build-macos.yml b/.github/workflows/template-tauri-build-macos.yml index 4646041cf..82370b10b 100644 --- a/.github/workflows/template-tauri-build-macos.yml +++ b/.github/workflows/template-tauri-build-macos.yml @@ -101,7 +101,30 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/Cargo.toml---------" cat ./src-tauri/Cargo.toml # Temporarily enable devtool on prod build diff --git a/.github/workflows/template-tauri-build-windows-x64-external.yml b/.github/workflows/template-tauri-build-windows-x64-external.yml index ed1d601a3..5559fe146 100644 --- a/.github/workflows/template-tauri-build-windows-x64-external.yml +++ b/.github/workflows/template-tauri-build-windows-x64-external.yml @@ -54,9 +54,32 @@ jobs: jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" - echo "---------Cargo.toml---------" + echo "---------./src-tauri/Cargo.toml---------" cat ./src-tauri/Cargo.toml + if [ "${{ inputs.channel }}" != "stable" ]; then jq '.plugins.updater.endpoints = ["https://delta.jan.ai/${{ inputs.channel }}/latest.json"]' ./src-tauri/tauri.conf.json > /tmp/tauri.conf.json mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 1f25e5295..246b3705b 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -97,9 +97,31 @@ jobs: mv /tmp/tauri.conf.json ./src-tauri/tauri.conf.json jq --arg version "${{ inputs.new_version }}" '.version = $version' web-app/package.json > /tmp/package.json mv /tmp/package.json web-app/package.json + + # Update tauri plugin versions + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-hardware/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-hardware/package.json + echo "---------./src-tauri/tauri-plugin-hardware/package.json---------" + cat ./src-tauri/tauri-plugin-hardware/package.json + + jq --arg version "${{ inputs.new_version }}" '.version = $version' ./src-tauri/tauri-plugin-llamacpp/package.json > /tmp/package.json + mv /tmp/package.json ./src-tauri/tauri-plugin-llamacpp/package.json + + echo "---------./src-tauri/tauri-plugin-llamacpp/package.json---------" + cat ./src-tauri/tauri-plugin-llamacpp/package.json + + ctoml ./src-tauri/tauri-plugin-hardware/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-hardware/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-hardware/Cargo.toml + + ctoml ./src-tauri/tauri-plugin-llamacpp/Cargo.toml package.version "${{ inputs.new_version }}" + echo "---------./src-tauri/tauri-plugin-llamacpp/Cargo.toml---------" + cat ./src-tauri/tauri-plugin-llamacpp/Cargo.toml + ctoml ./src-tauri/Cargo.toml package.version "${{ inputs.new_version }}" - echo "---------Cargo.toml---------" + echo "---------./src-tauri/Cargo.toml---------" cat ./src-tauri/Cargo.toml # Add sign commands to tauri.windows.conf.json From 1b120712d49830e6d23fe5ae967a2ae8a467ecc7 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 25 Sep 2025 11:26:15 +0700 Subject: [PATCH 30/56] enhancement: update responsive footer and copy hero section --- docs/src/components/FooterMenu/index.tsx | 6 +++--- docs/src/components/Home/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/components/FooterMenu/index.tsx b/docs/src/components/FooterMenu/index.tsx index 1609430bf..68e1e6e78 100644 --- a/docs/src/components/FooterMenu/index.tsx +++ b/docs/src/components/FooterMenu/index.tsx @@ -77,9 +77,9 @@ export default function Footer() { return (