feat: add quick access model setting via dropdown model (#5104)

* feat: add quick access model setting via dropdown model

* Update web-app/src/containers/DropdownModelProvider.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
This commit is contained in:
Faisal Amir 2025-05-26 13:29:09 +07:00 committed by GitHub
parent 83464b367f
commit 8afb962739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 129 additions and 142 deletions

View File

@ -33,9 +33,9 @@ const DropdownAssistant = () => {
return ( return (
<> <>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}> <DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<div className="flex items-center justify-between gap-1"> <div className="flex items-center justify-between gap-2 bg-main-view-fg/5 py-1 hover:bg-main-view-fg/8 px-2 rounded-sm">
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="bg-main-view-fg/5 py-1 hover:bg-main-view-fg/8 px-2 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40"> <button className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40">
<div className="text-main-view-fg/80 flex items-center gap-1"> <div className="text-main-view-fg/80 flex items-center gap-1">
{selectedAssistant?.avatar && ( {selectedAssistant?.avatar && (
<span className="shrink-0 w-4 h-4 relative flex items-center justify-center"> <span className="shrink-0 w-4 h-4 relative flex items-center justify-center">
@ -75,10 +75,13 @@ const DropdownAssistant = () => {
align="start" align="start"
> >
{assistants.map((assistant) => ( {assistants.map((assistant) => (
<div className="relative pr-6" key={assistant.id}> <div
<DropdownMenuItem asChild> className="relative pr-6 hover:bg-main-view-fg/4 rounded-sm"
key={assistant.id}
>
<DropdownMenuItem className="hover:bg-transparent">
<div <div
className="text-main-view-fg/70 cursor-pointer" className="text-main-view-fg/70 cursor-pointer flex gap-2"
onClick={() => { onClick={() => {
setCurrentAssistant(assistant) setCurrentAssistant(assistant)
updateCurrentThreadAssistant(assistant) updateCurrentThreadAssistant(assistant)

View File

@ -14,14 +14,20 @@ import { IconSettings } from '@tabler/icons-react'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { route } from '@/constants/routes' import { route } from '@/constants/routes'
import { useThreads } from '@/hooks/useThreads' import { useThreads } from '@/hooks/useThreads'
import { ModelSetting } from '@/containers/ModelSetting'
type DropdownModelProviderProps = { type DropdownModelProviderProps = {
model?: ThreadModel model?: ThreadModel
} }
const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => { const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
const { providers, selectModelProvider, selectedProvider, selectedModel } = const {
useModelProvider() providers,
getProviderByName,
selectModelProvider,
selectedProvider,
selectedModel,
} = useModelProvider()
const [displayModel, setDisplayModel] = useState<string>('') const [displayModel, setDisplayModel] = useState<string>('')
const { updateCurrentThreadModel } = useThreads() const { updateCurrentThreadModel } = useThreads()
const navigate = useNavigate() const navigate = useNavigate()
@ -48,107 +54,123 @@ const DropdownModelProvider = ({ model }: DropdownModelProviderProps) => {
if (!providers.length) return null if (!providers.length) return null
const provider = getProviderByName(selectedProvider)
return ( return (
<DropdownMenu> <>
<DropdownMenuTrigger asChild> <DropdownMenu>
<button <div className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 flex items-center gap-1.5 rounded-sm">
title={displayModel} <DropdownMenuTrigger asChild>
className="bg-main-view-fg/5 hover:bg-main-view-fg/8 px-2 py-1 rounded font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-40" <button
> title={displayModel}
<img className="font-medium cursor-pointer flex items-center gap-1.5 relative z-20 max-w-38"
src={getProviderLogo(selectedProvider as string)} >
alt={`${selectedProvider} - Logo`} <img
className="size-4" src={getProviderLogo(selectedProvider as string)}
/> alt={`${selectedProvider} - Logo`}
<span className="size-4"
className={cn( />
'text-main-view-fg/80 truncate leading-normal', <span
!selectedModel?.id && 'text-main-view-fg/50'
)}
>
{displayModel}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-60 max-h-[320px]"
side="bottom"
align="start"
>
<DropdownMenuGroup>
{providers.map((provider, index) => {
// Only show active providers
if (!provider.active) return null
return (
<div
className={cn( className={cn(
'bg-main-view-fg/4 first:mt-0 rounded-sm my-1.5 first:mb-0 ' 'text-main-view-fg/80 truncate leading-normal',
!selectedModel?.id && 'text-main-view-fg/50'
)} )}
key={`provider-${index}`}
> >
<div className="flex items-center justify-between"> {displayModel}
<DropdownMenuLabel className="flex items-center gap-1.5"> </span>
<img </button>
src={getProviderLogo(provider.provider)} </DropdownMenuTrigger>
alt={`${provider.provider} - Logo`} {selectedModel && (
className="size-4" <ModelSetting
/> model={selectedModel as Model}
<span className="capitalize truncate"> provider={provider as ProviderObject}
{getProviderTitle(provider.provider)} />
</span> )}
</DropdownMenuLabel> </div>
<div <DropdownMenuContent
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out mr-2" className="w-60 max-h-[320px]"
onClick={() => side="bottom"
navigate({ align="start"
to: route.settings.providers, >
params: { providerName: provider.provider }, <DropdownMenuGroup>
}) {providers.map((provider, index) => {
} // Only show active providers
> if (!provider.active) return null
<IconSettings size={18} className="text-main-view-fg/50" />
</div>
</div>
{provider.models.map((model, modelIndex) => { return (
const capabilities = model.capabilities || [] <div
className={cn(
return ( 'bg-main-view-fg/4 first:mt-0 rounded-sm my-1.5 first:mb-0 '
<DropdownMenuItem )}
className={cn( key={`provider-${index}`}
'h-8 mx-1', >
provider.provider !== 'llama.cpp' && <div className="flex items-center justify-between">
!provider.api_key?.length && <DropdownMenuLabel className="flex items-center gap-1.5">
'hidden' <img
)} src={getProviderLogo(provider.provider)}
title={model.id} alt={`${provider.provider} - Logo`}
key={`model-${modelIndex}`} className="size-4"
onClick={() => { />
selectModelProvider(provider.provider, model.id) <span className="capitalize truncate text-sm">
updateCurrentThreadModel({ {getProviderTitle(provider.provider)}
id: model.id, </span>
provider: provider.provider, </DropdownMenuLabel>
<div
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out mr-2"
onClick={() =>
navigate({
to: route.settings.providers,
params: { providerName: provider.provider },
}) })
}} }
> >
<div className="flex items-center gap-1.5 w-full"> <IconSettings
<span className="truncate text-main-view-fg/70"> size={18}
{model.id} className="text-main-view-fg/50"
</span> />
<div className="-mr-1.5"> </div>
<Capabilities capabilities={capabilities} /> </div>
{provider.models.map((model, modelIndex) => {
const capabilities = model.capabilities || []
return (
<DropdownMenuItem
className={cn(
'h-8 mx-1',
provider.provider !== 'llama.cpp' &&
!provider.api_key?.length &&
'hidden'
)}
title={model.id}
key={`model-${modelIndex}`}
onClick={() => {
selectModelProvider(provider.provider, model.id)
updateCurrentThreadModel({
id: model.id,
provider: provider.provider,
})
}}
>
<div className="flex items-center gap-1.5 w-full">
<span className="truncate text-main-view-fg/70">
{model.id}
</span>
<div className="-mr-1.5">
<Capabilities capabilities={capabilities} />
</div>
</div> </div>
</div> </DropdownMenuItem>
</DropdownMenuItem> )
) })}
})} </div>
</div> )
) })}
})} </DropdownMenuGroup>
</DropdownMenuGroup> </DropdownMenuContent>
</DropdownMenuContent> </DropdownMenu>
</DropdownMenu> </>
) )
} }

View File

@ -13,12 +13,6 @@ import { useModelProvider } from '@/hooks/useModelProvider'
import { updateModel } from '@/services/models' import { updateModel } from '@/services/models'
import { ModelSettingParams } from '@janhq/core' import { ModelSettingParams } from '@janhq/core'
// import {
// HoverCard,
// HoverCardContent,
// HoverCardTrigger,
// } from '@/components/ui/hover-card'
type ModelSettingProps = { type ModelSettingProps = {
provider: ProviderObject provider: ProviderObject
model: Model model: Model
@ -91,10 +85,10 @@ export function ModelSetting({ model, provider }: ModelSettingProps) {
const config = value as ProviderSetting const config = value as ProviderSetting
return ( return (
<div key={key} className="space-y-2"> <div key={key} className="space-y-2">
<div className="flex flex-col"> <div className="flex items-start justify-between gap-8">
<div className="space-y-1 mb-2"> <div className="space-y-1 mb-2">
<h3 className="font-medium">{config.title}</h3> <h3 className="font-medium">{config.title}</h3>
<p className="text-main-view-fg/60 text-xs"> <p className="text-main-view-fg/70 text-xs">
{config.description} {config.description}
</p> </p>
</div> </div>
@ -109,38 +103,6 @@ export function ModelSetting({ model, provider }: ModelSettingProps) {
}} }}
onChange={(newValue) => handleSettingChange(key, newValue)} onChange={(newValue) => handleSettingChange(key, newValue)}
/> />
{/* <div className="mt-2">
<HoverCard openDelay={200}>
<HoverCardTrigger asChild>
<div>
<div className="flex items-center justify-between mb-2">
<label htmlFor={config.key}>{config.title}</label>
</div>
<DynamicControllerSetting
key={config.key}
title={config.title}
description={config.description}
controllerType={config.controller_type}
controllerProps={{
...config.controller_props,
value: value,
}}
onChange={(newValue) =>
handleSettingChange(key, newValue)
}
/>
</div>
</HoverCardTrigger>
<HoverCardContent
align="start"
className="w-[260px] text-sm"
side="left"
sideOffset={24}
>
{config.description}
</HoverCardContent>
</HoverCard>
</div> */}
</div> </div>
</div> </div>
) )

View File

@ -163,7 +163,7 @@ function Hub() {
<div className="flex flex-col h-full w-full"> <div className="flex flex-col h-full w-full">
<HeaderPage> <HeaderPage>
<div className="pr-4 py-3 border-b border-main-view-fg/5 h-10 w-full flex items-center justify-between relative z-20 "> <div className="pr-4 py-3 border-b border-main-view-fg/5 h-10 w-full flex items-center justify-between relative z-20 ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 w-full pr-4">
<IconSearch className="text-main-view-fg/60" size={14} /> <IconSearch className="text-main-view-fg/60" size={14} />
<input <input
placeholder="Search models..." placeholder="Search models..."

View File

@ -300,7 +300,7 @@ function ProviderDetail() {
</div> </div>
} }
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-1">
<DialogEditModel <DialogEditModel
provider={provider} provider={provider}
modelId={model.id} modelId={model.id}