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

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

* chore: polish model detail page

* chore: fix test
This commit is contained in:
Louis 2025-02-26 15:54:06 +07:00 committed by GitHub
parent 916b28044d
commit d7329c7719
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 937 additions and 1092 deletions

File diff suppressed because it is too large Load Diff

View File

@ -451,7 +451,7 @@ export default class JanModelExtension extends ModelExtension {
return this.queue.add(() => return this.queue.add(() =>
ky ky
.get(`${API_URL}/v1/models/hub?author=cortexso`) .get(`${API_URL}/v1/models/hub?author=cortexso&tag=cortex.cpp`)
.json<Data<string>>() .json<Data<string>>()
.then((e) => { .then((e) => {
e.data?.forEach((model) => { e.data?.forEach((model) => {

View File

@ -35,14 +35,16 @@ import useDownloadModel from '@/hooks/useDownloadModel'
import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { useGetEngines } from '@/hooks/useEngineManagement' import { useGetEngines } from '@/hooks/useEngineManagement'
import { useGetModelSources } from '@/hooks/useModelSource' import {
useGetModelSources,
useGetFeaturedSources,
} from '@/hooks/useModelSource'
import useRecommendedModel from '@/hooks/useRecommendedModel' import useRecommendedModel from '@/hooks/useRecommendedModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
import { formatDownloadPercentage, toGigabytes } from '@/utils/converter' import { formatDownloadPercentage, toGigabytes } from '@/utils/converter'
import { manualRecommendationModel } from '@/utils/model'
import { getLogoEngine, getTitleByEngine } from '@/utils/modelEngine' import { getLogoEngine, getTitleByEngine } from '@/utils/modelEngine'
import { extractModelName } from '@/utils/modelSource' import { extractModelName } from '@/utils/modelSource'
@ -93,6 +95,7 @@ const ModelDropdown = ({
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>( const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>(
null null
) )
const { sources: featuredModels } = useGetFeaturedSources()
const { engines } = useGetEngines() const { engines } = useGetEngines()
@ -103,9 +106,6 @@ const ModelDropdown = ({
const configuredModels = useAtomValue(configuredModelsAtom) const configuredModels = useAtomValue(configuredModelsAtom)
const { stopModel } = useActiveModel() const { stopModel } = useActiveModel()
const featuredModels = sources?.filter((x) =>
manualRecommendationModel.includes(x.id)
)
const { updateThreadMetadata } = useCreateNewThread() const { updateThreadMetadata } = useCreateNewThread()
const engineList = useMemo( const engineList = useMemo(

View File

@ -36,6 +36,22 @@ export function useGetModelSources() {
return { sources, error, mutate } return { sources, error, mutate }
} }
/**
* @returns A Promise that resolves to featured model sources.
*/
export function useGetFeaturedSources() {
const { sources, error, mutate } = useGetModelSources()
return {
sources: sources?.filter((e) => e.metadata?.tags.includes('featured')),
error,
mutate,
}
}
/**
* @returns A Promise that resolves to model source mutation.
*/
export const useModelSourcesMutation = () => { export const useModelSourcesMutation = () => {
const extension = useMemo( const extension = useMemo(
() => extensionManager.get<ModelExtension>(ExtensionTypeEnum.Model), () => extensionManager.get<ModelExtension>(ExtensionTypeEnum.Model),

View File

@ -23,7 +23,7 @@ import { useRefreshModelList } from '@/hooks/useEngineManagement'
import { MarkdownTextMessage } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage' import { MarkdownTextMessage } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage'
import { toGigabytes } from '@/utils/converter' import { toGigabytes } from '@/utils/converter'
import { extractModelName } from '@/utils/modelSource' import { extractModelName, removeYamlFrontMatter } from '@/utils/modelSource'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom' import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { import {
@ -239,7 +239,7 @@ const ModelPage = ({ model, onGoBack }: Props) => {
{/* README */} {/* README */}
<div className="mt-8 flex w-full flex-col items-start justify-between sm:flex-row"> <div className="mt-8 flex w-full flex-col items-start justify-between sm:flex-row">
<MarkdownTextMessage <MarkdownTextMessage
text={model.metadata?.description ?? ''} text={removeYamlFrontMatter(model.metadata?.description ?? '')}
className="h-full w-full text-[hsla(var(--text-secondary))]" className="h-full w-full text-[hsla(var(--text-secondary))]"
/> />
</div> </div>

View File

@ -72,6 +72,7 @@ describe('OnDeviceStarterScreen', () => {
error: null, error: null,
mutate: jest.fn(), mutate: jest.fn(),
}) })
jest.spyOn(source, 'useGetFeaturedSources').mockReturnValue([])
render( render(
<Provider> <Provider>
<OnboardingScreen isShowStarterScreen={true} /> <OnboardingScreen isShowStarterScreen={true} />
@ -88,6 +89,7 @@ describe('OnDeviceStarterScreen', () => {
error: null, error: null,
mutate: jest.fn(), mutate: jest.fn(),
}) })
jest.spyOn(source, 'useGetFeaturedSources').mockReturnValue([])
render( render(
<Provider> <Provider>
<OnboardingScreen isShowStarterScreen={true} /> <OnboardingScreen isShowStarterScreen={true} />
@ -108,6 +110,7 @@ describe('OnDeviceStarterScreen', () => {
error: null, error: null,
mutate: jest.fn(), mutate: jest.fn(),
}) })
jest.spyOn(source, 'useGetFeaturedSources').mockReturnValue([])
render( render(
<Provider> <Provider>
<OnboardingScreen isShowStarterScreen={true} /> <OnboardingScreen isShowStarterScreen={true} />
@ -126,31 +129,31 @@ describe('OnDeviceStarterScreen', () => {
id: 'cortexso/deepseek-r1', id: 'cortexso/deepseek-r1',
name: 'DeepSeek R1', name: 'DeepSeek R1',
metadata: { metadata: {
tags: ['Featured'],
author: 'Test Author', author: 'Test Author',
size: 3000000000, size: 3000000000,
tags: ['featured'],
}, },
models: [ models: [
{ {
id: 'cortexso/deepseek-r1', id: 'cortexso/deepseek-r1',
name: 'DeepSeek R1', name: 'DeepSeek R1',
metadata: { metadata: {},
tags: ['Featured'],
},
}, },
], ],
}, },
{ {
id: 'cortexso/llama3.2', id: 'cortexso/llama3.2',
name: 'Llama 3.1', name: 'Llama 3.1',
metadata: { tags: [], author: 'Test Author', size: 2000000000 }, metadata: {
author: 'Test Author',
size: 2000000000,
tags: ['featured'],
},
models: [ models: [
{ {
id: 'cortexso/deepseek-r1', id: 'cortexso/deepseek-r1',
name: 'DeepSeek R1', name: 'DeepSeek R1',
metadata: { metadata: {},
tags: ['Featured'],
},
}, },
], ],
}, },
@ -161,6 +164,13 @@ describe('OnDeviceStarterScreen', () => {
error: null, error: null,
mutate: jest.fn(), mutate: jest.fn(),
}) })
jest
.spyOn(source, 'useGetFeaturedSources')
.mockReturnValue({
sources: mockConfiguredModels,
error: null,
mutate: jest.fn(),
})
render( render(
<Provider> <Provider>
@ -182,6 +192,10 @@ describe('OnDeviceStarterScreen', () => {
{ id: 'remote-model-2', name: 'Remote Model 2', engine: 'anthropic' }, { id: 'remote-model-2', name: 'Remote Model 2', engine: 'anthropic' },
] ]
jest
.spyOn(source, 'useGetFeaturedSources')
.mockReturnValue(mockRemoteModels)
mockAtomValue.mockImplementation((atom) => { mockAtomValue.mockImplementation((atom) => {
if (atom === jotai.atom([])) { if (atom === jotai.atom([])) {
return mockRemoteModels return mockRemoteModels

View File

@ -26,23 +26,19 @@ import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { useGetEngines } from '@/hooks/useEngineManagement' import { useGetEngines } from '@/hooks/useEngineManagement'
import { useGetModelSources } from '@/hooks/useModelSource' import {
useGetFeaturedSources,
useGetModelSources,
} from '@/hooks/useModelSource'
import { formatDownloadPercentage, toGigabytes } from '@/utils/converter' import { formatDownloadPercentage, toGigabytes } from '@/utils/converter'
import { manualRecommendationModel } from '@/utils/model'
import { import { getLogoEngine, getTitleByEngine } from '@/utils/modelEngine'
getLogoEngine,
getTitleByEngine,
isLocalEngine,
} from '@/utils/modelEngine'
import { extractModelName } from '@/utils/modelSource' import { extractModelName } from '@/utils/modelSource'
import { mainViewStateAtom } from '@/helpers/atoms/App.atom' import { mainViewStateAtom } from '@/helpers/atoms/App.atom'
import { import { getDownloadingModelAtom } from '@/helpers/atoms/Model.atom'
configuredModelsAtom,
getDownloadingModelAtom,
} from '@/helpers/atoms/Model.atom'
import { import {
selectedSettingAtom, selectedSettingAtom,
showScrollBarAtom, showScrollBarAtom,
@ -65,9 +61,7 @@ function OnboardingScreen({ isShowStarterScreen }: Props) {
const { sources } = useGetModelSources() const { sources } = useGetModelSources()
const setMainViewState = useSetAtom(mainViewStateAtom) const setMainViewState = useSetAtom(mainViewStateAtom)
const featuredModels = sources?.filter((x) => const { sources: featuredModels } = useGetFeaturedSources()
manualRecommendationModel.includes(x.id)
)
const filteredModels = useMemo( const filteredModels = useMemo(
() => () =>

View File

@ -7,13 +7,3 @@
export const normalizeModelId = (downloadUrl: string): string => { export const normalizeModelId = (downloadUrl: string): string => {
return downloadUrl.split('/').pop() ?? downloadUrl return downloadUrl.split('/').pop() ?? downloadUrl
} }
/**
* Default models to recommend to users when they first open the app.
* TODO: These will be replaced when we have a proper recommendation system
* AND cortexso repositories are updated with tags.
*/
export const manualRecommendationModel = [
'cortexso/deepseek-r1',
'cortexso/llama3.2',
]

View File

@ -4,12 +4,22 @@
*/ */
export const extractDescription = (text?: string) => { export const extractDescription = (text?: string) => {
if (!text) return text if (!text) return text
const normalizedText = removeYamlFrontMatter(text)
const overviewPattern = /(?:##\s*Overview\s*\n)([\s\S]*?)(?=\n\s*##|$)/ const overviewPattern = /(?:##\s*Overview\s*\n)([\s\S]*?)(?=\n\s*##|$)/
const matches = text?.match(overviewPattern) const matches = normalizedText?.match(overviewPattern)
if (matches && matches[1]) { if (matches && matches[1]) {
return matches[1].trim() return matches[1].trim()
} }
return text?.slice(0, 500).trim() return normalizedText?.slice(0, 500).trim()
}
/**
* Remove YAML (HF metadata) front matter from content
* @param content
* @returns
*/
export const removeYamlFrontMatter = (content: string): string => {
return content.replace(/^---\n([\s\S]*?)\n---\n/, '')
} }
/** /**