refactor: align Model Settings popover with Figma design

Restructure the popover layout to match design specs: add header with
close button, anchor popup to settings icon, change trigger to semantic
button, and widen panel to 400px.
This commit is contained in:
yyh
2026-03-11 17:22:26 +08:00
parent d72fbce31c
commit 908e57b9f5
6 changed files with 55 additions and 43 deletions

View File

@ -91,7 +91,7 @@ vi.mock('./presets-parameter', () => ({
}))
vi.mock('./trigger', () => ({
default: () => <button>Open Settings</button>,
default: () => <div data-testid="trigger-mock">Open Settings</div>,
}))
vi.mock('@/config', async (importOriginal) => {

View File

@ -9,12 +9,13 @@ import type {
} from '../declarations'
import type { ParameterValue } from './parameter-item'
import type { TriggerProps } from './trigger'
import { useMemo, useState } from 'react'
import { useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import Loading from '@/app/components/base/loading'
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from '@/app/components/base/ui/popover'
@ -65,6 +66,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const settingsIconRef = useRef<HTMLDivElement>(null)
const { data: parameterRulesData, isLoading } = useModelParameterRules(provider, modelId)
const {
currentProvider,
@ -132,7 +134,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
>
<PopoverTrigger
render={(
<div className="block">
<button type="button" className="block w-full border-none bg-transparent p-0 text-left [color:inherit] [font:inherit]">
{
renderTrigger
? renderTrigger({
@ -149,23 +151,30 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
currentModel={currentModel}
providerName={provider}
modelId={modelId}
settingsRef={settingsIconRef}
/>
)
}
</div>
</button>
)}
/>
<PopoverContent
placement={isInWorkflow ? 'left' : 'bottom-end'}
placement={isInWorkflow ? 'left' : (renderTrigger ? 'bottom-end' : 'left-start')}
sideOffset={4}
className={portalToFollowElemContentClassName}
popupClassName={cn(popupClassName, 'w-[389px] rounded-2xl')}
popupClassName={cn(popupClassName, 'w-[400px] rounded-2xl')}
positionerProps={!renderTrigger ? { anchor: settingsIconRef } : undefined}
>
<div className="max-h-[420px] overflow-y-auto p-4 pt-3">
<div className="relative">
<div className="mb-1 flex h-6 items-center text-text-secondary system-sm-semibold">
{t('modelProvider.model', { ns: 'common' }).toLocaleUpperCase()}
</div>
<div className="relative px-3 pb-1 pt-3.5">
<div className="pl-1 pr-8 text-text-primary system-xl-semibold">
{t('modelProvider.modelSettings', { ns: 'common' })}
</div>
<PopoverClose className="absolute right-2.5 top-2.5 flex items-center justify-center rounded-lg p-1.5 hover:bg-state-base-hover">
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</PopoverClose>
</div>
<div className="max-h-[420px] overflow-y-auto">
<div className="px-4 pb-4 pt-2">
<ModelSelector
defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined}
modelList={activeTextGenerationModelList}
@ -175,41 +184,40 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
</div>
{
!!parameterRules.length && (
<div className="my-3 h-px bg-divider-subtle" />
)
}
{
isLoading && (
<div className="mt-5"><Loading /></div>
)
}
{
!isLoading && !!parameterRules.length && (
<div className="mb-2 flex items-center justify-between">
<div className="flex h-6 items-center text-text-secondary system-sm-semibold">{t('modelProvider.parameters', { ns: 'common' })}</div>
<div className="flex flex-col gap-2 border-t border-divider-subtle px-4 pb-4 pt-3">
<div className="flex items-center gap-1">
<div className="flex flex-1 items-center text-text-secondary system-sm-semibold-uppercase">{t('modelProvider.parameters', { ns: 'common' })}</div>
{
PROVIDER_WITH_PRESET_TONE.includes(provider) && (
<PresetsParameter onSelect={handleSelectPresetParameter} />
)
}
</div>
{
PROVIDER_WITH_PRESET_TONE.includes(provider) && (
<PresetsParameter onSelect={handleSelectPresetParameter} />
)
isLoading
? <div className="py-5"><Loading /></div>
: (
[
...parameterRules,
...(isAdvancedMode ? [STOP_PARAMETER_RULE] : []),
].map(parameter => (
<ParameterItem
key={`${modelId}-${parameter.name}`}
parameterRule={parameter}
value={completionParams?.[parameter.name]}
onChange={v => handleParamChange(parameter.name, v)}
onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
isInWorkflow={isInWorkflow}
/>
))
)
}
</div>
)
}
{
!isLoading && !!parameterRules.length && (
[
...parameterRules,
...(isAdvancedMode ? [STOP_PARAMETER_RULE] : []),
].map(parameter => (
<ParameterItem
key={`${modelId}-${parameter.name}`}
parameterRule={parameter}
value={completionParams?.[parameter.name]}
onChange={v => handleParamChange(parameter.name, v)}
onSwitch={(checked, assignValue) => handleSwitch(parameter.name, checked, assignValue)}
isInWorkflow={isInWorkflow}
/>
))
!parameterRules.length && isLoading && (
<div className="px-4 py-5"><Loading /></div>
)
}
</div>

View File

@ -219,7 +219,7 @@ function ParameterItem({
return (
<Select
value={renderValue as string}
onValueChange={v => handleInputChange(v)}
onValueChange={v => handleInputChange(v ?? undefined)}
>
<SelectTrigger className="w-full">
<SelectValue />

View File

@ -1,4 +1,4 @@
import type { FC } from 'react'
import type { FC, Ref } from 'react'
import type {
Model,
ModelItem,
@ -24,6 +24,7 @@ export type TriggerProps = {
providerName?: string
modelId?: string
isInWorkflow?: boolean
settingsRef?: Ref<HTMLDivElement>
}
const Trigger: FC<TriggerProps> = ({
@ -32,6 +33,7 @@ const Trigger: FC<TriggerProps> = ({
providerName,
modelId,
isInWorkflow,
settingsRef,
}) => {
const { t } = useTranslation()
const { modelProviders } = useProviderContext()
@ -111,7 +113,7 @@ const Trigger: FC<TriggerProps> = ({
</div>
)}
</div>
<div className={cn('flex shrink-0 items-center justify-center rounded-r-lg p-2', isInWorkflow ? 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg' : 'bg-components-button-tertiary-bg')}>
<div ref={settingsRef} className={cn('flex shrink-0 items-center justify-center rounded-r-lg p-2', isInWorkflow ? 'border border-workflow-block-parma-bg bg-workflow-block-parma-bg' : 'bg-components-button-tertiary-bg')}>
<span className="i-ri-equalizer-2-line h-4 w-4 text-text-tertiary" />
</div>
</div>

View File

@ -406,6 +406,7 @@
"modelProvider.model": "Model",
"modelProvider.modelAndParameters": "Model and Parameters",
"modelProvider.modelHasBeenDeprecated": "This model has been deprecated",
"modelProvider.modelSettings": "Model Settings",
"modelProvider.models": "Models",
"modelProvider.modelsNum": "{{num}} Models",
"modelProvider.noModelFound": "No model found for {{model}}",

View File

@ -406,6 +406,7 @@
"modelProvider.model": "模型",
"modelProvider.modelAndParameters": "模型及参数",
"modelProvider.modelHasBeenDeprecated": "该模型已废弃",
"modelProvider.modelSettings": "模型设置",
"modelProvider.models": "模型列表",
"modelProvider.modelsNum": "{{num}} 个模型",
"modelProvider.noModelFound": "找不到模型 {{model}}",