refactor(ui): simplify scroll area API (#39857)

This commit is contained in:
yyh
2026-07-31 12:43:11 +00:00
committed by GitHub
parent c62ab071eb
commit 7e30aba12a
59 changed files with 1467 additions and 1463 deletions
+40 -30
View File
@@ -1,7 +1,13 @@
'use client'
import type { ResourceOpenScope } from '@/models/access-control'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
@@ -89,35 +95,39 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP
)
return (
<ScrollArea
className="h-full bg-background-default-subtle"
slotClassNames={{ viewport: 'overscroll-contain' }}
>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-xl-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-sm-regular text-text-tertiary">
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={appAccessRules}
userAccessSettings={appUserAccessSettings}
isLoadingRules={isLoadingAppAccessRules}
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={maintainerId}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
<ScrollArea className="h-full bg-background-default-subtle">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-xl-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-sm-regular text-text-tertiary">
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={appAccessRules}
userAccessSettings={appUserAccessSettings}
isLoadingRules={isLoadingAppAccessRules}
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={maintainerId}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}
+356 -338
View File
@@ -8,7 +8,13 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
import { Field, FieldControl, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { Input } from '@langgenius/dify-ui/input'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import {
Select,
SelectContent,
@@ -369,355 +375,367 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onFormSubmit={handleFormSubmit}
>
{/* form body */}
<ScrollArea
className="relative min-h-0"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-w-0 space-y-5 px-6 py-3',
}}
>
{/* name & icon */}
<div className="flex gap-4">
<Field name="title" className="grow">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
</FieldLabel>
<FieldControl
value={inputInfo.title}
onValueChange={(value) => setInputInfo((item) => ({ ...item, title: value }))}
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
/>
</Field>
<AppIcon
size="xxl"
onClick={() => {
setShowAppIconPicker(true)
}}
className="mt-2 cursor-pointer"
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
/>
</div>
{/* description */}
<Field name="description">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
</FieldLabel>
<Textarea
value={inputInfo.desc}
onValueChange={onDesChange}
placeholder={
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
<FieldDescription>
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
{/* answer icon */}
{isChat && (
<Field name="use_icon_as_answer_icon" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
<Switch
checked={inputInfo.use_icon_as_answer_icon}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
}
/>
</div>
<FieldDescription>
{t(($) => $['answerIcon.description'], { ns: 'app' })}
</FieldDescription>
</Field>
)}
{/* language */}
<div className="flex items-center">
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
</div>
<Select
value={selectedLanguage?.value ?? null}
onValueChange={handleLanguageChange}
>
<SelectTrigger
aria-label={t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
size="medium"
className="w-50"
>
{selectedLanguage?.name ?? t(($) => $['placeholder.select'], { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* theme color */}
{isChat && (
<div className="flex items-center">
<div className="grow">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
</div>
<div className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], { ns: 'appOverview' })}
</div>
</div>
<Field name="chat_color_theme" className="w-50 shrink-0">
<FieldControl
className="mb-1"
value={inputInfo.chatColorTheme ?? ''}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
}
placeholder="E.g #A020F0"
/>
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
<span>
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
ns: 'appOverview',
})}
</span>
<Switch
checked={inputInfo.chatColorThemeInverted}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
}
></Switch>
</div>
</Field>
</div>
)}
{/* workflow detail */}
<Field name="show_workflow_steps" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
</FieldLabel>
<Switch
disabled={
!(
appInfo.mode === AppModeEnum.WORKFLOW ||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
)
}
checked={inputInfo.show_workflow_steps}
onCheckedChange={(v) => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
/>
</div>
<FieldDescription>
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
<div className="space-y-5">
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div
id={inputPlaceholderLabelId}
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
ns: 'appOverview',
})}
</div>
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
</div>
<p
id={inputPlaceholderDescriptionId}
className="pb-0.5 body-xs-regular text-text-tertiary"
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
ns: 'appOverview',
})}
</p>
{isCloudSandboxPlan ? (
<Tooltip>
<TooltipTrigger render={inputPlaceholderField} />
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
) : (
inputPlaceholderField
)}
{!isCloudSandboxPlan && (
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
</div>
)}
</div>
)}
{/* copyright */}
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
</div>
{/* upgrade button */}
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
{webappCopyrightEnabled ? (
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
<ScrollArea className="relative min-h-0">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent style={{ minWidth: 0 }} className="space-y-5 px-6 py-3">
{/* name & icon */}
<div className="flex gap-4">
<Field name="title" className="grow">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
</FieldLabel>
<FieldControl
value={inputInfo.title}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, title: value }))
}
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
/>
) : (
<Tooltip>
<TooltipTrigger
render={
<div>
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
disabled
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
</div>
}
/>
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
)}
</Field>
<AppIcon
size="xxl"
onClick={() => {
setShowAppIconPicker(true)
}}
className="mt-2 cursor-pointer"
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
/>
</div>
<p className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
</p>
{copyrightSwitchValue && (
<Input
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
{/* description */}
<Field name="description">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
</FieldLabel>
<Textarea
value={inputInfo.desc}
onValueChange={onDesChange}
placeholder={
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
<FieldDescription>
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
{/* answer icon */}
{isChat && (
<Field name="use_icon_as_answer_icon" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
<Switch
checked={inputInfo.use_icon_as_answer_icon}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
}
/>
</div>
<FieldDescription>
{t(($) => $['answerIcon.description'], { ns: 'app' })}
</FieldDescription>
</Field>
)}
</div>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
{/* language */}
<div className="flex items-center">
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
</div>
<Select
value={selectedLanguage?.value ?? null}
onValueChange={handleLanguageChange}
>
<SelectTrigger
aria-label={t(($) => $[`${prefixSettings}.language`], {
ns: 'appOverview',
})}
size="medium"
className="w-50"
>
{selectedLanguage?.name ??
t(($) => $['placeholder.select'], { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
ns="appOverview"
components={{
privacyPolicyLink: (
<Link
href="https://dify.ai/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-text-accent"
{/* theme color */}
{isChat && (
<div className="flex items-center">
<div className="grow">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
</div>
<div className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], {
ns: 'appOverview',
})}
</div>
</div>
<Field name="chat_color_theme" className="w-50 shrink-0">
<FieldControl
className="mb-1"
value={inputInfo.chatColorTheme ?? ''}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
}
placeholder="E.g #A020F0"
/>
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
<span>
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
ns: 'appOverview',
})}
</span>
<Switch
checked={inputInfo.chatColorThemeInverted}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
}
></Switch>
</div>
</Field>
</div>
)}
{/* workflow detail */}
<Field name="show_workflow_steps" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
</FieldLabel>
<Switch
disabled={
!(
appInfo.mode === AppModeEnum.WORKFLOW ||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
)
}
checked={inputInfo.show_workflow_steps}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, show_workflow_steps: v })
}
/>
</div>
<FieldDescription>
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
<div className="space-y-5">
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div
id={inputPlaceholderLabelId}
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
ns: 'appOverview',
})}
</div>
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
</div>
<p
id={inputPlaceholderDescriptionId}
className="pb-0.5 body-xs-regular text-text-tertiary"
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
ns: 'appOverview',
})}
</p>
{isCloudSandboxPlan ? (
<Tooltip>
<TooltipTrigger render={inputPlaceholderField} />
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
) : (
inputPlaceholderField
)}
{!isCloudSandboxPlan && (
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
</div>
)}
</div>
)}
{/* copyright */}
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
</div>
{/* upgrade button */}
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
{webappCopyrightEnabled ? (
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
),
}}
/>
</p>
<Input
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], { ns: 'appOverview' })}
) : (
<Tooltip>
<TooltipTrigger
render={
<div>
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
disabled
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
</div>
}
/>
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
)}
</div>
<p className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
</p>
{copyrightSwitchValue && (
<Input
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
)}
</div>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
ns="appOverview"
components={{
privacyPolicyLink: (
<Link
href="https://dify.ai/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-text-accent"
/>
),
}}
/>
</p>
<Input
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
ns: 'appOverview',
})}
</p>
<Textarea
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
}
placeholder={
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
ns: 'appOverview',
})}
</p>
<Textarea
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
}
placeholder={
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
{/* footer */}
<div className="flex shrink-0 justify-end p-6 pt-5">
+3 -3
View File
@@ -3,9 +3,9 @@ import type { FC } from 'react'
import type { Category } from './types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -51,7 +51,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
}}
>
<DialogContent className="inset-0 size-full max-h-none max-w-none translate-0 overflow-hidden rounded-none border-none bg-saas-background p-0 shadow-none">
<ScrollAreaRoot className="relative h-full w-full overflow-hidden">
<ScrollArea className="relative h-full w-full overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="min-h-full min-w-300">
<div className="relative grid min-h-full grid-rows-[1fr_auto_auto_1fr] overflow-hidden">
@@ -85,7 +85,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
<ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar>
<ScrollAreaCorner className="bg-saas-background" />
</ScrollAreaRoot>
</ScrollArea>
</DialogContent>
</Dialog>
)
@@ -1,7 +1,13 @@
'use client'
import type { ResourceOpenScope } from '@/models/access-control'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
@@ -103,35 +109,39 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
if (!canAccessConfig) return <Loading type="app" />
return (
<ScrollArea
className="h-full bg-background-default-subtle"
slotClassNames={{ viewport: 'overscroll-contain' }}
>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-sm-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={datasetAccessRules}
userAccessSettings={datasetUserAccessSettings}
isLoadingRules={isLoadingDatasetAccessRules}
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={dataset?.maintainer}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
<ScrollArea className="h-full bg-background-default-subtle">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-sm-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={datasetAccessRules}
userAccessSettings={datasetUserAccessSettings}
isLoadingRules={isLoadingDatasetAccessRules}
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={dataset?.maintainer}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}
+3 -3
View File
@@ -15,8 +15,8 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -193,7 +193,7 @@ const SideBar = () => {
)}
{!isFold ? (
<div className="min-h-0 flex-1">
<ScrollAreaRoot className="h-full">
<ScrollArea className="h-full">
<ScrollAreaViewport
ref={scrollRef}
aria-busy={installedAppsQuery.isFetchingNextPage}
@@ -219,7 +219,7 @@ const SideBar = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
) : (
<div
+7 -7
View File
@@ -24,8 +24,8 @@ import {
} from '@langgenius/dify-ui/dialog'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -443,11 +443,11 @@ function GotoAnythingDialog() {
<AutocompleteStatus className="sr-only">{autocompleteStatus}</AutocompleteStatus>
<ScrollAreaRoot
aria-busy={isLoading || undefined}
className="relative h-60 min-h-0 overflow-hidden"
>
<ScrollAreaViewport className="scroll-py-1 overscroll-contain">
<ScrollArea className="relative h-60 min-h-0 overflow-hidden">
<ScrollAreaViewport
aria-busy={isLoading || undefined}
className="scroll-py-1 overscroll-contain"
>
<ScrollAreaContent
className="min-h-full w-full max-w-full"
style={{ minWidth: '100%' }}
@@ -576,7 +576,7 @@ function GotoAnythingDialog() {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
<Footer
resultCount={autocompleteResultCount}
@@ -2,7 +2,13 @@
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useRef } from 'react'
@@ -225,37 +231,44 @@ export default function AccountSetting({
</div>
</div>
<div className="relative flex min-h-0 w-206 min-w-0">
<ScrollArea
ref={scrollContainerRef}
className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg"
slotClassNames={{
viewport: 'overscroll-contain overflow-x-hidden',
content: 'min-h-full min-w-0 w-full max-w-full pb-4',
}}
>
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
{activeItem?.title ?? activeItem?.name}
{activeItem?.description && (
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
{activeItem?.description}
<ScrollArea className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg">
<ScrollAreaViewport
ref={scrollContainerRef}
style={{ overflowX: 'hidden' }}
className="overscroll-contain"
>
<ScrollAreaContent
style={{ minWidth: 0 }}
className="min-h-full w-full max-w-full pb-4"
>
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
{activeItem?.title ?? activeItem?.name}
{activeItem?.description && (
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
{activeItem?.description}
</div>
)}
</div>
)}
</div>
</div>
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
<PermissionsPage containerRef={scrollContainerRef} />
)}
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
<WorkflowLogArchivesPage />
)}
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
</div>
</div>
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
<PermissionsPage containerRef={scrollContainerRef} />
)}
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
<WorkflowLogArchivesPage />
)}
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</div>
@@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { ComboboxInput, ComboboxInputGroup } from '@langgenius/dify-ui/combobox'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -75,18 +75,19 @@ type ModelSelectorScrollBodyProps = {
export function ModelSelectorScrollBody({ children, label }: ModelSelectorScrollBodyProps) {
return (
<ScrollAreaRoot className="relative min-h-0 overflow-hidden overscroll-contain">
<ScrollArea className="relative min-h-0 overflow-hidden">
<ScrollAreaViewport
aria-label={label}
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overflow-x-hidden overscroll-contain"
style={{ overflowX: 'hidden' }}
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overscroll-contain"
role="region"
>
<ScrollAreaContent className="min-w-0 overflow-x-hidden">{children}</ScrollAreaContent>
<ScrollAreaContent style={{ minWidth: 0 }}>{children}</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="z-2">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
}
@@ -3,7 +3,13 @@
import type { PermissionGroup } from '@/models/access-control'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -75,113 +81,114 @@ const PermissionGroupList = ({
className,
)}
>
<ScrollArea
className="h-full overflow-hidden rounded-xl"
slotClassNames={{
viewport: 'overscroll-contain',
content: groups.length === 0 ? 'h-full' : undefined,
}}
>
{groups.length === 0 ? (
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
</div>
) : (
<div className="flex flex-col">
{groups.map((group) => {
const expanded = expandedGroupKeysMerged.has(group.group_key)
const selectedCount = group.permissions.reduce(
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
0,
)
const totalCount = group.permissions.length
const allSelected = totalCount > 0 && selectedCount === totalCount
<ScrollArea className="h-full overflow-hidden rounded-xl">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className={groups.length === 0 ? 'h-full' : undefined}>
{groups.length === 0 ? (
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
</div>
) : (
<div className="flex flex-col">
{groups.map((group) => {
const expanded = expandedGroupKeysMerged.has(group.group_key)
const selectedCount = group.permissions.reduce(
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
0,
)
const totalCount = group.permissions.length
const allSelected = totalCount > 0 && selectedCount === totalCount
return (
<div key={group.group_key} className="border-b border-b-divider-subtle">
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
<button
type="button"
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-expanded={expanded}
onClick={() => toggleGroupExpanded(group.group_key)}
>
{group.group_name}
</button>
{!readonly && totalCount > 0 && (
<button
type="button"
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
onClick={() => toggleGroupSelection(group, selectedCount)}
>
{allSelected
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
</button>
)}
{selectedCount > 0 && (
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
{selectedCount}/{totalCount}
</span>
)}
<button
type="button"
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-label={
expanded
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
}
onClick={() => toggleGroupExpanded(group.group_key)}
>
<span
aria-hidden
className={cn(
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{expanded && (
<div className="flex flex-col">
{group.permissions.map((permission) => {
const checked = selectedSet.has(permission.key)
return (
<div
key={permission.key}
className={cn(
'flex min-h-9 items-center gap-3 px-3 py-1.5',
readonly
? 'cursor-default'
: 'cursor-pointer hover:bg-state-base-hover',
checked && 'bg-state-accent-hover',
checked && !readonly && 'hover:bg-state-accent-hover',
)}
onClick={() => togglePermission(permission.key)}
return (
<div key={group.group_key} className="border-b border-b-divider-subtle">
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
<button
type="button"
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-expanded={expanded}
onClick={() => toggleGroupExpanded(group.group_key)}
>
{group.group_name}
</button>
{!readonly && totalCount > 0 && (
<button
type="button"
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
onClick={() => toggleGroupSelection(group, selectedCount)}
>
<Checkbox
checked={checked}
disabled={readonly}
className="shrink-0"
onCheckedChange={() => {
togglePermission(permission.key)
}}
/>
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
{permission.name}
</span>
</div>
)
})}
{allSelected
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
</button>
)}
{selectedCount > 0 && (
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
{selectedCount}/{totalCount}
</span>
)}
<button
type="button"
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-label={
expanded
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
}
onClick={() => toggleGroupExpanded(group.group_key)}
>
<span
aria-hidden
className={cn(
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{expanded && (
<div className="flex flex-col">
{group.permissions.map((permission) => {
const checked = selectedSet.has(permission.key)
return (
<div
key={permission.key}
className={cn(
'flex min-h-9 items-center gap-3 px-3 py-1.5',
readonly
? 'cursor-default'
: 'cursor-pointer hover:bg-state-base-hover',
checked && 'bg-state-accent-hover',
checked && !readonly && 'hover:bg-state-accent-hover',
)}
onClick={() => togglePermission(permission.key)}
>
<Checkbox
checked={checked}
disabled={readonly}
className="shrink-0"
onCheckedChange={() => {
togglePermission(permission.key)
}}
/>
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
{permission.name}
</span>
</div>
)
})}
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
)
})}
</div>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)
@@ -5,7 +5,13 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocale } from '@/context/i18n'
@@ -227,99 +233,102 @@ const WorkspaceRoleCheckboxList = ({
</div>
</div>
<ScrollArea
ref={containerRef}
className="min-h-0 flex-1"
slotClassNames={{ viewport: 'px-3 overscroll-contain' }}
>
{rolesLoading ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.loading'], { ns: 'permission' })}
</div>
) : filteredRoles.length === 0 ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
</div>
) : (
<>
{allowMultipleRoles ? (
<ul className="flex flex-col gap-0.5 pb-2">
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
const handleToggle = () => toggleRole(role)
return (
<li key={role.id}>
<div
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={cn(
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
handleToggle()
}
}}
>
<Checkbox
checked={checked}
disabled={disabled}
className="pointer-events-none mt-0.5"
/>
{renderRoleText(role)}
</div>
</li>
)
})}
</ul>
<ScrollArea className="min-h-0 flex-1">
<ScrollAreaViewport ref={containerRef} className="overscroll-contain px-3">
<ScrollAreaContent>
{rolesLoading ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.loading'], { ns: 'permission' })}
</div>
) : filteredRoles.length === 0 ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
</div>
) : (
<RadioGroup
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
value={selectedRoleIds[0] ?? ''}
onValueChange={handleRadioValueChange}
className="flex-col items-stretch gap-0.5 pb-2"
render={<ul />}
>
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
<>
{allowMultipleRoles ? (
<ul className="flex flex-col gap-0.5 pb-2">
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
const handleToggle = () => toggleRole(role)
return (
<li key={role.id}>
<RadioItem
value={role.id}
disabled={disabled}
nativeButton
render={
<button
type="button"
return (
<li key={role.id}>
<div
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={cn(
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
/>
}
>
<RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)}
</RadioItem>
</li>
)
})}
</RadioGroup>
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
handleToggle()
}
}}
>
<Checkbox
checked={checked}
disabled={disabled}
className="pointer-events-none mt-0.5"
/>
{renderRoleText(role)}
</div>
</li>
)
})}
</ul>
) : (
<RadioGroup
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
value={selectedRoleIds[0] ?? ''}
onValueChange={handleRadioValueChange}
className="flex-col items-stretch gap-0.5 pb-2"
render={<ul />}
>
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
return (
<li key={role.id}>
<RadioItem
value={role.id}
disabled={disabled}
nativeButton
render={
<button
type="button"
className={cn(
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
/>
}
>
<RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)}
</RadioItem>
</li>
)
})}
</RadioGroup>
)}
<div ref={anchorRef} className="h-0" />
</>
)}
<div ref={anchorRef} className="h-0" />
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</>
)
+33 -23
View File
@@ -5,7 +5,13 @@ import type { IntegrationSection } from '@/app/components/integrations/routes'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { cn } from '@langgenius/dify-ui/cn'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UpdateSettingDialog from '@/app/components/header/account-setting/update-setting-dialog'
@@ -339,28 +345,32 @@ export default function IntegrationsPage({
/>
</div>
) : (
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={scrollAreaLabel}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full',
}}
>
<IntegrationSectionRenderer
key={section}
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={scrollAreaLabel}
className="overscroll-contain"
role={scrollAreaLabel ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<IntegrationSectionRenderer
key={section}
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)}
</section>
@@ -1,7 +1,13 @@
'use client'
import type { ReactNode } from 'react'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
type IntegrationSectionLayoutProps = {
bodyClassName?: string
@@ -15,15 +21,19 @@ export function IntegrationSectionLayout({
label,
}: IntegrationSectionLayoutProps) {
return (
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={label}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full',
}}
>
<div className={bodyClassName}>{children}</div>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={label}
className="overscroll-contain"
role={label ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<div className={bodyClassName}>{children}</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}
@@ -5,8 +5,8 @@ import type { ToolsContentInset } from '@/app/components/tools/content-inset'
import type { Collection } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -203,7 +203,7 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
const body = (
<>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-hidden bg-components-panel-bg">
<ScrollAreaRoot className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
<ScrollArea className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
<ScrollAreaViewport
ref={containerRef}
aria-label={t(($) => $['menus.tools'], { ns: 'common' })}
@@ -261,7 +261,7 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail
@@ -16,8 +16,8 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -213,15 +213,16 @@ const WebAppsSectionContent = () => {
</div>
)}
{appsExpanded && (
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden overscroll-contain">
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
ref={scrollRef}
aria-busy={installedAppsQuery.isPending || installedAppsQuery.isFetchingNextPage}
aria-label={t(($) => $['sidebar.webApps'], { ns: 'explore' })}
className="overflow-x-hidden"
style={{ overflowX: 'hidden' }}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className="w-full max-w-full min-w-0! px-2">
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full px-2">
{installedAppsQuery.isPending && <WebAppsSkeleton />}
{!installedAppsQuery.isPending && installedAppsQuery.isError && (
<div
@@ -277,7 +278,7 @@ const WebAppsSectionContent = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)}
<AlertDialog
open={uninstallDialogAppId !== null}
@@ -1,7 +1,13 @@
import type { ReactNode } from 'react'
import type { PluginStatus } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import PluginItem from './plugin-item'
type PluginSectionProps = {
@@ -36,36 +42,36 @@ function PluginSection({
return (
<>
<div className="sticky top-0 flex items-start justify-between pt-1 pr-1 pl-2 system-sm-semibold-uppercase text-text-secondary">
<span className="flex h-6 items-center">
<h3 className="flex h-6 items-center">
{title} ({count})
</span>
</h3>
{headerAction}
</div>
<ScrollArea
className="overflow-hidden"
label={title}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-w-0',
}}
>
{plugins.map((plugin) => (
<PluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
statusIcon={statusIcon}
statusText={plugin.message || defaultStatusText}
statusClassName={statusClassName}
action={renderItemAction?.(plugin)}
onClear={
onClearSingle
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
: undefined
}
/>
))}
<ScrollArea className="overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent style={{ minWidth: 0 }}>
{plugins.map((plugin) => (
<PluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
statusIcon={statusIcon}
statusText={plugin.message || defaultStatusText}
statusClassName={statusClassName}
action={renderItemAction?.(plugin)}
onClear={
onClearSingle
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
: undefined
}
/>
))}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</>
)
@@ -1,6 +1,12 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { useGetLanguage } from '@/context/i18n'
import ErrorPluginItem from './error-plugin-item'
@@ -36,84 +42,89 @@ function PluginTaskList({
className="w-90 max-w-[calc(100vw-32px)] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
data-testid="plugin-task-list"
>
<ScrollArea
className="max-h-105 overflow-hidden"
label={t(($) => $['task.installing'], { ns: 'plugin' })}
slotClassNames={{
viewport: 'max-h-[420px] overscroll-contain',
content: 'w-full! max-w-full! min-w-0! overflow-x-hidden! pr-3',
}}
>
{runningPlugins.length > 0 && (
<PluginSection
title={runningSectionTitle}
count={runningPlugins.length}
plugins={runningPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
}
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
/>
)}
<ScrollArea className="max-h-105 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['task.installing'], { ns: 'plugin' })}
style={{ overflowX: 'hidden' }}
className="max-h-[420px] overscroll-contain"
role="region"
>
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full pr-3">
{runningPlugins.length > 0 && (
<PluginSection
title={runningSectionTitle}
count={runningPlugins.length}
plugins={runningPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
}
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
/>
)}
{successPlugins.length > 0 && (
<PluginSection
title={successSectionTitle}
count={successPlugins.length}
plugins={successPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />}
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
statusClassName="text-text-success"
headerAction={
<Button
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
}
onClearSingle={onClearSingle}
/>
)}
{successPlugins.length > 0 && (
<PluginSection
title={successSectionTitle}
count={successPlugins.length}
plugins={successPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />
}
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
statusClassName="text-text-success"
headerAction={
<Button
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
}
onClearSingle={onClearSingle}
/>
)}
{errorPlugins.length > 0 && (
<>
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
{errorSectionTitle} ({errorPlugins.length})
<Button
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearErrors}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
</div>
<div
aria-label={errorSectionTitle}
className="w-full max-w-full min-w-0 overflow-hidden"
role="region"
>
{errorPlugins.map((plugin) => (
<ErrorPluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
/>
))}
</div>
</>
)}
{errorPlugins.length > 0 && (
<>
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
<h3>
{errorSectionTitle} ({errorPlugins.length})
</h3>
<Button
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearErrors}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
</div>
<div className="w-full max-w-full min-w-0 overflow-hidden">
{errorPlugins.map((plugin) => (
<ErrorPluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
/>
))}
</div>
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)
@@ -5,8 +5,8 @@ import type { Collection } from '@/app/components/tools/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -103,7 +103,7 @@ const PluginsPanelResults = ({
const { t } = useTranslation()
return (
<ScrollAreaRoot
<ScrollArea
className={cn(
'min-h-0 grow self-stretch overflow-hidden bg-components-panel-bg',
contentFrameClassName,
@@ -169,7 +169,7 @@ const PluginsPanelResults = ({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
}
@@ -16,7 +16,13 @@ import { Field, FieldItem, FieldLabel } from '@langgenius/dify-ui/field'
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
import { Input } from '@langgenius/dify-ui/input'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
@@ -112,185 +118,203 @@ export default function ConfigCredential({ positionCenter, credential, onChange,
/>
</div>
</div>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'space-y-4 pt-2 pr-8 pl-6',
}}
>
<Field name="auth_type" className="contents">
<Fieldset
render={
<RadioGroup<AuthType>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.auth_type}
onValueChange={handleAuthTypeChange}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
/>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
ns: 'tools',
})}
value={AuthType.apiKeyHeader}
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
/>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
ns: 'tools',
})}
value={AuthType.apiKeyQuery}
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
/>
</Fieldset>
</Field>
{tempCredential.auth_type === AuthType.apiKeyHeader && (
<>
<Field name="api_key_header_prefix" className="contents">
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="space-y-4 pt-2 pr-8 pl-6">
<Field name="auth_type" className="contents">
<Fieldset
render={
<RadioGroup<AuthHeaderPrefix>
<RadioGroup<AuthType>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.api_key_header_prefix}
onValueChange={(value) =>
setTempCredential({ ...tempCredential, api_key_header_prefix: value })
}
value={tempCredential.auth_type}
onValueChange={handleAuthTypeChange}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.basic}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
}
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.bearer}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
}
value={AuthType.apiKeyHeader}
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.custom}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
}
value={AuthType.apiKeyQuery}
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
/>
</Fieldset>
</Field>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_header}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_header: e.target.value })
}
placeholder={t(($) => $['createTool.authMethod.types.apiKeyPlaceholder'], {
ns: 'tools',
})!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyQuery && (
<>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.queryParamTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_query_param}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_query_param: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyHeader && (
<>
<Field name="api_key_header_prefix" className="contents">
<Fieldset
render={
<RadioGroup<AuthHeaderPrefix>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.api_key_header_prefix}
onValueChange={(value) =>
setTempCredential({
...tempCredential,
api_key_header_prefix: value,
})
}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.basic}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.bearer}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.custom}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
}
/>
</Fieldset>
</Field>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_header}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_header: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiKeyPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_value: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyQuery && (
<>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
</Infotip>
</div>
<Input
value={tempCredential.api_key_query_param}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_query_param: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_value: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div className="mt-4 flex shrink-0 justify-end space-x-2 px-6 py-4">
<Button onClick={onHide}>
@@ -8,8 +8,8 @@ import {
PreviewCardTrigger,
} from '@langgenius/dify-ui/preview-card'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -154,7 +154,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
) : !snippets.length ? (
<SnippetEmptyState />
) : (
<ScrollAreaRoot className="relative max-h-120 max-w-125 overflow-hidden">
<ScrollArea className="relative max-h-120 max-w-125 overflow-hidden">
<ScrollAreaViewport ref={viewportRef}>
<ScrollAreaContent className="p-1">
{snippets.map((item) => {
@@ -183,7 +183,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
<ScrollAreaScrollbar orientation="vertical">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
return (
@@ -7,8 +7,8 @@ import type {
} from '@/app/components/workflow/block-selector/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -171,7 +171,7 @@ const TriggerPluginItem: FC<Props> = ({
</BlockSelectorRow>
{!notShowProvider && hasAction && !isFold && (
<ScrollAreaRoot className="relative max-h-60 overflow-hidden overscroll-contain">
<ScrollArea className="relative max-h-60 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['tabs.allTriggers'], { ns: 'workflow' })}
className="max-h-60 overscroll-contain"
@@ -194,7 +194,7 @@ const TriggerPluginItem: FC<Props> = ({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)}
</div>
</div>
@@ -1,6 +1,12 @@
'use client'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
@@ -49,35 +55,40 @@ export function AgentAccessPage({ agentId }: AgentAccessPageProps) {
</div>
</header>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
content: 'px-6 pt-2 pb-8',
}}
>
<div className="w-full min-w-0 space-y-6">
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<WebAppAccessCard
agent={agentQuery.data}
agentId={agentId}
isLoading={agentQuery.isPending}
/>
<ServiceApiAccessCard agentId={agentId} />
</div>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport>
<ScrollAreaContent className="px-6 pt-2 pb-8">
<div className="w-full min-w-0 space-y-6">
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<WebAppAccessCard
agent={agentQuery.data}
agentId={agentId}
isLoading={agentQuery.isPending}
/>
<ServiceApiAccessCard agentId={agentId} />
</div>
<section aria-labelledby="agent-workflow-access-title">
<div className="mb-3">
<h3 id="agent-workflow-access-title" className="system-md-semibold text-text-primary">
{t(($) => $['agentDetail.access.workflow.title'])}
</h3>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.access.workflow.description'])}
</p>
<section aria-labelledby="agent-workflow-access-title">
<div className="mb-3">
<h3
id="agent-workflow-access-title"
className="system-md-semibold text-text-primary"
>
{t(($) => $['agentDetail.access.workflow.title'])}
</h3>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.access.workflow.description'])}
</p>
</div>
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
</section>
</div>
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
</section>
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</AgentDetailSectionSurface>
)
@@ -687,7 +687,7 @@ describe('AgentConfigurePage', () => {
)
})
it('should expose a single configure workspace region after loading', () => {
it('should leave the configure landmark to the orchestrate panel after loading', () => {
const queryClient = new QueryClient()
mocks.queryState.composer = {
data: {},
@@ -705,11 +705,8 @@ describe('AgentConfigurePage', () => {
)
expect(
screen.getAllByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).toHaveLength(1)
expect(
screen.getByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).toBeVisible()
screen.queryByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).not.toBeInTheDocument()
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toBeInTheDocument()
})
@@ -13,7 +13,13 @@ import {
FileTreeLabel,
FileTreeList,
} from '@langgenius/dify-ui/file-tree'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { Fragment } from 'react'
type AgentFileTreeFolderOpenStrategy = (context: { file: AgentFileNode; depth: number }) => boolean
@@ -133,8 +139,7 @@ export function AgentFileTree({
selectedFileId,
id,
treeLabel,
label,
labelledBy,
treeLabelledBy,
header,
className,
scrollAreaClassName,
@@ -153,8 +158,7 @@ export function AgentFileTree({
selectedFileId?: string
id?: string
treeLabel?: string
label?: string
labelledBy?: string
treeLabelledBy?: string
header?: ReactNode
className?: string
scrollAreaClassName?: string
@@ -174,35 +178,36 @@ export function AgentFileTree({
{header}
<ScrollArea
className={cn('min-h-0 w-full max-w-full flex-1 overflow-hidden', scrollAreaClassName)}
label={label}
labelledBy={labelledBy}
slotClassNames={{
viewport: 'max-h-[inherit]',
content: 'w-full max-w-full min-w-0!',
scrollbar: 'hidden',
}}
>
<FileTree
id={id}
<ScrollAreaViewport
aria-label={treeLabel}
className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}
aria-labelledby={treeLabelledBy}
className="max-h-[inherit]"
role={treeLabel || treeLabelledBy ? 'region' : undefined}
>
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
<AgentFileTreeRows
files={files}
selectedFileId={selectedFileId}
depth={1}
folderOpenStrategy={folderOpenStrategy}
folderOpenState={folderOpenState}
onFolderOpenChange={onFolderOpenChange}
onFolderDoubleClick={onFolderDoubleClick}
onFolderOpen={onFolderOpen}
renderFile={renderFile}
renderFolderSuffix={renderFolderSuffix}
renderFolderPanel={renderFolderPanel}
/>
</FileTreeList>
</FileTree>
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full">
<FileTree id={id} className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}>
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
<AgentFileTreeRows
files={files}
selectedFileId={selectedFileId}
depth={1}
folderOpenStrategy={folderOpenStrategy}
folderOpenState={folderOpenState}
onFolderOpenChange={onFolderOpenChange}
onFolderDoubleClick={onFolderDoubleClick}
onFolderOpen={onFolderOpen}
renderFile={renderFile}
renderFolderSuffix={renderFolderSuffix}
renderFolderPanel={renderFolderPanel}
/>
</FileTreeList>
</FileTree>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="hidden">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)
@@ -9,7 +9,13 @@ import type { AgentBuildDraftChangedKey } from './build-draft-changes-context'
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { AgentComposerModel } from '@/features/agent-v2/agent-composer/form-state'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { AgentOrchestrateAddActionsProvider } from './add-actions'
@@ -134,36 +140,40 @@ export function AgentOrchestratePanel({
<AgentOrchestrateReadOnlyContext value={readOnly}>
<div aria-readonly={readOnly} className="flex min-h-0 flex-1 flex-col">
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={showHeader ? undefined : orchestrateLabel}
slotClassNames={{
viewport: 'overscroll-contain',
content: cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20'),
scrollbar: hasBottomAction ? 'z-20' : undefined,
}}
>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
<AgentKnowledgeRetrieval />
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={showHeader ? undefined : orchestrateLabel}
aria-labelledby={showHeader ? orchestrateHeadingId : undefined}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className={cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20')}>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
<AgentKnowledgeRetrieval />
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className={hasBottomAction ? 'z-20' : undefined}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</AgentOrchestrateReadOnlyContext>
@@ -10,8 +10,14 @@ import {
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { FileTreeFile } from '@langgenius/dify-ui/file-tree'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useCallback } from 'react'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useCallback, useId } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { AgentFileTree } from '../files/tree'
@@ -113,7 +119,7 @@ function AgentSkillFileList({
<AgentFileTree
files={files}
selectedFileId={selectedFileId}
labelledBy="agent-skill-detail-files-heading"
treeLabelledBy="agent-skill-detail-files-heading"
className={cn('h-full bg-background-section p-1', fileListTreeClassName)}
listClassName={fileListTreeListClassName}
scrollAreaClassName="flex-1"
@@ -337,8 +343,8 @@ export function AgentSkillDetailDialog({
skillName: string
detail: AgentSkillDetail
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const dialogTitleId = useId()
const previewTitle = detail.filePreview?.fileName
const isHeaderDownloadLoading = detail.filePreview?.downloadActionLoadingTarget === 'header'
@@ -355,7 +361,9 @@ export function AgentSkillDetailDialog({
)}
>
<DialogDescription className="sr-only">{detail.description}</DialogDescription>
<DialogTitle className="sr-only">{previewTitle || skillName}</DialogTitle>
<DialogTitle id={dialogTitleId} className="sr-only">
{previewTitle || skillName}
</DialogTitle>
<div className="min-h-0 w-full">
<AgentSkillFileList
fileListHeader={detail.fileListHeader}
@@ -408,35 +416,42 @@ export function AgentSkillDetailDialog({
<DialogCloseButton className="static size-7 shrink-0 rounded-md" />
</div>
</div>
<ScrollArea
className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid"
label={t(($) => $['agentDetail.configure.skills.detail.contentRegion'])}
slotClassNames={{
viewport: 'overscroll-contain outline-none focus-visible:outline-none',
content: 'flex min-h-full w-full max-w-full min-w-0 flex-col gap-2',
}}
>
{detail.filePreview && (
<AgentFilePreviewContent
binary={detail.filePreview.binary}
content={detail.filePreview.content}
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
downloadUrl={detail.filePreview.downloadUrl}
fileName={detail.filePreview.fileName}
imageData={detail.filePreview.imageData}
isDownloadError={detail.filePreview.isDownloadError}
isDownloadLoading={detail.filePreview.isDownloadLoading}
isError={detail.filePreview.isError}
isImage={detail.filePreview.isImage}
isLoading={detail.filePreview.isLoading}
onDownloadFile={detail.onDownloadFile}
/>
)}
{detail.sections.map((section) => (
<div key={section.id} className="px-4">
<AgentSkillDetailSectionBlock section={section} />
</div>
))}
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid">
<ScrollAreaViewport
aria-labelledby={dialogTitleId}
className="overscroll-contain outline-none focus-visible:outline-none"
role="region"
>
<ScrollAreaContent
style={{ minWidth: 0 }}
className="flex min-h-full w-full max-w-full flex-col gap-2"
>
{detail.filePreview && (
<AgentFilePreviewContent
binary={detail.filePreview.binary}
content={detail.filePreview.content}
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
downloadUrl={detail.filePreview.downloadUrl}
fileName={detail.filePreview.fileName}
imageData={detail.filePreview.imageData}
isDownloadError={detail.filePreview.isDownloadError}
isDownloadLoading={detail.filePreview.isDownloadLoading}
isError={detail.filePreview.isError}
isImage={detail.filePreview.isImage}
isLoading={detail.filePreview.isLoading}
onDownloadFile={detail.onDownloadFile}
/>
)}
{detail.sections.map((section) => (
<div key={section.id} className="px-4">
<AgentSkillDetailSectionBlock section={section} />
</div>
))}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</DialogContent>
@@ -2,7 +2,6 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
type AgentConfigureWorkspaceProps = {
'aria-busy'?: boolean
@@ -19,11 +18,8 @@ export function AgentConfigureWorkspace({
rightPanel,
sidePanels,
}: AgentConfigureWorkspaceProps) {
const { t } = useTranslation('agentV2')
return (
<section
aria-label={t(($) => $['agentDetail.sections.configure'])}
aria-busy={ariaBusy}
className={cn(
'flex h-full min-w-0 flex-1 gap-1 overflow-hidden bg-background-body p-1',
@@ -3,8 +3,8 @@ import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -52,7 +52,7 @@ export function AgentLogsTable({
</table>
</div>
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['agentDetail.logs.title'])}
role="region"
@@ -78,7 +78,7 @@ export function AgentLogsTable({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
)
}
@@ -3,7 +3,13 @@
import type { AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import {
Select,
SelectContent,
@@ -129,46 +135,48 @@ export function AgentMonitoringPage({ agentId }: AgentMonitoringPageProps) {
</div>
</header>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
content: 'px-6 pt-2 pb-3',
}}
>
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
{shouldShowError && (
<AgentMonitoringState>
<div className="flex items-center justify-center gap-2">
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
void statisticsQuery.refetch()
}}
>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
</AgentMonitoringState>
)}
{!shouldShowInitialSkeleton && !shouldShowError && (
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
{metrics.map((metric) => (
<AgentMonitoringChart
key={metric.id}
titleKey={metric.titleKey}
explanationKey={metric.explanationKey}
summaryValue={metric.summaryValue}
rows={metric.rows}
chartType={metric.chartType}
valueKey={metric.valueKey}
unitKey={metric.unitKey}
yMaxWhenEmpty={metric.yMaxWhenEmpty}
/>
))}
</div>
)}
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport>
<ScrollAreaContent className="px-6 pt-2 pb-3">
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
{shouldShowError && (
<AgentMonitoringState>
<div className="flex items-center justify-center gap-2">
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
void statisticsQuery.refetch()
}}
>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
</AgentMonitoringState>
)}
{!shouldShowInitialSkeleton && !shouldShowError && (
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
{metrics.map((metric) => (
<AgentMonitoringChart
key={metric.id}
titleKey={metric.titleKey}
explanationKey={metric.explanationKey}
summaryValue={metric.summaryValue}
rows={metric.rows}
chartType={metric.chartType}
valueKey={metric.valueKey}
unitKey={metric.unitKey}
yMaxWhenEmpty={metric.yMaxWhenEmpty}
/>
))}
</div>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</AgentDetailSectionSurface>
)
+3 -3
View File
@@ -3,8 +3,8 @@
import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen'
import type { RosterFilterValue } from './components/roster-filter'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@@ -108,7 +108,7 @@ export default function RosterPage() {
</div>
<div className="min-h-0 flex-1">
<ScrollAreaRoot className="relative h-full min-h-0 min-w-0 overflow-hidden">
<ScrollArea className="relative h-full min-h-0 min-w-0 overflow-hidden">
<ScrollAreaViewport tabIndex={-1} className="overscroll-contain">
<ScrollAreaContent className="min-h-full px-8 pt-2 pb-8">
<AgentRosterList
@@ -127,7 +127,7 @@ export default function RosterPage() {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
</div>
)
@@ -3,7 +3,13 @@
import type { ReactNode } from 'react'
import type { GuideStep } from '@/features/deployments/create-guide/state/types'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { TitleTooltip } from '@/features/deployments/shared/components/title-tooltip'
@@ -161,14 +167,13 @@ export function GuideCard({
return (
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col">
{contentScrollable ? (
<ScrollArea
className="min-h-0 flex-1"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full pt-0.5 pb-6',
}}
>
{children}
<ScrollArea className="min-h-0 flex-1">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="min-h-full pt-0.5 pb-6">{children}</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
) : (
<div className="min-h-0 flex-1 overflow-hidden pt-0.5 pb-6">{children}</div>
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "يشغل Preview الوكيل المكتمل كما سيراه المستخدمون، مع ردود واضحة وميزات الدردشة.",
"agentDetail.configure.rightPanel.previewTipTitle": "عاين وكيلك",
"agentDetail.configure.skills.add": "إضافة مهارة",
"agentDetail.configure.skills.detail.contentRegion": "محتوى تفاصيل المهارة",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ملفات",
"agentDetail.configure.skills.detail.files": "الملفات",
"agentDetail.configure.skills.empty.description": "تمنح المهارات الوكيل خبرة قابلة لإعادة الاستخدام يمكنه استدعاؤها أثناء العمل",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview führt den fertigen Agenten so aus, wie deine Benutzer ihn sehen, mit klaren Antworten und Chat-Funktionen.",
"agentDetail.configure.rightPanel.previewTipTitle": "Agenten vorschauen",
"agentDetail.configure.skills.add": "Skill hinzufügen",
"agentDetail.configure.skills.detail.contentRegion": "Skill-Detailinhalt",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATEIEN",
"agentDetail.configure.skills.detail.files": "Dateien",
"agentDetail.configure.skills.empty.description": "Skills geben dem Agenten wiederverwendbare Fachkenntnisse, die er bei der Arbeit nutzen kann",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview runs the finished agent the way your users will see it, with clean replies and chat features.",
"agentDetail.configure.rightPanel.previewTipTitle": "Preview your agent",
"agentDetail.configure.skills.add": "Add skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill detail content",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILES",
"agentDetail.configure.skills.detail.files": "Files",
"agentDetail.configure.skills.empty.description": "Skills give the agent reusable expertise it can call while working",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview ejecuta el agente terminado como lo verán tus usuarios, con respuestas claras y funciones de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Previsualiza tu agente",
"agentDetail.configure.skills.add": "Agregar habilidad",
"agentDetail.configure.skills.detail.contentRegion": "Contenido de los detalles de la habilidad",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARCHIVOS",
"agentDetail.configure.skills.detail.files": "Archivos",
"agentDetail.configure.skills.empty.description": "Las habilidades le dan al agente experiencia reutilizable que puede invocar mientras trabaja",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview عامل تکمیل‌شده را همان‌طور که کاربران می‌بینند اجرا می‌کند، با پاسخ‌های تمیز و قابلیت‌های گفتگو.",
"agentDetail.configure.rightPanel.previewTipTitle": "پیش‌نمایش عامل",
"agentDetail.configure.skills.add": "افزودن مهارت",
"agentDetail.configure.skills.detail.contentRegion": "محتوای جزئیات مهارت",
"agentDetail.configure.skills.detail.fileCount": "{{count}} فایل",
"agentDetail.configure.skills.detail.files": "فایل‌ها",
"agentDetail.configure.skills.empty.description": "مهارت‌ها به عامل تخصص قابل استفاده مجدد می‌دهند که هنگام کار می‌تواند فراخوانی کند",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview exécute l’agent final comme vos utilisateurs le verront, avec des réponses claires et les fonctions de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Prévisualiser votre agent",
"agentDetail.configure.skills.add": "Ajouter une compétence",
"agentDetail.configure.skills.detail.contentRegion": "Contenu des détails de la compétence",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FICHIERS",
"agentDetail.configure.skills.detail.files": "Fichiers",
"agentDetail.configure.skills.empty.description": "Les compétences offrent à l’agent une expertise réutilisable qu’il peut invoquer pendant son travail",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview तैयार agent को वैसे चलाता है जैसे आपके users उसे देखेंगे, साफ replies और chat features के साथ।",
"agentDetail.configure.rightPanel.previewTipTitle": "अपने agent का preview करें",
"agentDetail.configure.skills.add": "कौशल जोड़ें",
"agentDetail.configure.skills.detail.contentRegion": "कौशल विवरण सामग्री",
"agentDetail.configure.skills.detail.fileCount": "{{count}} फ़ाइलें",
"agentDetail.configure.skills.detail.files": "फ़ाइलें",
"agentDetail.configure.skills.empty.description": "कौशल एजेंट को पुनः उपयोग योग्य विशेषज्ञता देते हैं जिसे वह काम करते समय कॉल कर सकता है",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview menjalankan agent yang sudah selesai seperti yang akan dilihat pengguna, dengan balasan rapi dan fitur chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Pratinjau agen Anda",
"agentDetail.configure.skills.add": "Tambahkan keterampilan",
"agentDetail.configure.skills.detail.contentRegion": "Konten detail keterampilan",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
"agentDetail.configure.skills.detail.files": "File",
"agentDetail.configure.skills.empty.description": "Keterampilan memberi agen keahlian yang dapat digunakan kembali yang bisa dipanggil saat bekerja",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview esegue l’agente completato come lo vedranno gli utenti, con risposte pulite e funzionalità di chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Anteprima del tuo agente",
"agentDetail.configure.skills.add": "Aggiungi abilità",
"agentDetail.configure.skills.detail.contentRegion": "Contenuto dei dettagli dell’abilità",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
"agentDetail.configure.skills.detail.files": "File",
"agentDetail.configure.skills.empty.description": "Le abilità forniscono all’agente competenze riutilizzabili che può richiamare durante il lavoro",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview は完成したエージェントをユーザーに見える形で実行し、整った返信とチャット機能を確認できます。",
"agentDetail.configure.rightPanel.previewTipTitle": "エージェントをプレビュー",
"agentDetail.configure.skills.add": "スキルを追加",
"agentDetail.configure.skills.detail.contentRegion": "スキル詳細コンテンツ",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個のファイル",
"agentDetail.configure.skills.detail.files": "ファイル",
"agentDetail.configure.skills.empty.description": "スキルはエージェントが作業中に呼び出せる再利用可能な専門知識を提供します",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview는 사용자가 보게 될 방식으로 완성된 에이전트를 실행하며, 깔끔한 답변과 채팅 기능을 보여줍니다.",
"agentDetail.configure.rightPanel.previewTipTitle": "에이전트 미리보기",
"agentDetail.configure.skills.add": "스킬 추가",
"agentDetail.configure.skills.detail.contentRegion": "스킬 상세 내용",
"agentDetail.configure.skills.detail.fileCount": "파일 {{count}}개",
"agentDetail.configure.skills.detail.files": "파일",
"agentDetail.configure.skills.empty.description": "스킬은 에이전트가 작업하면서 호출할 수 있는 재사용 가능한 전문 능력을 제공합니다",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview voert de voltooide agent uit zoals je gebruikers die zien, met duidelijke antwoorden en chatfuncties.",
"agentDetail.configure.rightPanel.previewTipTitle": "Voorbeeld van je agent",
"agentDetail.configure.skills.add": "Skill toevoegen",
"agentDetail.configure.skills.detail.contentRegion": "Skill-detailinhoud",
"agentDetail.configure.skills.detail.fileCount": "{{count}} BESTANDEN",
"agentDetail.configure.skills.detail.files": "Bestanden",
"agentDetail.configure.skills.empty.description": "Skills geven de agent herbruikbare expertise die hij tijdens het werken kan inzetten",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview uruchamia gotowego agenta tak, jak zobaczą go użytkownicy, z przejrzystymi odpowiedziami i funkcjami czatu.",
"agentDetail.configure.rightPanel.previewTipTitle": "Podgląd agenta",
"agentDetail.configure.skills.add": "Dodaj umiejętność",
"agentDetail.configure.skills.detail.contentRegion": "Zawartość szczegółów umiejętności",
"agentDetail.configure.skills.detail.fileCount": "{{count}} PLIKÓW",
"agentDetail.configure.skills.detail.files": "Pliki",
"agentDetail.configure.skills.empty.description": "Umiejętności dają agentowi reużywalną wiedzę, którą może wywołać podczas pracy",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview executa o agente finalizado como seus usuários o verão, com respostas claras e recursos de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Pré-visualize seu agente",
"agentDetail.configure.skills.add": "Adicionar habilidade",
"agentDetail.configure.skills.detail.contentRegion": "Conteúdo dos detalhes da habilidade",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARQUIVOS",
"agentDetail.configure.skills.detail.files": "Arquivos",
"agentDetail.configure.skills.empty.description": "As habilidades dão ao agente expertise reutilizável que ele pode invocar enquanto trabalha",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview rulează agentul finalizat așa cum îl vor vedea utilizatorii, cu răspunsuri clare și funcții de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Previzualizează agentul",
"agentDetail.configure.skills.add": "Adaugă abilitate",
"agentDetail.configure.skills.detail.contentRegion": "Conținutul detaliilor abilității",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FIȘIERE",
"agentDetail.configure.skills.detail.files": "Fișiere",
"agentDetail.configure.skills.empty.description": "Abilitățile oferă agentului expertiză reutilizabilă pe care o poate apela în timpul lucrului",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускает готового агента так, как его увидят пользователи, с чистыми ответами и функциями чата.",
"agentDetail.configure.rightPanel.previewTipTitle": "Предпросмотр агента",
"agentDetail.configure.skills.add": "Добавить навык",
"agentDetail.configure.skills.detail.contentRegion": "Содержимое деталей навыка",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛОВ",
"agentDetail.configure.skills.detail.files": "Файлы",
"agentDetail.configure.skills.empty.description": "Навыки дают агенту переиспользуемую экспертизу, которую он может вызывать в работе",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview zažene dokončanega agenta tako, kot ga bodo videli uporabniki, z jasnimi odgovori in funkcijami klepeta.",
"agentDetail.configure.rightPanel.previewTipTitle": "Predogled agenta",
"agentDetail.configure.skills.add": "Dodaj veščino",
"agentDetail.configure.skills.detail.contentRegion": "Vsebina podrobnosti veščine",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATOTEK",
"agentDetail.configure.skills.detail.files": "Datoteke",
"agentDetail.configure.skills.empty.description": "Veščine agentu dajejo ponovno uporabno strokovnost, ki jo lahko kliče med delom",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview เรียกใช้เอเจนต์ที่เสร็จแล้วในแบบที่ผู้ใช้จะเห็น พร้อมคำตอบที่ชัดเจนและฟีเจอร์แชท",
"agentDetail.configure.rightPanel.previewTipTitle": "ดูตัวอย่างเอเจนต์",
"agentDetail.configure.skills.add": "เพิ่มทักษะ",
"agentDetail.configure.skills.detail.contentRegion": "เนื้อหารายละเอียดทักษะ",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ไฟล์",
"agentDetail.configure.skills.detail.files": "ไฟล์",
"agentDetail.configure.skills.empty.description": "ทักษะให้ตัวแทนมีความเชี่ยวชาญที่นำกลับมาใช้ใหม่ได้ซึ่งสามารถเรียกใช้ได้ขณะทำงาน",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview, tamamlanan ajanı kullanıcılarınızın göreceği şekilde, temiz yanıtlar ve sohbet özellikleriyle çalıştırır.",
"agentDetail.configure.rightPanel.previewTipTitle": "Ajanınızı önizleyin",
"agentDetail.configure.skills.add": "Beceri ekle",
"agentDetail.configure.skills.detail.contentRegion": "Beceri detay içeriği",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DOSYA",
"agentDetail.configure.skills.detail.files": "Dosyalar",
"agentDetail.configure.skills.empty.description": "Beceriler, ajana çalışırken çağırabileceği yeniden kullanılabilir uzmanlık sağlar",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускає готового агента так, як його побачать користувачі, з чистими відповідями та функціями чату.",
"agentDetail.configure.rightPanel.previewTipTitle": "Попередній перегляд агента",
"agentDetail.configure.skills.add": "Додати навичку",
"agentDetail.configure.skills.detail.contentRegion": "Вміст деталей навички",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛІВ",
"agentDetail.configure.skills.detail.files": "Файли",
"agentDetail.configure.skills.empty.description": "Навички дають агенту перевикористовну експертизу, яку він може викликати під час роботи",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview chạy agent hoàn chỉnh theo cách người dùng sẽ thấy, với câu trả lời rõ ràng và các tính năng chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Xem trước agent",
"agentDetail.configure.skills.add": "Thêm kỹ năng",
"agentDetail.configure.skills.detail.contentRegion": "Nội dung chi tiết kỹ năng",
"agentDetail.configure.skills.detail.fileCount": "{{count}} TỆP",
"agentDetail.configure.skills.detail.files": "Tệp",
"agentDetail.configure.skills.empty.description": "Kỹ năng mang đến cho tác nhân chuyên môn có thể tái sử dụng mà nó có thể gọi khi làm việc",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "像用户一样测试它。你的交互不会影响 Agent 后续的行为。",
"agentDetail.configure.rightPanel.previewTipTitle": "预览你的 Agent",
"agentDetail.configure.skills.add": "添加 Skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill 详情内容",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 个文件",
"agentDetail.configure.skills.detail.files": "文件",
"agentDetail.configure.skills.empty.description": "Skill 为 Agent 提供工作时可调用的可复用专业能力",
-1
View File
@@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "像使用者一樣測試它。你的互動不會影響 Agent 後續的行為。",
"agentDetail.configure.rightPanel.previewTipTitle": "預覽你的 Agent",
"agentDetail.configure.skills.add": "新增 Skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill 詳情內容",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個檔案",
"agentDetail.configure.skills.detail.files": "檔案",
"agentDetail.configure.skills.empty.description": "Skill 為 Agent 提供工作時可呼叫的可複用專業能力",