From 963d0a6c9d61d4e984cc078a42061899aabe5c13 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Mon, 11 Aug 2025 14:44:02 +0700 Subject: [PATCH 01/16] ci: update generate release note --- .github/workflows/jan-tauri-build.yaml | 27 ++------------------------ 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/workflows/jan-tauri-build.yaml b/.github/workflows/jan-tauri-build.yaml index 1dc22f2e4..95838e982 100644 --- a/.github/workflows/jan-tauri-build.yaml +++ b/.github/workflows/jan-tauri-build.yaml @@ -32,6 +32,7 @@ jobs: name: "${{ env.VERSION }}" draft: true prerelease: false + generate_release_notes: true build-macos: uses: ./.github/workflows/template-tauri-build-macos.yml @@ -118,28 +119,4 @@ jobs: upload_url: ${{ needs.create-draft-release.outputs.upload_url }} asset_path: ./latest.json asset_name: latest.json - asset_content_type: text/json - - update_release_draft: - needs: [build-macos, build-windows-x64, build-linux-x64] - permissions: - # write permission is required to create a github release - contents: write - # write permission is required for autolabeler - # otherwise, read permission is required at least - pull-requests: write - runs-on: ubuntu-latest - steps: - # (Optional) GitHub Enterprise requires GHE_HOST variable set - #- name: Set GHE_HOST - # run: | - # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV - - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml - # with: - # config-name: my-config.yml - # disable-autolabeler: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + asset_content_type: text/json \ No newline at end of file From 1563a41864bfd7befaac90eeb08ab08b73f84003 Mon Sep 17 00:00:00 2001 From: Minh141120 Date: Mon, 11 Aug 2025 14:48:53 +0700 Subject: [PATCH 02/16] ci: update jan docs publish pages condition --- .github/workflows/jan-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jan-docs.yml b/.github/workflows/jan-docs.yml index f64f93f66..3e92903c5 100644 --- a/.github/workflows/jan-docs.yml +++ b/.github/workflows/jan-docs.yml @@ -76,7 +76,7 @@ jobs: Preview URL: ${{ steps.deployCloudflarePages.outputs.url }} - name: Publish to Cloudflare Pages Production - if: (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev') + if: (github.event_name == 'push' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/dev') || (github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/heads/release/')) uses: cloudflare/pages-action@v1 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} From 02ded9b5451f57a16443635cdb09299442a40970 Mon Sep 17 00:00:00 2001 From: Akarshan Biswas Date: Tue, 12 Aug 2025 21:38:22 +0530 Subject: [PATCH 03/16] fix: Improve error message for invalid version/backend format (#6149) * fix: Improve error message for invalid version/backend format This commit changes the error message displayed when the `version_backend` configuration is invalid. The new message is more user-friendly and suggests a simple solution, such as restarting the application, which is more helpful to the user than the previous technical error message. * fix typo --- extensions/llamacpp-extension/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/llamacpp-extension/src/index.ts b/extensions/llamacpp-extension/src/index.ts index 75afb81ae..4c6d4a45e 100644 --- a/extensions/llamacpp-extension/src/index.ts +++ b/extensions/llamacpp-extension/src/index.ts @@ -1165,7 +1165,7 @@ export default class llamacpp_extension extends AIEngine { const [version, backend] = cfg.version_backend.split('/') if (!version || !backend) { throw new Error( - `Invalid version/backend format: ${cfg.version_backend}. Expected format: /` + "Initial setup for the backend failed due to a network issue. Please restart the app!" ) } From 985a8f31aefd9d70ec0923f81b69010f1370d2f1 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 13 Aug 2025 18:21:48 +0700 Subject: [PATCH 04/16] fix: migrations model setting (#6165) --- web-app/src/hooks/useModelProvider.ts | 56 +++++++-------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/web-app/src/hooks/useModelProvider.ts b/web-app/src/hooks/useModelProvider.ts index b1a988183..9be26ce41 100644 --- a/web-app/src/hooks/useModelProvider.ts +++ b/web-app/src/hooks/useModelProvider.ts @@ -227,34 +227,23 @@ export const useModelProvider = create()( > } - // Migration for cont_batching description update (version 0 -> 1) if (version === 0 && state?.providers) { - state.providers = state.providers.map((provider) => { - if (provider.provider === 'llamacpp' && provider.settings) { - provider.settings = provider.settings.map((setting) => { - if (setting.key === 'cont_batching') { - return { - ...setting, - description: - 'Enable continuous batching (a.k.a dynamic batching) for concurrent requests.', - } - } - return setting - }) - } - return provider - }) - } - - // Migration for chatTemplate key to chat_template (version 1 -> 2) - if (version === 1 && state?.providers) { state.providers.forEach((provider) => { + // Update cont_batching description for llamacpp provider + if (provider.provider === 'llamacpp' && provider.settings) { + const contBatchingSetting = provider.settings.find( + (s) => s.key === 'cont_batching' + ) + if (contBatchingSetting) { + contBatchingSetting.description = + 'Enable continuous batching (a.k.a dynamic batching) for concurrent requests.' + } + } + + // Migrate model settings if (provider.models) { provider.models.forEach((model) => { - // Initialize settings if it doesn't exist - if (!model.settings) { - model.settings = {} - } + if (!model.settings) model.settings = {} // Migrate chatTemplate key to chat_template if (model.settings.chatTemplate) { @@ -262,7 +251,7 @@ export const useModelProvider = create()( delete model.settings.chatTemplate } - // Add missing chat_template setting if it doesn't exist + // Add missing settings with defaults if (!model.settings.chat_template) { model.settings.chat_template = { ...modelSettings.chatTemplate, @@ -271,22 +260,7 @@ export const useModelProvider = create()( }, } } - }) - } - }) - } - // Migration for override_tensor_buffer_type key (version 2 -> 3) - if (version === 2 && state?.providers) { - state.providers.forEach((provider) => { - if (provider.models) { - provider.models.forEach((model) => { - // Initialize settings if it doesn't exist - if (!model.settings) { - model.settings = {} - } - - // Add missing override_tensor_buffer_type setting if it doesn't exist if (!model.settings.override_tensor_buffer_t) { model.settings.override_tensor_buffer_t = { ...modelSettings.override_tensor_buffer_t, @@ -303,7 +277,7 @@ export const useModelProvider = create()( return state }, - version: 3, + version: 1, } ) ) From b338849952cd75ebcd358653c0e63055cadd93a8 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 09:18:03 +0700 Subject: [PATCH 05/16] fix: handle modelId special char --- web-app/src/routes/hub/index.tsx | 4 ++-- web-app/src/services/models.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 5e43cc70e..7004f356e 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -287,8 +287,8 @@ function Hub() { const handleDownload = () => { // Immediately set local downloading state - addLocalDownloadingModel(modelId) - pullModel(modelId, modelUrl) + addLocalDownloadingModel(modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '')) + pullModel(modelId.replace(/[^a-zA-Z0-9/_\-.]/g, ''), modelUrl) } return ( diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 50b1c0ebc..d4156dc98 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -162,7 +162,9 @@ export const convertHfRepoToCatalogModel = ( } // Generate model_id from filename (remove .gguf extension, case-insensitive) - const modelId = file.rfilename.replace(/\.gguf$/i, '') + const modelId = file.rfilename + .replace(/\.gguf$/i, '') + .replace(/[^a-zA-Z0-9/_\-.]/g, '') return { model_id: modelId, From 1f0e79e934f2a8f955621b98703cce03bfd352b5 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 09:26:54 +0700 Subject: [PATCH 06/16] chore: update handle download variant --- web-app/src/routes/hub/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 7004f356e..bf6e06157 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -745,10 +745,16 @@ function Hub() { title={t('hub:downloadModel')} onClick={() => { addLocalDownloadingModel( - variant.model_id + variant.model_id.replace( + /[^a-zA-Z0-9/_\-.]/g, + '' + ) ) pullModel( - variant.model_id, + variant.model_id.replace( + /[^a-zA-Z0-9/_\-.]/g, + '' + ), variant.path ) }} From ace8214d4d30cb6a9ecfdbb6a2777dce6317faba Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 09:42:47 +0700 Subject: [PATCH 07/16] chore: make utils sanitize modelId --- web-app/src/lib/utils.ts | 4 ++++ web-app/src/routes/hub/index.tsx | 16 +++++----------- web-app/src/services/models.ts | 5 ++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index dd3301e03..38d3dd672 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -155,3 +155,7 @@ export function formatDuration(startTime: number, endTime?: number): string { return `${durationMs}ms` } } + +export function sanitizeModelId(modelId: string): string { + return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '') +} diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index bf6e06157..7ee5d0799 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -3,7 +3,7 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' -import { cn } from '@/lib/utils' +import { cn, sanitizeModelId } from '@/lib/utils' import { useState, useMemo, @@ -287,8 +287,8 @@ function Hub() { const handleDownload = () => { // Immediately set local downloading state - addLocalDownloadingModel(modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '')) - pullModel(modelId.replace(/[^a-zA-Z0-9/_\-.]/g, ''), modelUrl) + addLocalDownloadingModel(sanitizeModelId(modelId)) + pullModel(sanitizeModelId(modelId), modelUrl) } return ( @@ -745,16 +745,10 @@ function Hub() { title={t('hub:downloadModel')} onClick={() => { addLocalDownloadingModel( - variant.model_id.replace( - /[^a-zA-Z0-9/_\-.]/g, - '' - ) + sanitizeModelId(variant.model_id) ) pullModel( - variant.model_id.replace( - /[^a-zA-Z0-9/_\-.]/g, - '' - ), + sanitizeModelId(variant.model_id), variant.path ) }} diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index d4156dc98..36b72c895 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -5,6 +5,7 @@ import { SettingComponentProps, } from '@janhq/core' import { Model as CoreModel } from '@janhq/core' +import { sanitizeModelId } from '@/lib/utils' // Types for model catalog export interface ModelQuant { model_id: string @@ -162,9 +163,7 @@ export const convertHfRepoToCatalogModel = ( } // Generate model_id from filename (remove .gguf extension, case-insensitive) - const modelId = file.rfilename - .replace(/\.gguf$/i, '') - .replace(/[^a-zA-Z0-9/_\-.]/g, '') + const modelId = sanitizeModelId(file.rfilename.replace(/\.gguf$/i, '')) return { model_id: modelId, From da08becd938a69c3f85767dbc27dca702eec354f Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 10:15:33 +0700 Subject: [PATCH 08/16] fix: duplicate HF search results --- web-app/src/routes/hub/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 7ee5d0799..5935d4538 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -194,7 +194,11 @@ function Hub() { if (repoInfo) { const catalogModel = convertHfRepoToCatalogModel(repoInfo) if ( - !sources.some((s) => s.model_name === catalogModel.model_name) + !sources.some( + (s) => + catalogModel.model_name.trim().split('/').pop() === + s.model_name.trim() + ) ) { setHuggingFaceRepo(catalogModel) } From dd5b9e23f57ab3e1547f232731cbe0b0976c6f53 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 10:22:57 +0700 Subject: [PATCH 09/16] chore: update progress bar variant model --- web-app/src/routes/hub/index.tsx | 43 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 5935d4538..6adcc300f 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -139,7 +139,7 @@ function Hub() { filtered = filtered?.filter((model) => model.quants.some((variant) => llamaProvider?.models.some( - (m: { id: string }) => m.id === variant.model_id + (m: { id: string }) => m.id === sanitizeModelId(variant.model_id) ) ) ) @@ -677,8 +677,8 @@ function Hub() { {filteredModels[virtualItem.index].quants.map( (variant) => (

