Skip to content

Commit 03861bc

Browse files
refactor: stabilize selector preview cards (langgenius#36105)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent c34fc42 commit 03861bc

24 files changed

Lines changed: 609 additions & 307 deletions

File tree

eslint-suppressions.json

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,22 +2987,6 @@
29872987
"count": 1
29882988
}
29892989
},
2990-
"web/app/components/workflow/block-selector/featured-tools.tsx": {
2991-
"react/set-state-in-effect": {
2992-
"count": 2
2993-
},
2994-
"ts/no-explicit-any": {
2995-
"count": 2
2996-
}
2997-
},
2998-
"web/app/components/workflow/block-selector/featured-triggers.tsx": {
2999-
"react/set-state-in-effect": {
3000-
"count": 2
3001-
},
3002-
"ts/no-explicit-any": {
3003-
"count": 2
3004-
}
3005-
},
30062990
"web/app/components/workflow/block-selector/hooks.ts": {
30072991
"react/set-state-in-effect": {
30082992
"count": 1
@@ -3038,29 +3022,6 @@
30383022
"count": 1
30393023
}
30403024
},
3041-
"web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": {
3042-
"ts/no-explicit-any": {
3043-
"count": 1
3044-
}
3045-
},
3046-
"web/app/components/workflow/block-selector/tool/tool.tsx": {
3047-
"react/set-state-in-effect": {
3048-
"count": 2
3049-
}
3050-
},
3051-
"web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": {
3052-
"ts/no-explicit-any": {
3053-
"count": 1
3054-
}
3055-
},
3056-
"web/app/components/workflow/block-selector/trigger-plugin/item.tsx": {
3057-
"react/set-state-in-effect": {
3058-
"count": 2
3059-
},
3060-
"ts/no-explicit-any": {
3061-
"count": 1
3062-
}
3063-
},
30643025
"web/app/components/workflow/block-selector/types.ts": {
30653026
"erasable-syntax-only/enums": {
30663027
"count": 4

web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/index.spec.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,23 @@ describe('ModelSelector', () => {
118118
expect(triggerButton).toHaveAttribute('aria-expanded', 'false')
119119
})
120120

121+
it('should use the default model settings popup width when the trigger is narrow', () => {
122+
renderWithQueryClient(
123+
<div className="w-[355px]">
124+
<ModelSelector modelList={[makeModel()]} />
125+
</div>,
126+
)
127+
128+
fireEvent.click(screen.getByRole('combobox'))
129+
130+
expect(
131+
Array.from(document.body.querySelectorAll('[class]')).some(element =>
132+
element.className.includes('w-[432px]')
133+
&& element.className.includes('max-w-[432px]'),
134+
),
135+
).toBe(true)
136+
})
137+
121138
it('should not open popup when readonly', () => {
122139
renderWithQueryClient(<ModelSelector modelList={[makeModel()]} readonly />)
123140

web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup-item.spec.tsx

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ReactElement, ReactNode } from 'react'
22
import type { DefaultModel, Model, ModelItem } from '../../declarations'
33
import { Combobox } from '@langgenius/dify-ui/combobox'
4+
import { createPreviewCardHandle } from '@langgenius/dify-ui/preview-card'
45
import { fireEvent, render, screen } from '@testing-library/react'
56
import {
67
ConfigurationMethodEnum,
@@ -106,6 +107,11 @@ const makeProvider = (overrides: Record<string, unknown> = {}) => ({
106107
...overrides,
107108
})
108109

110+
const previewCardProps = () => ({
111+
previewCardHandle: createPreviewCardHandle(),
112+
onPreviewCardClose: vi.fn(),
113+
})
114+
109115
const createComboboxNode = (
110116
node: ReactElement,
111117
onValueChange = vi.fn(),
@@ -152,15 +158,15 @@ describe('PopupItem', () => {
152158
})
153159

154160
const { container } = renderWithCombobox(
155-
<PopupItem model={makeModel()} onHide={vi.fn()} />,
161+
<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />,
156162
)
157163

158164
expect(container.textContent).toBe('')
159165
})
160166

161167
it('should select the combobox value when clicking an active model', () => {
162168
const onValueChange = vi.fn()
163-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />, onValueChange)
169+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />, onValueChange)
164170

165171
fireEvent.click(screen.getByText('GPT-4'))
166172

@@ -170,10 +176,27 @@ describe('PopupItem', () => {
170176
)
171177
})
172178

179+
it('should close the shared preview before pressing an active model', () => {
180+
const onPreviewCardClose = vi.fn()
181+
renderWithCombobox(
182+
<PopupItem
183+
previewCardHandle={createPreviewCardHandle()}
184+
onPreviewCardClose={onPreviewCardClose}
185+
model={makeModel()}
186+
onHide={vi.fn()}
187+
/>,
188+
)
189+
190+
fireEvent.pointerDown(screen.getByText('GPT-4'))
191+
192+
expect(onPreviewCardClose).toHaveBeenCalledTimes(1)
193+
})
194+
173195
it('should not select the combobox value when model is not active', () => {
174196
const onValueChange = vi.fn()
175197
renderWithCombobox(
176198
<PopupItem
199+
{...previewCardProps()}
177200
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.disabled })] })}
178201
onHide={vi.fn()}
179202
/>,
@@ -188,7 +211,7 @@ describe('PopupItem', () => {
188211
it('should open model modal when clicking add on unconfigured model', () => {
189212
const onValueChange = vi.fn()
190213
const { rerender } = renderWithCombobox(
191-
<PopupItem model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
214+
<PopupItem {...previewCardProps()} model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })} onHide={vi.fn()} />,
192215
onValueChange,
193216
)
194217

@@ -206,6 +229,7 @@ describe('PopupItem', () => {
206229

207230
rerender(createComboboxNode(
208231
<PopupItem
232+
{...previewCardProps()}
209233
model={makeModel({
210234
models: [makeModelItem({ status: ModelStatusEnum.noConfigure, model_type: undefined as unknown as ModelTypeEnum })],
211235
})}
@@ -225,6 +249,7 @@ describe('PopupItem', () => {
225249
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
226250
renderWithCombobox(
227251
<PopupItem
252+
{...previewCardProps()}
228253
defaultModel={defaultModel}
229254
model={makeModel()}
230255
onHide={vi.fn()}
@@ -239,6 +264,7 @@ describe('PopupItem', () => {
239264

240265
renderWithCombobox(
241266
<PopupItem
267+
{...previewCardProps()}
242268
model={makeModel({
243269
label: { en_US: 'OpenAI only' } as Model['label'],
244270
models: [makeModelItem({ label: { en_US: 'GPT-4 only' } as ModelItem['label'] })],
@@ -252,7 +278,7 @@ describe('PopupItem', () => {
252278
})
253279

254280
it('should toggle collapsed state when clicking provider header', () => {
255-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
281+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
256282

257283
expect(screen.getByText('GPT-4'))!.toBeInTheDocument()
258284

@@ -266,7 +292,7 @@ describe('PopupItem', () => {
266292
})
267293

268294
it('should show credential name when using custom provider', () => {
269-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
295+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
270296

271297
expect(screen.getByText('my-api-key'))!.toBeInTheDocument()
272298
})
@@ -283,7 +309,7 @@ describe('PopupItem', () => {
283309
credits: 200,
284310
})
285311

286-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
312+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
287313

288314
expect(screen.getByText('stale-key'))!.toBeInTheDocument()
289315
expect(document.querySelector('.bg-components-badge-status-light-error-bg')).not.toBeNull()
@@ -309,7 +335,7 @@ describe('PopupItem', () => {
309335
credits: 0,
310336
})
311337

312-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
338+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
313339

314340
expect(screen.getByText(/modelProvider\.selector\.configureRequired/))!.toBeInTheDocument()
315341
})
@@ -331,7 +357,7 @@ describe('PopupItem', () => {
331357
credits: 200,
332358
})
333359

334-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
360+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
335361

336362
expect(screen.getByText(/modelProvider\.selector\.aiCredits/))!.toBeInTheDocument()
337363
})
@@ -356,15 +382,15 @@ describe('PopupItem', () => {
356382
credits: 0,
357383
})
358384

359-
renderWithCombobox(<PopupItem model={makeModel()} onHide={vi.fn()} />)
385+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={vi.fn()} />)
360386

361387
expect(screen.getByText(/modelProvider\.selector\.creditsExhausted/))!.toBeInTheDocument()
362388
})
363389

364390
it('should close the dropdown through dropdown content callbacks', () => {
365391
const onHide = vi.fn()
366392

367-
renderWithCombobox(<PopupItem model={makeModel()} onHide={onHide} />)
393+
renderWithCombobox(<PopupItem {...previewCardProps()} model={makeModel()} onHide={onHide} />)
368394

369395
fireEvent.click(screen.getByRole('button', { name: /my-api-key/ }))
370396
fireEvent.click(screen.getByRole('button', { name: 'close dropdown' }))

web/app/components/header/account-setting/model-provider-page/model-selector/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function ModelSelector({
130130
<ComboboxContent
131131
placement="bottom-start"
132132
sideOffset={4}
133-
popupClassName={cn('min-w-[320px] overflow-hidden rounded-xl', popupClassName)}
133+
popupClassName={cn('w-[432px] max-w-[432px] overflow-hidden rounded-xl', popupClassName)}
134134
>
135135
<Popup
136136
defaultModel={defaultModel}

web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
1-
import type { DefaultModel, Model } from '../declarations'
1+
import type { ComponentProps } from 'react'
2+
import type { DefaultModel, Model, ModelItem } from '../declarations'
23
import { cn } from '@langgenius/dify-ui/cn'
34
import { ComboboxGroup, ComboboxItem, ComboboxItemIndicator } from '@langgenius/dify-ui/combobox'
45
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
5-
import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
6+
import { PreviewCardTrigger } from '@langgenius/dify-ui/preview-card'
67
import { useCallback, useMemo, useState } from 'react'
78
import { useTranslation } from 'react-i18next'
89
import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce'
910
import { useModalContext } from '@/context/modal-context'
1011
import { useProviderContext } from '@/context/provider-context'
11-
import { ConfigurationMethodEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '../declarations'
12+
import { ConfigurationMethodEnum, ModelStatusEnum } from '../declarations'
1213
import { useLanguage, useUpdateModelList, useUpdateModelProviders } from '../hooks'
13-
import ModelBadge from '../model-badge'
1414
import ModelIcon from '../model-icon'
1515
import ModelName from '../model-name'
1616
import DropdownContent from '../provider-added-card/model-auth-dropdown/dropdown-content'
1717
import { useChangeProviderPriority } from '../provider-added-card/use-change-provider-priority'
1818
import { useCredentialPanelState } from '../provider-added-card/use-credential-panel-state'
19-
import { modelTypeFormat, sizeFormat } from '../utils'
20-
import FeatureIcon from './feature-icon'
19+
20+
export type ModelSelectorPreviewPayload = {
21+
provider: Model
22+
modelItem: ModelItem
23+
}
24+
25+
type PreviewCardHandle = NonNullable<ComponentProps<typeof PreviewCardTrigger>['handle']>
2126

2227
type PopupItemProps = {
2328
defaultModel?: DefaultModel
2429
model: Model
30+
previewCardHandle: PreviewCardHandle
31+
onPreviewCardClose: () => void
2532
onHide: () => void
2633
}
2734
function PopupItem({
2835
defaultModel,
2936
model,
37+
previewCardHandle,
38+
onPreviewCardClose,
3039
onHide,
3140
}: PopupItemProps) {
3241
const [collapsed, setCollapsed] = useState(false)
@@ -167,7 +176,11 @@ function PopupItem({
167176
)
168177
const itemRender = modelItem.status === ModelStatusEnum.noConfigure
169178
? (
170-
<div className={rowClassName} aria-disabled="true">
179+
<div
180+
className={rowClassName}
181+
aria-disabled="true"
182+
onPointerDown={onPreviewCardClose}
183+
>
171184
{rowContent}
172185
<button
173186
type="button"
@@ -186,67 +199,21 @@ function PopupItem({
186199
}}
187200
disabled={modelItem.status !== ModelStatusEnum.active}
188201
className={rowClassName}
202+
onPointerDown={onPreviewCardClose}
189203
>
190204
{rowContent}
191205
</ComboboxItem>
192206
)
193207

194208
return (
195-
<PreviewCard key={modelItem.model}>
196-
<PreviewCardTrigger
197-
delay={150}
198-
closeDelay={150}
199-
render={itemRender}
200-
/>
201-
<PreviewCardContent
202-
placement="right"
203-
popupClassName="w-[206px] bg-components-panel-bg-blur p-3 shadow-none backdrop-blur-xs"
204-
>
205-
<div className="flex flex-col gap-1">
206-
<div className="flex flex-col items-start gap-2">
207-
<ModelIcon
208-
className={cn('h-5 w-5 shrink-0')}
209-
provider={model}
210-
modelName={modelItem.model}
211-
/>
212-
<div className="system-md-medium text-wrap wrap-break-word text-text-primary">{modelItem.label[language] || modelItem.label.en_US}</div>
213-
</div>
214-
<div className="flex flex-wrap gap-1">
215-
{!!modelItem.model_type && (
216-
<ModelBadge>
217-
{modelTypeFormat(modelItem.model_type)}
218-
</ModelBadge>
219-
)}
220-
{!!modelItem.model_properties.mode && (
221-
<ModelBadge>
222-
{(modelItem.model_properties.mode as string).toLocaleUpperCase()}
223-
</ModelBadge>
224-
)}
225-
{!!modelItem.model_properties.context_size && (
226-
<ModelBadge>
227-
{sizeFormat(modelItem.model_properties.context_size as number)}
228-
</ModelBadge>
229-
)}
230-
</div>
231-
{[ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank].includes(modelItem.model_type as ModelTypeEnum)
232-
&& modelItem.features?.some(feature => [ModelFeatureEnum.vision, ModelFeatureEnum.audio, ModelFeatureEnum.video, ModelFeatureEnum.document].includes(feature))
233-
&& (
234-
<div className="pt-2">
235-
<div className="mb-1 system-2xs-medium-uppercase text-text-tertiary">{t('model.capabilities', { ns: 'common' })}</div>
236-
<div className="flex flex-wrap gap-1">
237-
{modelItem.features?.map(feature => (
238-
<FeatureIcon
239-
key={feature}
240-
feature={feature}
241-
showFeaturesLabel
242-
/>
243-
))}
244-
</div>
245-
</div>
246-
)}
247-
</div>
248-
</PreviewCardContent>
249-
</PreviewCard>
209+
<PreviewCardTrigger
210+
key={modelItem.model}
211+
delay={150}
212+
closeDelay={150}
213+
handle={previewCardHandle}
214+
payload={{ provider: model, modelItem }}
215+
render={itemRender}
216+
/>
250217
)
251218
})}
252219
</ComboboxGroup>

0 commit comments

Comments
 (0)