@@ -687,19 +687,32 @@ function Hub() { {(() => { const isDownloading = localDownloadingModels.has( - variant.model_id + sanitizeModelId( + variant.model_id + ) ) || downloadProcesses.some( - (e) => e.id === variant.model_id + (e) => + e.id === + sanitizeModelId( + variant.model_id + ) ) const downloadProgress = downloadProcesses.find( - (e) => e.id === variant.model_id + (e) => + e.id === + sanitizeModelId( + variant.model_id + ) )?.progress || 0 const isDownloaded = llamaProvider?.models.some( (m: { id: string }) => - m.id === variant.model_id + m.id === + sanitizeModelId( + variant.model_id + ) ) if (isDownloading) { @@ -733,7 +746,9 @@ function Hub() { size="sm" onClick={() => handleUseModel( - variant.model_id + sanitizeModelId( + variant.model_id + ) ) } > @@ -749,10 +764,18 @@ function Hub() { title={t('hub:downloadModel')} onClick={() => { addLocalDownloadingModel( - sanitizeModelId(variant.model_id) + sanitizeModelId( + sanitizeModelId( + variant.model_id + ) + ) ) pullModel( - sanitizeModelId(variant.model_id), + sanitizeModelId( + sanitizeModelId( + variant.model_id + ) + ), variant.path ) }} From 5657b6d9173619e72e38b3d8b3ddbe9d38726a2e Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 10:28:11 +0700 Subject: [PATCH 10/16] chore: fix sanitaize --- web-app/src/routes/hub/index.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 6adcc300f..e7e89490c 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -765,16 +765,12 @@ function Hub() { onClick={() => { addLocalDownloadingModel( sanitizeModelId( - sanitizeModelId( - variant.model_id - ) + variant.model_id ) ) pullModel( sanitizeModelId( - sanitizeModelId( - variant.model_id - ) + variant.model_id ), variant.path ) From 526e532e2d02bebc1927c768a9e7f8a206c855c9 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 10:50:50 +0700 Subject: [PATCH 11/16] fix: normalize model id from source preparation --- web-app/src/hooks/useModelSources.ts | 11 ++++++++++- web-app/src/lib/utils.ts | 2 +- web-app/src/routes/hub/index.tsx | 14 +++++--------- web-app/src/services/models.ts | 6 +++--- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/web-app/src/hooks/useModelSources.ts b/web-app/src/hooks/useModelSources.ts index 72d011582..3357947e1 100644 --- a/web-app/src/hooks/useModelSources.ts +++ b/web-app/src/hooks/useModelSources.ts @@ -2,6 +2,7 @@ import { create } from 'zustand' import { localStorageKey } from '@/constants/localStorage' import { createJSONStorage, persist } from 'zustand/middleware' import { fetchModelCatalog, CatalogModel } from '@/services/models' +import { sanitizeModelId } from '@/lib/utils' // Zustand store for model sources type ModelSourcesState = { @@ -20,7 +21,15 @@ export const useModelSources = create()( fetchSources: async () => { set({ loading: true, error: null }) try { - const newSources = await fetchModelCatalog() + const newSources = await fetchModelCatalog().then((catalogs) => + catalogs.map((catalog) => ({ + ...catalog, + quants: catalog.quants.map((quant) => ({ + ...quant, + model_id: sanitizeModelId(quant.model_id), + })), + })) + ) set({ sources: newSources.length ? newSources : get().sources, diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index 38d3dd672..ed2ec2bab 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -157,5 +157,5 @@ export function formatDuration(startTime: number, endTime?: number): string { } export function sanitizeModelId(modelId: string): string { - return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '') + return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(".", "_") } diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index e7e89490c..9ec2088df 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -3,7 +3,7 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { route } from '@/constants/routes' import { useModelSources } from '@/hooks/useModelSources' -import { cn, sanitizeModelId } from '@/lib/utils' +import { cn } from '@/lib/utils' import { useState, useMemo, @@ -291,8 +291,8 @@ function Hub() { const handleDownload = () => { // Immediately set local downloading state - addLocalDownloadingModel(sanitizeModelId(modelId)) - pullModel(sanitizeModelId(modelId), modelUrl) + addLocalDownloadingModel(modelId) + pullModel(modelId, modelUrl) } return ( @@ -764,14 +764,10 @@ function Hub() { title={t('hub:downloadModel')} onClick={() => { addLocalDownloadingModel( - sanitizeModelId( - variant.model_id - ) + variant.model_id ) pullModel( - sanitizeModelId( - variant.model_id - ), + variant.model_id, variant.path ) }} diff --git a/web-app/src/services/models.ts b/web-app/src/services/models.ts index 36b72c895..9cd9c7978 100644 --- a/web-app/src/services/models.ts +++ b/web-app/src/services/models.ts @@ -1,3 +1,4 @@ +import { sanitizeModelId } from '@/lib/utils' import { AIEngine, EngineManager, @@ -5,7 +6,6 @@ import { SettingComponentProps, } from '@janhq/core' import { Model as CoreModel } from '@janhq/core' -import { sanitizeModelId } from '@/lib/utils' // Types for model catalog export interface ModelQuant { model_id: string @@ -163,10 +163,10 @@ export const convertHfRepoToCatalogModel = ( } // Generate model_id from filename (remove .gguf extension, case-insensitive) - const modelId = sanitizeModelId(file.rfilename.replace(/\.gguf$/i, '')) + const modelId = file.rfilename.replace(/\.gguf$/i, '') return { - model_id: modelId, + model_id: sanitizeModelId(modelId), path: `https://huggingface.co/${repo.modelId}/resolve/main/${file.rfilename}`, file_size: formatFileSize(file.size), } From 8c28f9b6a69214cc2c34c2ed9d9a13f842324e3f Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 10:54:40 +0700 Subject: [PATCH 12/16] fix: should normalize model ID from source instead of frontend --- web-app/src/routes/hub/index.tsx | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index 9ec2088df..b27fb2e79 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -139,7 +139,7 @@ function Hub() { filtered = filtered?.filter((model) => model.quants.some((variant) => llamaProvider?.models.some( - (m: { id: string }) => m.id === sanitizeModelId(variant.model_id) + (m: { id: string }) => m.id === variant.model_id ) ) ) @@ -677,8 +677,8 @@ function Hub() { {filteredModels[virtualItem.index].quants.map( (variant) => (

@@ -687,32 +687,19 @@ function Hub() { {(() => { const isDownloading = localDownloadingModels.has( - sanitizeModelId( - variant.model_id - ) + variant.model_id ) || downloadProcesses.some( - (e) => - e.id === - sanitizeModelId( - variant.model_id - ) + (e) => e.id === variant.model_id ) const downloadProgress = downloadProcesses.find( - (e) => - e.id === - sanitizeModelId( - variant.model_id - ) + (e) => e.id === variant.model_id )?.progress || 0 const isDownloaded = llamaProvider?.models.some( (m: { id: string }) => - m.id === - sanitizeModelId( - variant.model_id - ) + m.id === variant.model_id ) if (isDownloading) { @@ -746,9 +733,7 @@ function Hub() { size="sm" onClick={() => handleUseModel( - sanitizeModelId( - variant.model_id - ) + variant.model_id ) } > From 83bb765bcce02fb48adc2e1e455d1badb6f76326 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 10:55:49 +0700 Subject: [PATCH 13/16] Apply suggestion from @ellipsis-dev[bot] Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- web-app/src/lib/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index ed2ec2bab..3d896b883 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -157,5 +157,5 @@ export function formatDuration(startTime: number, endTime?: number): string { } export function sanitizeModelId(modelId: string): string { - return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(".", "_") + return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(/\./g, "_") } From 16bfd6eafbf3c38bb311fa06c32d0a7ca0140391 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 14 Aug 2025 11:33:03 +0700 Subject: [PATCH 14/16] fix: full url search --- .../hooks/__tests__/useModelSources.test.ts | 77 ++++++--- web-app/src/routes/hub/index.tsx | 4 +- .../settings/__tests__/general.test.tsx | 156 +++++++++++++----- 3 files changed, 172 insertions(+), 65 deletions(-) diff --git a/web-app/src/hooks/__tests__/useModelSources.test.ts b/web-app/src/hooks/__tests__/useModelSources.test.ts index d010f7f8a..41e5985a8 100644 --- a/web-app/src/hooks/__tests__/useModelSources.test.ts +++ b/web-app/src/hooks/__tests__/useModelSources.test.ts @@ -25,6 +25,11 @@ vi.mock('@/services/models', () => ({ fetchModelCatalog: vi.fn(), })) +// Mock the sanitizeModelId function +vi.mock('@/lib/utils', () => ({ + sanitizeModelId: vi.fn((id: string) => id), +})) + describe('useModelSources', () => { let mockFetchModelCatalog: any @@ -56,15 +61,19 @@ describe('useModelSources', () => { const mockSources: CatalogModel[] = [ { model_name: 'model-1', - provider: 'provider-1', description: 'First model', - version: '1.0.0', + developer: 'provider-1', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }], }, { model_name: 'model-2', - provider: 'provider-2', description: 'Second model', - version: '2.0.0', + developer: 'provider-2', + downloads: 200, + num_quants: 1, + quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }], }, ] @@ -101,18 +110,22 @@ describe('useModelSources', () => { const existingSources: CatalogModel[] = [ { model_name: 'existing-model', - provider: 'existing-provider', description: 'Existing model', - version: '1.0.0', + developer: 'existing-provider', + downloads: 50, + num_quants: 1, + quants: [{ model_id: 'existing-model-q4', path: '/path/existing', file_size: '1GB' }], }, ] const newSources: CatalogModel[] = [ { model_name: 'new-model', - provider: 'new-provider', description: 'New model', - version: '2.0.0', + developer: 'new-provider', + downloads: 150, + num_quants: 1, + quants: [{ model_id: 'new-model-q4', path: '/path/new', file_size: '2GB' }], }, ] @@ -138,24 +151,30 @@ describe('useModelSources', () => { const existingSources: CatalogModel[] = [ { model_name: 'duplicate-model', - provider: 'old-provider', description: 'Old version', - version: '1.0.0', + developer: 'old-provider', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'duplicate-model-q4', path: '/path/old', file_size: '1GB' }], }, { model_name: 'unique-model', - provider: 'provider', description: 'Unique model', - version: '1.0.0', + developer: 'provider', + downloads: 75, + num_quants: 1, + quants: [{ model_id: 'unique-model-q4', path: '/path/unique', file_size: '1GB' }], }, ] const newSources: CatalogModel[] = [ { model_name: 'duplicate-model', - provider: 'new-provider', description: 'New version', - version: '2.0.0', + developer: 'new-provider', + downloads: 200, + num_quants: 1, + quants: [{ model_id: 'duplicate-model-q4-new', path: '/path/new', file_size: '2GB' }], }, ] @@ -207,9 +226,11 @@ describe('useModelSources', () => { const mockSources: CatalogModel[] = [ { model_name: 'model-1', - provider: 'provider-1', description: 'Model 1', - version: '1.0.0', + developer: 'provider-1', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }], }, ] @@ -238,9 +259,11 @@ describe('useModelSources', () => { const mockSources: CatalogModel[] = [ { model_name: 'shared-model', - provider: 'shared-provider', description: 'Shared model', - version: '1.0.0', + developer: 'shared-provider', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'shared-model-q4', path: '/path/shared', file_size: '1GB' }], }, ] @@ -288,18 +311,22 @@ describe('useModelSources', () => { const sources1: CatalogModel[] = [ { model_name: 'model-1', - provider: 'provider-1', description: 'First batch', - version: '1.0.0', + developer: 'provider-1', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'model-1-q4', path: '/path/1', file_size: '1GB' }], }, ] const sources2: CatalogModel[] = [ { model_name: 'model-2', - provider: 'provider-2', description: 'Second batch', - version: '2.0.0', + developer: 'provider-2', + downloads: 200, + num_quants: 1, + quants: [{ model_id: 'model-2-q4', path: '/path/2', file_size: '2GB' }], }, ] @@ -338,9 +365,11 @@ describe('useModelSources', () => { const mockSources: CatalogModel[] = [ { model_name: 'recovery-model', - provider: 'recovery-provider', description: 'Recovery model', - version: '1.0.0', + developer: 'recovery-provider', + downloads: 100, + num_quants: 1, + quants: [{ model_id: 'recovery-model-q4', path: '/path/recovery', file_size: '1GB' }], }, ] diff --git a/web-app/src/routes/hub/index.tsx b/web-app/src/routes/hub/index.tsx index b27fb2e79..a45a1779a 100644 --- a/web-app/src/routes/hub/index.tsx +++ b/web-app/src/routes/hub/index.tsx @@ -132,7 +132,9 @@ function Hub() { // Apply search filter if (debouncedSearchValue.length) { const fuse = new Fuse(filtered, searchOptions) - filtered = fuse.search(debouncedSearchValue).map((result) => result.item) + // Remove domain from search value (e.g., "huggingface.co/author/model" -> "author/model") + const cleanedSearchValue = debouncedSearchValue.replace(/^https?:\/\/[^/]+\//, '') + filtered = fuse.search(cleanedSearchValue).map((result) => result.item) } // Apply downloaded filter if (showOnlyDownloaded) { diff --git a/web-app/src/routes/settings/__tests__/general.test.tsx b/web-app/src/routes/settings/__tests__/general.test.tsx index f5033b30b..96388b0fb 100644 --- a/web-app/src/routes/settings/__tests__/general.test.tsx +++ b/web-app/src/routes/settings/__tests__/general.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { render, screen, fireEvent, waitFor, act } from '@testing-library/react' import { Route as GeneralRoute } from '../general' // Mock all the dependencies @@ -68,9 +68,12 @@ vi.mock('@/hooks/useGeneralSetting', () => ({ }), })) +// Create a controllable mock +const mockCheckForUpdate = vi.fn() + vi.mock('@/hooks/useAppUpdater', () => ({ useAppUpdater: () => ({ - checkForUpdate: vi.fn(), + checkForUpdate: mockCheckForUpdate, }), })) @@ -184,12 +187,17 @@ vi.mock('@tauri-apps/plugin-opener', () => ({ revealItemInDir: vi.fn(), })) -vi.mock('@tauri-apps/api/webviewWindow', () => ({ - WebviewWindow: vi.fn().mockImplementation((label: string, options: any) => ({ +vi.mock('@tauri-apps/api/webviewWindow', () => { + const MockWebviewWindow = vi.fn().mockImplementation((label: string, options: any) => ({ once: vi.fn(), setFocus: vi.fn(), - })), -})) + })) + MockWebviewWindow.getByLabel = vi.fn().mockReturnValue(null) + + return { + WebviewWindow: MockWebviewWindow, + } +}) vi.mock('@tauri-apps/api/event', () => ({ emit: vi.fn(), @@ -244,6 +252,7 @@ global.window = { core: { api: { relaunch: vi.fn(), + getConnectedServers: vi.fn().mockResolvedValue([]), }, }, } @@ -258,20 +267,26 @@ Object.assign(navigator, { describe('General Settings Route', () => { beforeEach(() => { vi.clearAllMocks() + // Reset the mock to return a promise that resolves immediately by default + mockCheckForUpdate.mockResolvedValue(null) }) - it('should render the general settings page', () => { + it('should render the general settings page', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) expect(screen.getByTestId('header-page')).toBeInTheDocument() expect(screen.getByTestId('settings-menu')).toBeInTheDocument() expect(screen.getByText('common:settings')).toBeInTheDocument() }) - it('should render app version', () => { + it('should render app version', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) expect(screen.getByText('v1.0.0')).toBeInTheDocument() }) @@ -284,64 +299,82 @@ describe('General Settings Route', () => { // expect(screen.getByTestId('language-switcher')).toBeInTheDocument() // }) - it('should render switches for experimental features and spell check', () => { + it('should render switches for experimental features and spell check', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const switches = screen.getAllByTestId('switch') expect(switches.length).toBeGreaterThanOrEqual(2) }) - it('should render huggingface token input', () => { + it('should render huggingface token input', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const input = screen.getByTestId('input') expect(input).toBeInTheDocument() expect(input).toHaveValue('test-token') }) - it('should handle spell check toggle', () => { + it('should handle spell check toggle', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const switches = screen.getAllByTestId('switch') expect(switches.length).toBeGreaterThan(0) // Test that switches are interactive - fireEvent.click(switches[0]) + await act(async () => { + fireEvent.click(switches[0]) + }) expect(switches[0]).toBeInTheDocument() }) - it('should handle experimental features toggle', () => { + it('should handle experimental features toggle', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const switches = screen.getAllByTestId('switch') expect(switches.length).toBeGreaterThan(0) // Test that switches are interactive if (switches.length > 1) { - fireEvent.click(switches[1]) + await act(async () => { + fireEvent.click(switches[1]) + }) expect(switches[1]).toBeInTheDocument() } }) - it('should handle huggingface token change', () => { + it('should handle huggingface token change', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const input = screen.getByTestId('input') expect(input).toBeInTheDocument() // Test that input is interactive - fireEvent.change(input, { target: { value: 'new-token' } }) + await act(async () => { + fireEvent.change(input, { target: { value: 'new-token' } }) + }) expect(input).toBeInTheDocument() }) it('should handle check for updates', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const buttons = screen.getAllByTestId('button') const checkUpdateButton = buttons.find((button) => @@ -350,7 +383,9 @@ describe('General Settings Route', () => { if (checkUpdateButton) { expect(checkUpdateButton).toBeInTheDocument() - fireEvent.click(checkUpdateButton) + await act(async () => { + fireEvent.click(checkUpdateButton) + }) // Test that button is interactive expect(checkUpdateButton).toBeInTheDocument() } @@ -358,7 +393,9 @@ describe('General Settings Route', () => { it('should handle data folder display', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) // Test that component renders without errors expect(screen.getByTestId('header-page')).toBeInTheDocument() @@ -367,25 +404,31 @@ describe('General Settings Route', () => { it('should handle copy to clipboard', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) // Test that component renders without errors expect(screen.getByTestId('header-page')).toBeInTheDocument() expect(screen.getByTestId('settings-menu')).toBeInTheDocument() }) - it('should handle factory reset dialog', () => { + it('should handle factory reset dialog', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) expect(screen.getByTestId('dialog')).toBeInTheDocument() expect(screen.getByTestId('dialog-trigger')).toBeInTheDocument() expect(screen.getByTestId('dialog-content')).toBeInTheDocument() }) - it('should render external links', () => { + it('should render external links', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) // Check for external links const links = screen.getAllByRole('link') @@ -394,7 +437,9 @@ describe('General Settings Route', () => { it('should handle logs window opening', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const buttons = screen.getAllByTestId('button') const openLogsButton = buttons.find((button) => @@ -404,14 +449,18 @@ describe('General Settings Route', () => { if (openLogsButton) { expect(openLogsButton).toBeInTheDocument() // Test that button is interactive - fireEvent.click(openLogsButton) + await act(async () => { + fireEvent.click(openLogsButton) + }) expect(openLogsButton).toBeInTheDocument() } }) it('should handle reveal logs folder', async () => { const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const buttons = screen.getAllByTestId('button') const revealLogsButton = buttons.find((button) => @@ -421,26 +470,39 @@ describe('General Settings Route', () => { if (revealLogsButton) { expect(revealLogsButton).toBeInTheDocument() // Test that button is interactive - fireEvent.click(revealLogsButton) + await act(async () => { + fireEvent.click(revealLogsButton) + }) expect(revealLogsButton).toBeInTheDocument() } }) - it('should show correct file explorer text for Windows', () => { + it('should show correct file explorer text for Windows', async () => { global.IS_WINDOWS = true global.IS_MACOS = false const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) expect( screen.getByText('settings:general.showInFileExplorer') ).toBeInTheDocument() }) - it('should disable check for updates button when checking', () => { + it('should disable check for updates button when checking', async () => { + // Create a promise that we can control + let resolveUpdate: (value: any) => void + const updatePromise = new Promise((resolve) => { + resolveUpdate = resolve + }) + mockCheckForUpdate.mockReturnValue(updatePromise) + const Component = GeneralRoute.component as React.ComponentType - render() + await act(async () => { + render() + }) const buttons = screen.getAllByTestId('button') const checkUpdateButton = buttons.find((button) => @@ -448,8 +510,22 @@ describe('General Settings Route', () => { ) if (checkUpdateButton) { - fireEvent.click(checkUpdateButton) + // Click the button but don't await it yet + act(() => { + fireEvent.click(checkUpdateButton) + }) + + // Now the button should be disabled while checking expect(checkUpdateButton).toBeDisabled() + + // Resolve the promise to finish the update check + await act(async () => { + resolveUpdate!(null) + await updatePromise + }) + + // Button should be enabled again + expect(checkUpdateButton).not.toBeDisabled() } }) }) From 10f3f7cea5c56f1163d52aaf275e9c790e4a2a70 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 11:57:28 +0700 Subject: [PATCH 15/16] chore: handle copy error object message --- .../src/containers/dialogs/LoadModelErrorDialog.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx b/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx index 136f13853..c3e4f95aa 100644 --- a/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx +++ b/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx @@ -37,6 +37,16 @@ export default function LoadModelErrorDialog() { if (typeof error === 'string') return error + if (typeof error === 'object') { + const errorObj = error as { + code?: string + message: string + details?: string + } + + return errorObj.message + } + if (typeof error === 'object' && 'code' in error && 'message' in error) { const errorObj = error as { code?: string From 6acdf22e4142658cfad6139dd33a87639be6fffd Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Thu, 14 Aug 2025 12:02:47 +0700 Subject: [PATCH 16/16] chore: reorder error --- .../dialogs/LoadModelErrorDialog.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx b/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx index c3e4f95aa..410305c46 100644 --- a/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx +++ b/web-app/src/containers/dialogs/LoadModelErrorDialog.tsx @@ -37,16 +37,6 @@ export default function LoadModelErrorDialog() { if (typeof error === 'string') return error - if (typeof error === 'object') { - const errorObj = error as { - code?: string - message: string - details?: string - } - - return errorObj.message - } - if (typeof error === 'object' && 'code' in error && 'message' in error) { const errorObj = error as { code?: string @@ -62,6 +52,16 @@ export default function LoadModelErrorDialog() { return copyText } + if (typeof error === 'object') { + const errorObj = error as { + code?: string + message: string + details?: string + } + + return errorObj.message + } + return JSON.stringify(error) }