merge main

This commit is contained in:
Joel
2024-12-23 15:33:08 +08:00
396 changed files with 7187 additions and 2056 deletions

View File

@ -25,6 +25,7 @@ import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import AppContext, { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { App } from '@/types/app'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@ -41,12 +42,14 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const pathname = usePathname()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceEditor } = useAppContext()
const { isCurrentWorkspaceEditor, isLoadingCurrentWorkspace } = useAppContext()
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore(useShallow(state => ({
appDetail: state.appDetail,
setAppDetail: state.setAppDetail,
setAppSiderbarExpand: state.setAppSiderbarExpand,
})))
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
const [navigation, setNavigation] = useState<Array<{
name: string
href: string
@ -107,33 +110,43 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
useEffect(() => {
setAppDetail()
setIsLoadingAppDetail(true)
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
// redirection
const canIEditApp = isCurrentWorkspaceEditor
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
router.replace(`/app/${appId}/overview`)
return
}
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail({ ...res, enable_sso: false })
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
fetchAppSSO({ appId }).then((ssoRes) => {
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
})
}
}
setAppDetailRes(res)
}).catch((e: any) => {
if (e.status === 404)
router.replace('/apps')
}).finally(() => {
setIsLoadingAppDetail(false)
})
}, [appId, isCurrentWorkspaceEditor, systemFeatures, getNavigations, pathname, router, setAppDetail])
}, [appId, router, setAppDetail])
useEffect(() => {
if (!appDetailRes || isLoadingCurrentWorkspace || isLoadingAppDetail)
return
const res = appDetailRes
// redirection
const canIEditApp = isCurrentWorkspaceEditor
if (!canIEditApp && (pathname.endsWith('configuration') || pathname.endsWith('workflow') || pathname.endsWith('logs'))) {
router.replace(`/app/${appId}/overview`)
return
}
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail({ ...res, enable_sso: false })
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode))
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
fetchAppSSO({ appId }).then((ssoRes) => {
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
})
}
}
}, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail, systemFeatures.enable_web_sso_switch_component])
useUnmount(() => {
setAppDetail()

View File

@ -18,10 +18,10 @@ import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
const titleClassName = `
text-sm font-medium text-gray-900
system-sm-semibold text-text-secondary
`
const descriptionClassName = `
mt-1 text-xs font-normal text-gray-500
mt-1 body-xs-regular text-text-tertiary
`
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@ -122,7 +122,7 @@ export default function AccountPage() {
<div className='mr-3'>
<AppIcon size='tiny' />
</div>
<div className='mt-[3px] text-xs font-medium text-gray-700 leading-[18px]'>{item.name}</div>
<div className='mt-[3px] system-sm-medium text-text-secondary'>{item.name}</div>
</div>
)
}
@ -130,7 +130,7 @@ export default function AccountPage() {
return (
<>
<div className='pt-2 pb-3'>
<h4 className='title-2xl-semi-bold text-primary'>{t('common.account.myAccount')}</h4>
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div>
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
<Avatar name={userProfile.name} size={64} />
@ -142,10 +142,10 @@ export default function AccountPage() {
<div className='mb-8'>
<div className={titleClassName}>{t('common.account.name')}</div>
<div className='flex items-center justify-between gap-2 w-full mt-2'>
<div className='flex-1 bg-gray-100 rounded-md p-2 system-sm-regular text-components-input-text-filled '>
<div className='flex-1 bg-components-input-bg-normal rounded-lg p-2 system-sm-regular text-components-input-text-filled '>
<span className='pl-1'>{userProfile.name}</span>
</div>
<div className=' bg-gray-100 rounded-md py-2 px-3 cursor-pointer system-sm-medium text-components-input-text-filled' onClick={handleEditName}>
<div className='bg-components-button-tertiary-bg rounded-lg py-2 px-3 cursor-pointer system-sm-medium text-components-button-tertiary-text' onClick={handleEditName}>
{t('common.operation.edit')}
</div>
</div>
@ -153,7 +153,7 @@ export default function AccountPage() {
<div className='mb-8'>
<div className={titleClassName}>{t('common.account.email')}</div>
<div className='flex items-center justify-between gap-2 w-full mt-2'>
<div className='flex-1 bg-gray-100 rounded-md p-2 system-sm-regular text-components-input-text-filled '>
<div className='flex-1 bg-components-input-bg-normal rounded-lg p-2 system-sm-regular text-components-input-text-filled '>
<span className='pl-1'>{userProfile.email}</span>
</div>
</div>
@ -162,14 +162,14 @@ export default function AccountPage() {
systemFeatures.enable_email_password_login && (
<div className='mb-8 flex justify-between gap-2'>
<div>
<div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div>
<div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div>
<div className='mb-1 system-sm-semibold text-text-secondary'>{t('common.account.password')}</div>
<div className='mb-2 body-xs-regular text-text-tertiary'>{t('common.account.passwordTip')}</div>
</div>
<Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
</div>
)
}
<div className='mb-6 border-[0.5px] border-gray-100' />
<div className='mb-6 border-[1px] border-divider-subtle' />
<div className='mb-8'>
<div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
<div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
@ -181,7 +181,7 @@ export default function AccountPage() {
wrapperClassName='mt-2'
/>
)}
{!IS_CE_EDITION && <Button className='mt-2 text-[#D92D20]' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
{!IS_CE_EDITION && <Button className='mt-2 text-components-button-destructive-secondary-text' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
</div>
{
editNameModalVisible && (
@ -190,7 +190,7 @@ export default function AccountPage() {
onClose={() => setEditNameModalVisible(false)}
className={s.modal}
>
<div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div>
<div className='mb-6 title-2xl-semi-bold text-text-primary'>{t('common.account.editName')}</div>
<div className={titleClassName}>{t('common.account.name')}</div>
<Input className='mt-2'
value={editName}
@ -219,7 +219,7 @@ export default function AccountPage() {
}}
className={s.modal}
>
<div className='mb-6 text-lg font-medium text-gray-900'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
<div className='mb-6 title-2xl-semi-bold text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
{userProfile.is_password_set && (
<>
<div className={titleClassName}>{t('common.account.currentPassword')}</div>
@ -242,7 +242,7 @@ export default function AccountPage() {
</div>
</>
)}
<div className='mt-8 text-sm font-medium text-gray-900'>
<div className='mt-8 system-sm-semibold text-text-secondary'>
{userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
</div>
<div className='relative mt-2'>
@ -261,7 +261,7 @@ export default function AccountPage() {
</Button>
</div>
</div>
<div className='mt-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div>
<div className='mt-8 system-sm-semibold text-text-secondary'>{t('common.account.confirmPassword')}</div>
<div className='relative mt-2'>
<Input
type={showConfirmPassword ? 'text' : 'password'}
@ -305,13 +305,13 @@ export default function AccountPage() {
title={t('common.account.delete')}
content={
<>
<div className='my-1 text-[#D92D20] text-sm leading-5'>
<div className='my-1 text-text-destructive body-md-medium'>
{t('common.account.deleteTip')}
</div>
<div className='mt-3 text-sm leading-5'>
<span>{t('common.account.deleteConfirmTip')}</span>
<a
className='text-primary-600 cursor'
className='text-text-accent cursor'
href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`}
target='_blank'
rel='noreferrer noopener'
@ -323,7 +323,7 @@ export default function AccountPage() {
support@dify.ai
</a>
</div>
<div className='my-2 px-3 py-2 rounded-lg bg-gray-100 text-sm font-medium leading-5 text-gray-800'>{`${t('common.account.delete')}: ${userProfile.email}`}</div>
<div className='my-2 px-3 py-2 rounded-lg bg-components-input-bg-active border border-components-input-border-active system-sm-regular text-components-input-text-filled'>{`${t('common.account.delete')}: ${userProfile.email}`}</div>
</>
}
confirmText={t('common.operation.ok') as string}

View File

@ -40,9 +40,9 @@ export default function AppSelector() {
className={`
inline-flex items-center
rounded-[20px] p-1x text-sm
text-gray-700 hover:bg-gray-200
text-text-primary
mobile:px-1
${open && 'bg-gray-200'}
${open && 'bg-components-panel-bg-blur'}
`}
>
<Avatar name={userProfile.name} size={32} />
@ -60,7 +60,7 @@ export default function AppSelector() {
<Menu.Items
className="
absolute -right-2 -top-1 w-60 max-w-80
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
divide-y divide-divider-subtle origin-top-right rounded-lg bg-components-panel-bg-blur
shadow-lg
"
>
@ -78,10 +78,10 @@ export default function AppSelector() {
<Menu.Item>
<div className='p-1' onClick={() => handleLogout()}>
<div
className='flex items-center justify-start h-9 px-3 rounded-lg cursor-pointer group hover:bg-gray-50'
className='flex items-center justify-start h-9 px-3 rounded-lg cursor-pointer group hover:bg-state-base-hover'
>
<LogOut01 className='w-4 h-4 text-gray-500 flex mr-1' />
<div className='font-normal text-[14px] text-gray-700'>{t('common.userProfile.logout')}</div>
<LogOut01 className='w-4 h-4 text-text-tertiary flex mr-1' />
<div className='font-normal text-[14px] text-text-secondary'>{t('common.userProfile.logout')}</div>
</div>
</div>
</Menu.Item>

View File

@ -21,7 +21,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
<HeaderWrapper>
<Header />
</HeaderWrapper>
<div className='relative flex flex-col overflow-y-auto bg-white shrink-0 h-0 grow'>
<div className='relative flex flex-col overflow-y-auto bg-components-panel-bg shrink-0 h-0 grow'>
{children}
</div>
</ModalContextProvider>

View File

@ -25,10 +25,10 @@ const AppCard = ({
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={app.app.icon_type}
icon={app.app.icon}
background={app.app.icon_background}
imageUrl={app.app.icon_url}
iconType={appBasicInfo.icon_type}
icon={appBasicInfo.icon}
background={appBasicInfo.icon_background}
imageUrl={appBasicInfo.icon_url}
/>
<AppTypeIcon wrapperClassName='absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-[4px] border border-divider-regular outline outline-components-panel-on-panel-item-bg'
className='w-3 h-3' type={appBasicInfo.mode} />

View File

@ -16,6 +16,7 @@ import { createContext, useContext } from 'use-context-selector'
import { useShallow } from 'zustand/react/shallow'
import { useTranslation } from 'react-i18next'
import type { ChatItemInTree } from '../../base/chat/types'
import Indicator from '../../header/indicator'
import VarPanel from './var-panel'
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
@ -58,6 +59,12 @@ type IDrawerContext = {
appDetail?: App
}
type StatusCount = {
success: number
failed: number
partial_success: number
}
const DrawerContext = createContext<IDrawerContext>({} as IDrawerContext)
/**
@ -72,6 +79,33 @@ const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = (
</div>
}
const statusTdRender = (statusCount: StatusCount) => {
if (statusCount.partial_success + statusCount.failed === 0) {
return (
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'green'} />
<span className='text-util-colors-green-green-600'>Success</span>
</div>
)
}
else if (statusCount.failed === 0) {
return (
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'green'} />
<span className='text-util-colors-green-green-600'>Partial Success</span>
</div>
)
}
else {
return (
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'red'} />
<span className='text-util-colors-red-red-600'>{statusCount.failed} {`${statusCount.failed > 1 ? 'Failures' : 'Failure'}`}</span>
</div>
)
}
}
const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
const newChatList: IChatItem[] = []
messages.forEach((item: ChatMessage) => {
@ -497,8 +531,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
}
/**
* Text App Conversation Detail Component
*/
* Text App Conversation Detail Component
*/
const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
// Text Generator App Session Details Including Message List
const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
@ -543,8 +577,8 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
}
/**
* Chat App Conversation Detail Component
*/
* Chat App Conversation Detail Component
*/
const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
@ -586,8 +620,8 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
}
/**
* Conversation list component including basic information
*/
* Conversation list component including basic information
*/
const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
@ -598,6 +632,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
const { setShowPromptLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
setShowPromptLogModal: state.setShowPromptLogModal,
setShowAgentLogModal: state.setShowAgentLogModal,
@ -640,6 +675,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
<td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
{isChatflow && <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.status')}</td>}
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
<td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
@ -670,6 +706,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
{renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
</td>
<td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
{isChatflow && <td className='p-3 pr-2 w-[160px]' style={{ maxWidth: isChatMode ? 300 : 200 }}>
{statusTdRender(log.status_count)}
</td>}
<td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
{renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
</td>

View File

@ -63,6 +63,14 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
</div>
)
}
if (status === 'partial-succeeded') {
return (
<div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
<Indicator color={'green'} />
<span className='text-util-colors-green-green-600'>Partial Success</span>
</div>
)
}
}
const onCloseDrawer = () => {

View File

@ -3,7 +3,6 @@
import type { FC } from 'react'
import { init } from 'emoji-mart'
import data from '@emoji-mart/data'
import Image from 'next/image'
import { cva } from 'class-variance-authority'
import type { AppIconType } from '@/types/app'
import classNames from '@/utils/classnames'
@ -62,7 +61,8 @@ const AppIcon: FC<AppIconProps> = ({
onClick={onClick}
>
{isValidImageIcon
? <Image src={imageUrl} className="w-full h-full" alt="app icon" />
// eslint-disable-next-line @next/next/no-img-element
? <img src={imageUrl} className="w-full h-full" alt="app icon" />
: (innerIcon || ((icon && icon !== '') ? <em-emoji id={icon} /> : <em-emoji id='🤖' />))
}
</span>

View File

@ -64,6 +64,12 @@ const WorkflowProcessItem = ({
setShowMessageLogModal(true)
}, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
const showRetryDetail = useCallback(() => {
setCurrentLogItem(item)
setCurrentLogModalActiveTab('TRACING')
setShowMessageLogModal(true)
}, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
return (
<div
className={cn(
@ -105,6 +111,7 @@ const WorkflowProcessItem = ({
<TracingPanel
list={data.tracing}
onShowIterationDetail={showIterationDetail}
onShowRetryDetail={showRetryDetail}
hideNodeInfo={hideInfo}
hideNodeProcessDetail={hideProcessDetail}
/>

View File

@ -28,6 +28,7 @@ export type InputProps = {
destructive?: boolean
wrapperClassName?: string
styleCss?: CSSProperties
unit?: string
} & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants>
const Input = ({
@ -43,6 +44,7 @@ const Input = ({
value,
placeholder,
onChange,
unit,
...props
}: InputProps) => {
const { t } = useTranslation()
@ -80,6 +82,13 @@ const Input = ({
{destructive && (
<RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' />
)}
{
unit && (
<div className='absolute right-2 top-1/2 -translate-y-1/2 system-sm-regular text-text-tertiary'>
{unit}
</div>
)
}
</div>
)
}

View File

@ -1,6 +1,6 @@
import { Dialog, Transition } from '@headlessui/react'
import { Fragment } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import { RiCloseLine } from '@remixicon/react'
import classNames from '@/utils/classnames'
// https://headlessui.com/react/dialog
@ -39,7 +39,7 @@ export default function Modal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
<div className="fixed inset-0 bg-background-overlay-fullscreen" />
</Transition.Child>
<div
@ -66,16 +66,16 @@ export default function Modal({
)}>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-text-primary"
className="title-2xl-semi-bold text-text-primary"
>
{title}
</Dialog.Title>}
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>
{description && <Dialog.Description className='text-text-secondary body-md-regular mt-2'>
{description}
</Dialog.Description>}
{closable
&& <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'>
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={
&& <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-state-base-hover'>
<RiCloseLine className='w-4 h-4 text-text-tertiary' onClick={
(e) => {
e.stopPropagation()
onClose()

View File

@ -23,6 +23,7 @@ const SearchInput: FC<SearchInputProps> = ({
const { t } = useTranslation()
const [focus, setFocus] = useState<boolean>(false)
const isComposing = useRef<boolean>(false)
const [internalValue, setInternalValue] = useState<string>(value)
return (
<div className={cn(
@ -45,16 +46,18 @@ const SearchInput: FC<SearchInputProps> = ({
white && '!bg-white hover:!bg-white group-hover:!bg-white placeholder:!text-gray-400',
)}
placeholder={placeholder || t('common.operation.search')!}
value={value}
value={internalValue}
onChange={(e) => {
setInternalValue(e.target.value)
if (!isComposing.current)
onChange(e.target.value)
}}
onCompositionStart={() => {
isComposing.current = true
}}
onCompositionEnd={() => {
onCompositionEnd={(e) => {
isComposing.current = false
onChange(e.data)
}}
onFocus={() => setFocus(true)}
onBlur={() => setFocus(false)}
@ -63,7 +66,10 @@ const SearchInput: FC<SearchInputProps> = ({
{value && (
<div
className='shrink-0 flex items-center justify-center w-4 h-4 cursor-pointer group/clear'
onClick={() => onChange('')}
onClick={() => {
onChange('')
setInternalValue('')
}}
>
<XCircle className='w-3.5 h-3.5 text-gray-400 group-hover/clear:text-gray-600' />
</div>

View File

@ -4,6 +4,7 @@ import React, { Fragment, useEffect, useState } from 'react'
import { Combobox, Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid'
import Badge from '../badge/index'
import { RiCheckLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import classNames from '@/utils/classnames'
import {
@ -153,7 +154,7 @@ const Select: FC<ISelectProps> = ({
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
@ -211,7 +212,7 @@ const SimpleSelect: FC<ISelectProps> = ({
<div className={classNames('group/simple-select relative h-9', wrapperClassName)}>
{renderTrigger && <Listbox.Button className='w-full'>{renderTrigger(selectedItem)}</Listbox.Button>}
{!renderTrigger && (
<Listbox.Button className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-gray-100 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<Listbox.Button className={classNames(`flex items-center w-full h-full rounded-lg border-0 bg-components-input-bg-normal pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-state-base-hover-alt group-hover/simple-select:bg-state-base-hover-alt ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`, className)}>
<span className={classNames('block truncate text-left system-sm-regular text-components-input-text-filled', !selectedItem?.name && 'text-components-input-text-placeholder')}>{selectedItem?.name ?? localPlaceholder}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2">
{(selectedItem && !notClearable)
@ -244,13 +245,13 @@ const SimpleSelect: FC<ISelectProps> = ({
leaveTo="opacity-0"
>
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
<Listbox.Options className={classNames('absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg border-components-panel-border border-[0.5px] focus:outline-none sm:text-sm', optionWrapClassName)}>
{items.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
classNames(
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''}`,
'relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-state-base-hover text-text-secondary',
optionClassName,
)
}
@ -266,10 +267,10 @@ const SimpleSelect: FC<ISelectProps> = ({
{selected && !hideChecked && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
'absolute inset-y-0 right-0 flex items-center pr-4 text-text-accent',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>)}
@ -376,7 +377,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
)}
</span>
{!hideChecked && item.value === value && (
<CheckIcon className='shrink-0 h-4 w-4 text-text-accent' />
<RiCheckLine className='shrink-0 h-4 w-4 text-text-accent' />
)}
</div>
))}

View File

@ -346,6 +346,9 @@ The text generation application offers non-session support and is ideal for tran
<Property name='user' type='string' key='user'>
User identifier, defined by the developer's rules, must be unique within the application.
</Property>
<Property name='content' type='string' key='content'>
The specific content of message feedback.
</Property>
</Properties>
### Response
@ -353,7 +356,7 @@ The text generation application offers non-session support and is ideal for tran
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -361,7 +364,8 @@ The text generation application offers non-session support and is ideal for tran
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```

View File

@ -345,6 +345,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='user' type='string' key='user'>
開発者のルールで定義されたユーザー識別子。アプリケーション内で一意である必要があります。
</Property>
<Property name='content' type='string' key='content'>
メッセージのフィードバックです。
</Property>
</Properties>
### レスポンス
@ -352,7 +355,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -360,7 +363,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```

View File

@ -320,6 +320,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
</Property>
<Property name='content' type='string' key='content'>
消息反馈的具体信息。
</Property>
</Properties>
### Response
@ -327,7 +330,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -335,7 +338,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```

View File

@ -71,6 +71,7 @@ Chat applications support session persistence, allowing previous chat history to
- `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG')
- `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR')
- `video` ('MP4', 'MOV', 'MPEG', 'MPGA')
- `custom` (Other file types)
- `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload
- `url` (string) Image URL (when the transfer method is `remote_url`)
- `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`)
@ -444,6 +445,9 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='user' type='string' key='user'>
User identifier, defined by the developer's rules, must be unique within the application.
</Property>
<Property name='content' type='string' key='content'>
The specific content of message feedback.
</Property>
</Properties>
### Response
@ -451,7 +455,7 @@ Chat applications support session persistence, allowing previous chat history to
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -459,7 +463,8 @@ Chat applications support session persistence, allowing previous chat history to
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```
@ -675,7 +680,7 @@ Chat applications support session persistence, allowing previous chat history to
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20'`}>
<CodeGroup title="Request" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \\\n --header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \

View File

@ -71,6 +71,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG')
- `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR')
- `video` ('MP4', 'MOV', 'MPEG', 'MPGA')
- `custom` (他のファイルタイプ)
- `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file`
- `url` (string) 画像URL転送方法が`remote_url`の場合)
- `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります転送方法が`local_file`の場合)
@ -444,6 +445,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='user' type='string' key='user'>
ユーザー識別子、開発者のルールによって定義され、アプリケーション内で一意でなければなりません。
</Property>
<Property name='content' type='string' key='content'>
メッセージのフィードバックです。
</Property>
</Properties>
### 応答
@ -451,7 +455,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
<Col sticky>
<CodeGroup title="リクエスト" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="リクエスト" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -459,7 +463,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```
@ -674,7 +679,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
<Col sticky>
<CodeGroup title="リクエスト" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20'`}>
<CodeGroup title="リクエスト" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \\\n --header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \

View File

@ -3,7 +3,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# 工作流编排对话型应用 API
对话应用支持会话持久化,可将之前的聊天记录作为上下进行回答,可适用于聊天/客服 AI 等。
对话应用支持会话持久化,可将之前的聊天记录作为上下进行回答,可适用于聊天/客服 AI 等。
<div>
### 基础 URL
@ -68,6 +68,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
- `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'
- `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR'
- `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA'
- `custom` 具体类型包含:其他文件类型
- `transfer_method` (string) 传递方式:
- `remote_url`: 图片地址。
- `local_file`: 上传文件。
@ -450,6 +451,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
</Property>
<Property name='content' type='string' key='content'>
消息反馈的具体信息。
</Property>
</Properties>
### Response
@ -457,7 +461,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -465,7 +469,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```

View File

@ -408,6 +408,9 @@ Chat applications support session persistence, allowing previous chat history to
<Property name='user' type='string' key='user'>
User identifier, defined by the developer's rules, must be unique within the application.
</Property>
<Property name='content' type='string' key='content'>
The specific content of message feedback.
</Property>
</Properties>
### Response
@ -415,7 +418,7 @@ Chat applications support session persistence, allowing previous chat history to
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -423,7 +426,8 @@ Chat applications support session persistence, allowing previous chat history to
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```
@ -709,7 +713,7 @@ Chat applications support session persistence, allowing previous chat history to
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20'`}>
<CodeGroup title="Request" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \\\n --header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \

View File

@ -408,6 +408,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
<Property name='user' type='string' key='user'>
ユーザー識別子、開発者のルールで定義され、アプリケーション内で一意でなければなりません。
</Property>
<Property name='content' type='string' key='content'>
メッセージのフィードバックです。
</Property>
</Properties>
### 応答
@ -415,7 +418,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
<Col sticky>
<CodeGroup title="リクエスト" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="リクエスト" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n --header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -423,7 +426,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```
@ -708,7 +712,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
</Col>
<Col sticky>
<CodeGroup title="リクエスト" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20'`}>
<CodeGroup title="リクエスト" tag="GET" label="/conversations" targetCode={`curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \\\n --header 'Authorization: Bearer {api_key}'`}>
```bash {{ title: 'cURL' }}
curl -X GET '${props.appDetail.api_base_url}/conversations?user=abc-123&last_id=&limit=20' \

View File

@ -3,7 +3,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# 对话型应用 API
对话应用支持会话持久化,可将之前的聊天记录作为上下进行回答,可适用于聊天/客服 AI 等。
对话应用支持会话持久化,可将之前的聊天记录作为上下进行回答,可适用于聊天/客服 AI 等。
<div>
### 基础 URL
@ -423,6 +423,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
<Property name='user' type='string' key='user'>
用户标识,由开发者定义规则,需保证用户标识在应用内唯一。
</Property>
<Property name='content' type='string' key='content'>
消息反馈的具体信息。
</Property>
</Properties>
### Response
@ -430,7 +433,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123"\n}'`}>
<CodeGroup title="Request" tag="POST" label="/messages/:message_id/feedbacks" targetCode={`curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks \\\n--header 'Authorization: Bearer {api_key}' \\\n--header 'Content-Type: application/json' \\\n--data-raw '{\n "rating": "like",\n "user": "abc-123",\n "content": "message feedback information"\n}'`}>
```bash {{ title: 'cURL' }}
curl -X POST '${props.appDetail.api_base_url}/messages/:message_id/feedbacks' \
@ -438,7 +441,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
--header 'Content-Type: application/json' \
--data-raw '{
"rating": "like",
"user": "abc-123"
"user": "abc-123",
"content": "message feedback information"
}'
```

View File

@ -60,6 +60,7 @@ Workflow applications offers non-session support and is ideal for translation, a
- `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG')
- `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR')
- `video` ('MP4', 'MOV', 'MPEG', 'MPGA')
- `custom` (Other file types)
- `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload
- `url` (string) Image URL (when the transfer method is `remote_url`)
- `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`)

View File

@ -60,6 +60,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from
- `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG')
- `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR')
- `video` ('MP4', 'MOV', 'MPEG', 'MPGA')
- `custom` (他のファイルタイプ)
- `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file`
- `url` (string) 画像URL転送方法が`remote_url`の場合)
- `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります転送方法が`local_file`の場合)

View File

@ -58,6 +58,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
- `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG'
- `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR'
- `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA'
- `custom` 具体类型包含:其他文件类型
- `transfer_method` (string) 传递方式,`remote_url` 图片地址 / `local_file` 上传文件
- `url` (string) 图片地址(仅当传递方式为 `remote_url` 时)
- `upload_file_id` (string) (string) 上传文件 ID仅当传递方式为 `local_file` 时)

View File

@ -28,10 +28,10 @@ const AppCard = ({
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={app.app.icon_type}
icon={app.app.icon}
background={app.app.icon_background}
imageUrl={app.app.icon_url}
iconType={appBasicInfo.icon_type}
icon={appBasicInfo.icon}
background={appBasicInfo.icon_background}
imageUrl={appBasicInfo.icon_url}
/>
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{appBasicInfo.mode === 'advanced-chat' && (

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import { Fragment, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext } from 'use-context-selector'
import { RiArrowDownSLine } from '@remixicon/react'
import { RiArrowDownSLine, RiLogoutBoxRLine } from '@remixicon/react'
import Link from 'next/link'
import { Menu, Transition } from '@headlessui/react'
import Indicator from '../indicator'
@ -15,11 +15,11 @@ import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type'
import WorkplaceSelector from './workplace-selector'
export type IAppSelector = {
isMobile: boolean
@ -27,8 +27,8 @@ export type IAppSelector = {
export default function AppSelector({ isMobile }: IAppSelector) {
const itemClassName = `
flex items-center w-full h-9 px-3 text-gray-700 text-[14px]
rounded-lg font-normal hover:bg-gray-50 cursor-pointer
flex items-center w-full h-9 px-3 text-text-secondary system-md-regular
rounded-lg hover:bg-state-base-hover cursor-pointer
`
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
@ -88,7 +88,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<Menu.Items
className="
absolute right-0 mt-1.5 w-60 max-w-80
divide-y divide-gray-100 origin-top-right rounded-lg bg-white
divide-y divide-divider-subtle origin-top-right rounded-lg bg-components-panel-bg-blur
shadow-lg
"
>
@ -96,11 +96,15 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<div className='flex flex-nowrap items-center px-4 py-[13px]'>
<Avatar name={userProfile.name} size={36} className='mr-3' />
<div className='grow'>
<div className='leading-5 font-normal text-[14px] text-gray-800 break-all'>{userProfile.name}</div>
<div className='leading-[18px] text-xs font-normal text-gray-500 break-all'>{userProfile.email}</div>
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
</div>
</div>
</Menu.Item>
<div className='px-1 py-1'>
<div className='mt-2 px-3 text-xs font-medium text-text-tertiary'>{t('common.userProfile.workspace')}</div>
<WorkplaceSelector />
</div>
<div className="px-1 py-1">
<Menu.Item>
<Link
@ -108,7 +112,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
href='/account'
target='_self' rel='noopener noreferrer'>
<div>{t('common.account.account')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>
</Menu.Item>
<Menu.Item>
@ -122,7 +126,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.emailSupport')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</a>
</Menu.Item>}
<Menu.Item>
@ -131,7 +135,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.communityFeedback')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>
</Menu.Item>
<Menu.Item>
@ -140,7 +144,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
href='https://discord.gg/5AEfbxcd9k'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.community')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>
</Menu.Item>
<Menu.Item>
@ -151,7 +155,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
}
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.helpCenter')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>
</Menu.Item>
<Menu.Item>
@ -160,7 +164,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<div>{t('common.userProfile.roadmap')}</div>
<ArrowUpRight className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</Link>
</Menu.Item>
{
@ -169,7 +173,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<div className={classNames(itemClassName, 'justify-between')} onClick={() => setAboutVisible(true)}>
<div>{t('common.userProfile.about')}</div>
<div className='flex items-center'>
<div className='mr-2 text-xs font-normal text-gray-500'>{langeniusVersionInfo.current_version}</div>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
</div>
@ -180,10 +184,10 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<Menu.Item>
<div className='p-1' onClick={() => handleLogout()}>
<div
className='flex items-center justify-between h-9 px-3 rounded-lg cursor-pointer group hover:bg-gray-50'
className='flex items-center justify-between h-9 px-3 rounded-lg cursor-pointer group hover:bg-state-base-hover'
>
<div className='font-normal text-[14px] text-gray-700'>{t('common.userProfile.logout')}</div>
<LogOut01 className='hidden w-[14px] h-[14px] text-gray-500 group-hover:flex' />
<div className='system-md-regular text-text-secondary'>{t('common.userProfile.logout')}</div>
<RiLogoutBoxRLine className='hidden w-4 h-4 text-text-tertiary group-hover:flex' />
</div>
</div>
</Menu.Item>

View File

@ -25,18 +25,18 @@ const Collapse = ({
const toggle = () => setOpen(!open)
return (
<div className={classNames('border border-gray-200 bg-gray-50 rounded-lg', wrapperClassName)}>
<div className='flex items-center justify-between leading-[18px] px-3 py-2 text-xs font-medium text-gray-800 cursor-pointer' onClick={toggle}>
<div className={classNames('bg-background-section-burn rounded-xl', wrapperClassName)}>
<div className='flex items-center justify-between leading-[18px] px-3 py-2 text-xs font-medium text-text-secondary cursor-pointer' onClick={toggle}>
{title}
{
open
? <ChevronDownIcon className='w-3 h-3 text-gray-400' />
: <ChevronRightIcon className='w-3 h-3 text-gray-400' />
? <ChevronDownIcon className='w-3 h-3 text-components-button-tertiary-text' />
: <ChevronRightIcon className='w-3 h-3 text-components-button-tertiary-text' />
}
</div>
{
open && (
<div className='py-2 border-t border-t-gray-100'>
<div className='py-1 mb-1 mx-1 border-t border-divider-subtle rounded-lg bg-components-panel-on-panel-item-bg'>
{
items.map(item => (
<div key={item.key} onClick={() => onSelect && onSelect(item)}>

View File

@ -12,7 +12,6 @@ export default function DataSourcePage() {
return (
<div className='mb-8'>
<div className='mb-2 text-sm font-medium text-gray-900'>{t('common.dataSource.add')}</div>
<DataSourceNotion workspaces={notionWorkspaces} />
<DataSourceWebsite provider={DataSourceProvider.jinaReader} />
<DataSourceWebsite provider={DataSourceProvider.fireCrawl} />

View File

@ -44,22 +44,22 @@ const ConfigItem: FC<Props> = ({
const onChangeAuthorizedPage = notionActions?.onChangeAuthorizedPage || function () { }
return (
<div className={cn(s['workspace-item'], 'flex items-center mb-1 py-1 pr-1 bg-white rounded-lg')} key={payload.id}>
<div className={cn(s['workspace-item'], 'flex items-center mb-1 py-1 pr-1 bg-components-panel-on-panel-item-bg rounded-lg')} key={payload.id}>
<payload.logo className='ml-3 mr-1.5' />
<div className='grow py-[7px] leading-[18px] text-[13px] font-medium text-gray-700 truncate' title={payload.name}>{payload.name}</div>
<div className='grow py-[7px] system-sm-medium text-text-secondary truncate' title={payload.name}>{payload.name}</div>
{
payload.isActive
? <Indicator className='shrink-0 mr-[6px]' />
? <Indicator className='shrink-0 mr-[6px]' color='green' />
: <Indicator className='shrink-0 mr-[6px]' color='yellow' />
}
<div className='shrink-0 mr-3 text-xs font-medium uppercase'>
<div className={`shrink-0 mr-3 text-xs font-medium uppercase ${payload.isActive ? 'text-util-colors-green-green-600' : 'text-util-colors-warning-warning-600'}`}>
{
payload.isActive
? t(isNotion ? 'common.dataSource.notion.connected' : 'common.dataSource.website.active')
: t(isNotion ? 'common.dataSource.notion.disconnected' : 'common.dataSource.website.inactive')
}
</div>
<div className='mr-2 w-[1px] h-3 bg-gray-100' />
<div className='mr-2 w-[1px] h-3 bg-divider-regular' />
{isNotion && (
<Operate payload={{
id: payload.id,
@ -70,8 +70,8 @@ const ConfigItem: FC<Props> = ({
{
isWebsite && !readOnly && (
<div className='p-2 text-gray-500 cursor-pointer rounded-md hover:bg-black/5' onClick={onRemove} >
<RiDeleteBinLine className='w-4 h-4 ' />
<div className='p-2 text-text-tertiary cursor-pointer rounded-md hover:bg-black/5' onClick={onRemove} >
<RiDeleteBinLine className='w-4 h-4' />
</div>
)
}

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { PlusIcon } from '@heroicons/react/24/solid'
import { RiAddLine } from '@remixicon/react'
import type { ConfigItemType } from './config-item'
import ConfigItem from './config-item'
@ -41,12 +41,12 @@ const Panel: FC<Props> = ({
const isWebsite = type === DataSourceType.website
return (
<div className='mb-2 border-[0.5px] border-gray-200 bg-gray-50 rounded-xl'>
<div className='mb-2 bg-background-section-burn rounded-xl'>
<div className='flex items-center px-3 py-[9px]'>
<div className={cn(s[`${type}-icon`], 'w-8 h-8 mr-3 border border-gray-100 rounded-lg')} />
<div className={cn(s[`${type}-icon`], 'w-8 h-8 mr-3 border border-divider-subtle rounded-lg bg-background-default')} />
<div className='grow'>
<div className='flex items-center h-5'>
<div className='text-sm font-medium text-gray-800'>{t(`common.dataSource.${type}.title`)}</div>
<div className='text-sm font-medium text-text-primary'>{t(`common.dataSource.${type}.title`)}</div>
{isWebsite && (
<div className='ml-1 leading-[18px] px-1.5 rounded-md bg-white border border-gray-100 text-xs font-medium text-gray-700'>
<span className='text-gray-500'>{t('common.dataSource.website.with')}</span> { provider === DataSourceProvider.fireCrawl ? '🔥 Firecrawl' : 'Jina Reader'}
@ -55,7 +55,7 @@ const Panel: FC<Props> = ({
</div>
{
!isConfigured && (
<div className='leading-5 text-xs text-gray-500'>
<div className='system-xs-medium text-text-tertiary'>
{t(`common.dataSource.${type}.description`)}
</div>
)
@ -81,13 +81,13 @@ const Panel: FC<Props> = ({
<>
{isSupportList && <div
className={
`flex items-center px-3 py-1 min-h-7 bg-white border-[0.5px] border-gray-200 text-xs font-medium text-primary-600 rounded-md
`flex items-center px-3 py-1 min-h-7 bg-components-button-secondary-bg border-[0.5px] border-components-button-secondary-border system-sm-medium text-components-button-secondary-accent-text rounded-md
${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
onClick={onConfigure}
>
<PlusIcon className='w-[14px] h-[14px] mr-[5px]' />
{t('common.dataSource.notion.addWorkspace')}
<RiAddLine className='w-4 h-4 text-components-button-secondary-accent-text mr-[5px]' />
{t('common.dataSource.connect')}
</div>}
</>
)
@ -98,8 +98,8 @@ const Panel: FC<Props> = ({
{isWebsite && !isConfigured && (
<div
className={
`flex items-center ml-3 px-3 h-7 bg-white border border-gray-200
rounded-md text-xs font-medium text-gray-700
`flex items-center ml-3 px-3 h-7 bg-components-button-secondary-bg border-[0.5px] border-components-button-secondary-border
rounded-md text-xs font-medium text-components-button-secondary-accent-text
${!readOnly ? 'cursor-pointer' : 'grayscale opacity-50 cursor-default'}`
}
onClick={!readOnly ? onConfigure : undefined}
@ -113,10 +113,10 @@ const Panel: FC<Props> = ({
isConfigured && (
<>
<div className='flex items-center px-3 h-[18px]'>
<div className='text-xs font-medium text-gray-500'>
<div className='system-xs-medium text-text-tertiary'>
{isNotion ? t('common.dataSource.notion.connectedWorkspace') : t('common.dataSource.website.configuredCrawlers')}
</div>
<div className='grow ml-3 border-t border-t-gray-100' />
<div className='grow ml-3 border-t border-t-divider-subtle' />
</div>
<div className='px-3 pt-2 pb-3'>
{

View File

@ -148,24 +148,25 @@ export default function AccountSetting({
show
onClose={onCancel}
>
<div className='mx-auto max-w-[1048px] h-[100vh] flex'>
<div className='w-[44px] sm:w-[224px] pl-4 pr-6 border-r border-divider-burn flex flex-col'>
<div className='mt-6 mb-8 px-3 py-2 text-text-primary title-2xl-semi-bold'>{t('common.userProfile.settings')}</div>
<div className='flex'>
<div className='w-[44px] sm:w-[200px] px-[1px] py-4 sm:p-4 border border-divider-burn shrink-0 sm:shrink-1 flex flex-col items-center sm:items-start'>
<div className='mb-8 ml-0 sm:ml-2 sm:text-base title-2xl-semi-bold text-text-primary'>{t('common.userProfile.settings')}</div>
<div className='w-full'>
{
menuItems.map(menuItem => (
<div key={menuItem.key} className='mb-2'>
{!isCurrentWorkspaceDatasetOperator && (
<div className='py-2 pl-3 pb-1 mb-0.5 text-text-tertiary system-xs-medium-uppercase'>{menuItem.name}</div>
<div className='px-2 mb-[6px] sm:text-xs system-xs-medium-uppercase text-text-tertiary'>{menuItem.name}</div>
)}
<div>
{
menuItem.items.map(item => (
<div
key={item.key}
className={cn(
'flex items-center mb-0.5 p-1 pl-3 h-[37px] text-sm cursor-pointer rounded-lg',
activeMenu === item.key ? 'bg-state-base-active text-components-menu-item-text-active system-sm-semibold' : 'text-components-menu-item-text system-sm-medium')}
className={`
flex items-center h-[37px] mb-[2px] text-sm cursor-pointer rounded-lg
${activeMenu === item.key ? 'system-sm-semibold text-components-menu-item-text-active bg-state-base-active' : 'system-sm-medium text-components-menu-item-text'}
`}
title={item.name}
onClick={() => setActiveMenu(item.key)}
>
@ -180,17 +181,19 @@ export default function AccountSetting({
}
</div>
</div>
<div className='relative flex w-[824px]'>
<div className='absolute top-6 -right-11 flex flex-col items-center z-[9999]'>
<Button
variant='tertiary'
size='large'
className='px-2'
onClick={onCancel}
>
<RiCloseLine className='w-5 h-5' />
</Button>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
<div ref={scrollRef} className='relative w-[824px] h-[720px] pb-4 overflow-y-auto'>
<div className={cn('sticky top-0 px-6 py-4 flex items-center h-14 mb-4 bg-components-panel-bg title-2xl-semi-bold text-text-primary z-20', scrolled && scrolledClassName)}>
<div className='shrink-0'>{activeItem?.name}</div>
{
activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
)
}
<div className='grow flex justify-end'>
<div className='z-[10] flex items-center justify-center -mr-4 p-2 cursor-pointer rounded-[10px] hover:bg-components-button-tertiary-bg' onClick={onCancel}>
<RiCloseLine className='w-5 h-5 text-components-button-tertiary-text' />
</div>
</div>
</div>
<div ref={scrollRef} className='w-full pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b border-divider-regular')}>

View File

@ -13,7 +13,7 @@ import { timezones } from '@/utils/timezone'
import { languages } from '@/i18n/language'
const titleClassName = `
mb-2 text-sm font-medium text-gray-900
mb-2 system-sm-semibold text-text-secondary
`
export default function LanguagePage() {

View File

@ -85,32 +85,32 @@ const MembersPage = () => {
</div>
</div>
<div className='overflow-visible lg:overflow-visible'>
<div className='flex items-center py-[7px] border-b border-gray-200 min-w-[480px]'>
<div className='grow px-3 text-xs font-medium text-gray-500'>{t('common.members.name')}</div>
<div className='shrink-0 w-[104px] text-xs font-medium text-gray-500'>{t('common.members.lastActive')}</div>
<div className='shrink-0 w-[96px] px-3 text-xs font-medium text-gray-500'>{t('common.members.role')}</div>
<div className='flex items-center py-[7px] border-b border-divider-regular min-w-[480px]'>
<div className='grow px-3 system-xs-medium-uppercase text-text-tertiary'>{t('common.members.name')}</div>
<div className='shrink-0 w-[104px] system-xs-medium-uppercase text-text-tertiary'>{t('common.members.lastActive')}</div>
<div className='shrink-0 w-[96px] px-3 system-xs-medium-uppercase text-text-tertiary'>{t('common.members.role')}</div>
</div>
<div className='min-w-[480px] relative'>
{
accounts.map(account => (
<div key={account.id} className='flex border-b border-gray-100'>
<div key={account.id} className='flex border-b border-divider-subtle'>
<div className='grow flex items-center py-2 px-3'>
<Avatar size={24} className='mr-2' name={account.name} />
<div className=''>
<div className='text-[13px] font-medium text-gray-700 leading-[18px]'>
<div className='text-text-secondary system-sm-medium'>
{account.name}
{account.status === 'pending' && <span className='ml-1 text-xs text-[#DC6803]'>{t('common.members.pending')}</span>}
{userProfile.email === account.email && <span className='text-xs text-gray-500'>{t('common.members.you')}</span>}
{account.status === 'pending' && <span className='ml-1 system-xs-regular text-[#DC6803]'>{t('common.members.pending')}</span>}
{userProfile.email === account.email && <span className='system-xs-regular text-text-tertiary'>{t('common.members.you')}</span>}
</div>
<div className='text-xs text-gray-500 leading-[18px]'>{account.email}</div>
<div className='text-text-tertiary system-xs-regular'>{account.email}</div>
</div>
</div>
<div className='shrink-0 flex items-center w-[104px] py-2 text-[13px] text-gray-700'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
<div className='shrink-0 flex items-center w-[104px] py-2 system-xs-regular text-text-secondary'>{dayjs(Number((account.last_active_at || account.created_at)) * 1000).locale(locale === 'zh-Hans' ? 'zh-cn' : 'en').fromNow()}</div>
<div className='shrink-0 w-[96px] flex items-center'>
{
((isCurrentWorkspaceOwner && account.role !== 'owner') || (isCurrentWorkspaceManager && !['owner', 'admin'].includes(account.role)))
? <Operation member={account} operatorRole={currentWorkspace.role} onOperate={mutate} />
: <div className='px-3 text-[13px] text-gray-700'>{RoleMap[account.role] || RoleMap.normal}</div>
: <div className='px-3 system-xs-regular text-text-secondary'>{RoleMap[account.role] || RoleMap.normal}</div>
}
</div>
</div>

View File

@ -11,7 +11,7 @@ const HeaderWrapper = ({
children,
}: HeaderWrapperProps) => {
const pathname = usePathname()
const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools', '/account'].includes(pathname)
const isBordered = ['/apps', '/datasets', '/datasets/create', '/tools'].includes(pathname)
return (
<div className={classNames(

View File

@ -52,7 +52,7 @@ const Header = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedSegment])
return (
<div className='flex flex-1 items-center justify-between pr-3'>
<div className='flex flex-1 items-center justify-between px-4 bg-background-body'>
<div className='flex items-center'>
{isMobile && <div
className='flex items-center justify-center h-8 w-8 cursor-pointer'

View File

@ -17,20 +17,20 @@ type ColorMap = {
}
const BACKGROUND_MAP: ColorMap = {
green: 'bg-[#31C48D]',
orange: 'bg-[#FF5A1F]',
red: 'bg-[#F04438]',
blue: 'bg-[#36BFFA]',
green: 'bg-components-badge-status-light-success-bg',
orange: 'bg-components-badge-status-light-warning-bg',
red: 'bg-components-badge-status-light-error-bg',
blue: 'bg-components-badge-status-light-normal-bg',
yellow: 'bg-[#FDB022]',
gray: 'bg-[#D0D5DD]',
gray: 'bg-components-badge-status-light-disabled-bg',
}
const BORDER_MAP: ColorMap = {
green: 'border-[#0E9F6E]',
orange: 'border-[#D03801]',
red: 'border-[#D92D20]',
blue: 'border-[#0BA5EC]',
green: 'border-components-badge-status-light-success-border-inner',
orange: 'border-components-badge-status-light-warning-border-inner',
red: 'border-components-badge-status-light-error-border-inner',
blue: 'border-components-badge-status-light-normal-border-inner',
yellow: 'border-[#F79009]',
gray: 'border-[#98A2B3]',
gray: 'border-components-badge-status-light-disabled-border-inner',
}
const SHADOW_MAP: ColorMap = {
green: 'shadow-[0_0_5px_-3px_rgba(14,159,110,0.1),0.5px_0.5px_3px_rgba(14,159,110,0.3),inset_1.5px_1.5px_0px_rgba(255,255,255,0.2)]',

View File

@ -1,13 +1,19 @@
'use client'
import { useSearchParams } from 'next/navigation'
import { useTranslation } from 'react-i18next'
const Empty = () => {
const { t } = useTranslation()
const searchParams = useSearchParams()
return (
<div className='flex flex-col items-center'>
<div className="shrink-0 w-[163px] h-[149px] bg-cover bg-no-repeat bg-[url('~@/app/components/tools/add-tool-modal/empty.png')]"></div>
<div className='mb-1 text-[13px] font-medium text-text-secondary leading-[18px]'>{t('tools.addToolModal.emptyTitle')}</div>
<div className='text-[13px] text-text-tertiary leading-[18px]'>{t('tools.addToolModal.emptyTip')}</div>
<div className='mb-1 text-[13px] font-medium text-text-primary leading-[18px]'>
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTitle' : 'emptyTitleCustom'}`)}
</div>
<div className='text-[13px] text-text-tertiary leading-[18px]'>
{t(`tools.addToolModal.${searchParams.get('category') === 'workflow' ? 'emptyTip' : 'emptyTipCustom'}`)}
</div>
</div>
)
}

View File

@ -58,7 +58,7 @@ const Blocks = ({
>
{
classification !== '-' && !!list.length && (
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-text-tertiary'>
{t(`workflow.tabs.${classification}`)}
</div>
)
@ -68,7 +68,7 @@ const Blocks = ({
<Tooltip
key={block.type}
position='right'
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
popupClassName='w-[200px]'
popupContent={(
<div>
<BlockIcon
@ -76,21 +76,21 @@ const Blocks = ({
className='mb-2'
type={block.type}
/>
<div className='mb-1 text-sm leading-5 text-gray-900'>{block.title}</div>
<div className='text-xs text-gray-700 leading-[18px]'>{nodesExtraData[block.type].about}</div>
<div className='mb-1 system-md-medium text-text-primary'>{block.title}</div>
<div className='text-text-tertiary system-xs-regular'>{nodesExtraData[block.type].about}</div>
</div>
)}
>
<div
key={block.type}
className='flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50 cursor-pointer'
className='flex items-center px-3 w-full h-8 rounded-lg hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect(block.type)}
>
<BlockIcon
className='mr-2 shrink-0'
type={block.type}
/>
<div className='text-sm text-gray-900'>{block.title}</div>
<div className='text-sm text-text-secondary'>{block.title}</div>
</div>
</Tooltip>
))
@ -103,7 +103,7 @@ const Blocks = ({
<div className='p-1'>
{
isEmpty && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
)
}
{

View File

@ -27,6 +27,7 @@ import SearchBox from '@/app/components/plugins/marketplace/search-box'
import {
Plus02,
} from '@/app/components/base/icons/src/vender/line/general'
import classNames from '@/utils/classnames'
type NodeSelectorProps = {
open?: boolean
@ -117,12 +118,12 @@ const NodeSelector: FC<NodeSelectorProps> = ({
<div
className={`
flex items-center justify-center
w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10
w-4 h-4 rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover cursor-pointer z-10
${triggerClassName?.(open)}
`}
style={triggerStyle}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
<Plus02 className='w-2.5 h-2.5' />
</div>
)
}

View File

@ -36,16 +36,16 @@ const Tabs: FC<TabsProps> = ({
<div onClick={e => e.stopPropagation()}>
{
!noBlocks && (
<div className='flex items-center px-3 border-b-[0.5px] border-b-black/5'>
<div className='flex items-center px-3 border-b-[0.5px] border-divider-subtle'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative mr-4 h-[34px] text-[13px] leading-[34px] font-medium cursor-pointer',
'relative mr-4 pt-1 pb-2 system-sm-medium cursor-pointer',
activeTab === tab.key
? 'text-gray-700 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-primary-600'
: 'text-gray-500',
? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600'
: 'text-text-tertiary',
)}
onClick={() => onActiveTabChange(tab.key)}
>

View File

@ -78,7 +78,7 @@ const Blocks = ({
<div className='p-1 max-w-[320px]'>
{
!tools.length && !showWorkflowEmpty && (
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
)
}
{!tools.length && showWorkflowEmpty && (

View File

@ -506,3 +506,5 @@ export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
export const CUSTOM_NODE = 'custom'
export const CUSTOM_EDGE = 'custom'
export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK'
export const DEFAULT_RETRY_MAX = 3
export const DEFAULT_RETRY_INTERVAL = 100

View File

@ -13,7 +13,7 @@ const EditingTitle = () => {
const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft)
return (
<div className='flex items-center h-[18px] text-xs text-gray-500'>
<div className='flex items-center h-[18px] system-xs-regular text-text-tertiary'>
{
!!draftUpdatedAt && (
<>

View File

@ -27,6 +27,7 @@ import {
} from '../hooks'
import AppPublisher from '../../app/app-publisher'
import { ToastContext } from '../../base/toast'
import Divider from '../../base/divider'
import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import RunningTitle from './running-title'
@ -144,15 +145,12 @@ const Header: FC = () => {
return (
<div
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14'
style={{
background: 'linear-gradient(180deg, #F9FAFB 0%, rgba(249, 250, 251, 0.00) 100%)',
}}
className='absolute top-0 left-0 z-10 flex items-center justify-between w-full px-3 h-14 bg-mask-top2bottom-gray-50-to-transparent'
>
<div>
{
appSidebarExpand === 'collapse' && (
<div className='text-xs font-medium text-gray-700'>{appDetail?.name}</div>
<div className='system-xs-regular text-text-tertiary'>{appDetail?.name}</div>
)
}
{
@ -171,7 +169,7 @@ const Header: FC = () => {
{/* <GlobalVariableButton disabled={nodesReadOnly} /> */}
{isChatMode && <ChatVariableButton disabled={nodesReadOnly} />}
<EnvButton disabled={nodesReadOnly} />
<div className='w-[1px] h-3.5 bg-gray-200'></div>
<Divider type='vertical' className='h-3.5 mx-auto' />
<RunAndHistory />
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
@ -196,12 +194,11 @@ const Header: FC = () => {
}
{
viewHistory && (
<div className='flex items-center'>
<div className='flex items-center space-x-2'>
<ViewHistory withText />
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Divider type='vertical' className='h-3.5 mx-auto' />
<Button
variant='primary'
className='mr-2'
onClick={handleGoBackToEdit}
>
<ArrowNarrowLeft className='w-4 h-4 mr-1' />
@ -212,14 +209,13 @@ const Header: FC = () => {
}
{
restoring && (
<div className='flex items-center'>
<div className='flex items-center space-x-2'>
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
{t('workflow.common.features')}
</Button>
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
<Divider type='vertical' className='h-3.5 mx-auto' />
<Button
className='mr-2'
onClick={handleCancelRestore}
>
{t('common.operation.cancel')}

View File

@ -7,8 +7,10 @@ import {
} from '@remixicon/react'
import TipPopup from '../operator/tip-popup'
import { useWorkflowHistoryStore } from '../workflow-history-store'
import Divider from '../../base/divider'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
import classNames from '@/utils/classnames'
export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void }
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
@ -29,36 +31,35 @@ const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
const { nodesReadOnly } = useNodesReadOnly()
return (
<div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'>
<div className='flex items-center space-x-0.5 p-0.5 backdrop-blur-[5px] rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-lg'>
<TipPopup title={t('workflow.common.undo')!} shortcuts={['ctrl', 'z']}>
<div
data-tooltip-id='workflow.undo'
className={`
flex items-center px-1.5 w-8 h-8 rounded-md text-[13px] font-medium
hover:bg-black/5 hover:text-gray-700 cursor-pointer select-none
${(nodesReadOnly || buttonsDisabled.undo) && 'hover:bg-transparent opacity-50 !cursor-not-allowed'}
`}
className={
classNames('flex items-center px-1.5 w-8 h-8 rounded-md system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer select-none',
(nodesReadOnly || buttonsDisabled.undo)
&& 'hover:bg-transparent text-text-disabled hover:text-text-disabled cursor-not-allowed')}
onClick={() => !nodesReadOnly && !buttonsDisabled.undo && handleUndo()}
>
<RiArrowGoBackLine className='h-4 w-4' />
</div>
</TipPopup>
</TipPopup >
<TipPopup title={t('workflow.common.redo')!} shortcuts={['ctrl', 'y']}>
<div
data-tooltip-id='workflow.redo'
className={`
flex items-center px-1.5 w-8 h-8 rounded-md text-[13px] font-medium
hover:bg-black/5 hover:text-gray-700 cursor-pointer select-none
${(nodesReadOnly || buttonsDisabled.redo) && 'hover:bg-transparent opacity-50 !cursor-not-allowed'}
`}
className={
classNames('flex items-center px-1.5 w-8 h-8 rounded-md system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer select-none',
(nodesReadOnly || buttonsDisabled.redo)
&& 'hover:bg-transparent text-text-disabled hover:text-text-disabled cursor-not-allowed',
)}
onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()}
>
<RiArrowGoForwardFill className='h-4 w-4' />
</div>
</TipPopup>
<div className="mx-[3px] w-[1px] h-3.5 bg-gray-200"></div>
<Divider type='vertical' className="h-3.5 mx-0.5" />
<ViewWorkflowHistory />
</div>
</div >
)
}

View File

@ -17,6 +17,7 @@ import {
} from '../hooks'
import TipPopup from '../operator/tip-popup'
import type { WorkflowHistoryState } from '../workflow-history-store'
import Divider from '../../base/divider'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
@ -24,6 +25,7 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useStore as useAppStore } from '@/app/components/app/store'
import classNames from '@/utils/classnames'
type ChangeHistoryEntry = {
label: string
@ -47,7 +49,7 @@ const ViewWorkflowHistory = () => {
setCurrentLogItem: state.setCurrentLogItem,
setShowMessageLogModal: state.setShowMessageLogModal,
})))
const reactflowStore = useStoreApi()
const reactFlowStore = useStoreApi()
const { store, getHistoryLabel } = useWorkflowHistory()
const { pastStates, futureStates, undo, redo, clear } = store.temporal.getState()
@ -59,7 +61,7 @@ const ViewWorkflowHistory = () => {
}, [clear])
const handleSetState = useCallback(({ index }: ChangeHistoryEntry) => {
const { setEdges, setNodes } = reactflowStore.getState()
const { setEdges, setNodes } = reactFlowStore.getState()
const diff = currentHistoryStateIndex + index
if (diff === 0)
return
@ -75,7 +77,7 @@ const ViewWorkflowHistory = () => {
setEdges(edges)
setNodes(nodes)
}, [currentHistoryStateIndex, reactflowStore, redo, store, undo])
}, [currentHistoryStateIndex, reactFlowStore, redo, store, undo])
const calculateStepLabel = useCallback((index: number) => {
if (!index)
@ -83,8 +85,7 @@ const ViewWorkflowHistory = () => {
const count = index < 0 ? index * -1 : index
return `${index > 0 ? t('workflow.changeHistory.stepForward', { count }) : t('workflow.changeHistory.stepBackward', { count })}`
}
, [t])
}, [t])
const calculateChangeList: ChangeHistoryList = useMemo(() => {
const filterList = (list: any, startIndex = 0, reverse = false) => list.map((state: Partial<WorkflowHistoryState>, index: number) => {
@ -125,10 +126,11 @@ const ViewWorkflowHistory = () => {
title={t('workflow.changeHistory.title')}
>
<div
className={`
flex items-center justify-center w-8 h-8 rounded-md hover:bg-black/5 cursor-pointer
${open && 'bg-primary-50'} ${nodesReadOnly && 'bg-primary-50 opacity-50 !cursor-not-allowed'}
`}
className={
classNames('flex items-center justify-center w-8 h-8 rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer',
open && 'bg-state-accent-active text-text-accent',
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
)}
onClick={() => {
if (nodesReadOnly)
return
@ -136,16 +138,16 @@ const ViewWorkflowHistory = () => {
setShowMessageLogModal(false)
}}
>
<RiHistoryLine className={`w-4 h-4 hover:bg-black/5 hover:text-gray-700 ${open ? 'text-primary-600' : 'text-gray-500'}`} />
<RiHistoryLine className='w-4 h-4' />
</div>
</TipPopup>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[12]'>
<div
className='flex flex-col ml-2 min-w-[240px] max-w-[360px] bg-white border-[0.5px] border-gray-200 shadow-xl rounded-xl overflow-y-auto'
className='flex flex-col ml-2 min-w-[240px] max-w-[360px] bg-components-panel-bg-blur backdrop-blur-[5px] border-[0.5px] border-components-panel-border shadow-xl rounded-xl overflow-y-auto'
>
<div className='sticky top-0 bg-white flex items-center justify-between px-4 pt-3 text-base font-semibold text-gray-900'>
<div className='grow'>{t('workflow.changeHistory.title')}</div>
<div className='sticky top-0 flex items-center justify-between px-4 pt-3'>
<div className='grow text-text-secondary system-mg-regular'>{t('workflow.changeHistory.title')}</div>
<div
className='shrink-0 flex items-center justify-center w-6 h-6 cursor-pointer'
onClick={() => {
@ -154,7 +156,7 @@ const ViewWorkflowHistory = () => {
setOpen(false)
}}
>
<RiCloseLine className='w-4 h-4 text-gray-500' />
<RiCloseLine className='w-4 h-4 text-text-secondary' />
</div>
</div>
{
@ -168,8 +170,8 @@ const ViewWorkflowHistory = () => {
{
!calculateChangeList.statesCount && (
<div className='py-12'>
<RiHistoryLine className='mx-auto mb-2 w-8 h-8 text-gray-300' />
<div className='text-center text-[13px] text-gray-400'>
<RiHistoryLine className='mx-auto mb-2 w-8 h-8 text-text-tertiary' />
<div className='text-center text-[13px] text-text-tertiary'>
{t('workflow.changeHistory.placeholder')}
</div>
</div>
@ -181,8 +183,8 @@ const ViewWorkflowHistory = () => {
<div
key={item?.index}
className={cn(
'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer',
item?.index === currentHistoryStateIndex && 'bg-primary-50',
'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-state-base-hover text-text-secondary cursor-pointer',
item?.index === currentHistoryStateIndex && 'bg-state-base-hover',
)}
onClick={() => {
handleSetState(item)
@ -192,8 +194,7 @@ const ViewWorkflowHistory = () => {
<div>
<div
className={cn(
'flex items-center text-[13px] font-medium leading-[18px]',
item?.index === currentHistoryStateIndex && 'text-primary-600',
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
)}
>
{item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')})
@ -207,8 +208,8 @@ const ViewWorkflowHistory = () => {
<div
key={item?.index}
className={cn(
'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-primary-50 cursor-pointer',
item?.index === calculateChangeList.statesCount - 1 && 'bg-primary-50',
'flex mb-0.5 px-2 py-[7px] rounded-lg hover:bg-state-base-hover cursor-pointer',
item?.index === calculateChangeList.statesCount - 1 && 'bg-state-base-hover',
)}
onClick={() => {
handleSetState(item)
@ -218,8 +219,7 @@ const ViewWorkflowHistory = () => {
<div>
<div
className={cn(
'flex items-center text-[13px] font-medium leading-[18px]',
item?.index === calculateChangeList.statesCount - 1 && 'text-primary-600',
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
)}
>
{item?.label || t('workflow.changeHistory.sessionStart')} ({calculateStepLabel(item?.index)})
@ -234,12 +234,12 @@ const ViewWorkflowHistory = () => {
}
{
!!calculateChangeList.statesCount && (
<>
<div className="h-[1px] bg-gray-100" />
<div className='px-0.5'>
<Divider className='m-0' />
<div
className={cn(
'flex my-0.5 px-2 py-[7px] rounded-lg cursor-pointer',
'hover:bg-red-50 hover:text-red-600',
'flex my-0.5 px-2 py-[7px] rounded-lg text-text-secondary cursor-pointer',
'hover:bg-state-base-hover',
)}
onClick={() => {
handleClearHistory()
@ -256,12 +256,12 @@ const ViewWorkflowHistory = () => {
</div>
</div>
</div>
</>
</div>
)
}
<div className="px-3 w-[240px] py-2 text-xs text-gray-500" >
<div className="px-3 w-[240px] py-2 text-xs text-text-tertiary" >
<div className="flex items-center mb-1 h-[22px] font-medium uppercase">{t('workflow.changeHistory.hint')}</div>
<div className="mb-1 text-gray-700 leading-[18px]">{t('workflow.changeHistory.hintText')}</div>
<div className="mb-1 text-text-tertiary leading-[18px]">{t('workflow.changeHistory.hintText')}</div>
</div>
</div>
</PortalToFollowElemContent>

View File

@ -28,6 +28,7 @@ import {
getFilesInLogs,
} from '@/app/components/base/file-uploader/utils'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { NodeTracing } from '@/types/workflow'
export const useWorkflowRun = () => {
const store = useStoreApi()
@ -114,6 +115,7 @@ export const useWorkflowRun = () => {
onIterationStart,
onIterationNext,
onIterationFinish,
onNodeRetry,
onError,
...restCallback
} = callback || {}
@ -440,10 +442,13 @@ export const useWorkflowRun = () => {
})
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...data,
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
...data,
...(draft.tracing[currentIndex].retryDetail
? { retryDetail: draft.tracing[currentIndex].retryDetail }
: {}),
} as any
}
}))
@ -616,6 +621,41 @@ export const useWorkflowRun = () => {
if (onIterationFinish)
onIterationFinish(params)
},
onNodeRetry: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id)
if (currentRetryNodeIndex > -1) {
const currentRetryNode = tracing[currentRetryNodeIndex]
if (currentRetryNode.retryDetail)
draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing)
else
draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing]
}
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._retryIndex = data.retry_index
})
setNodes(newNodes)
if (onNodeRetry)
onNodeRetry(params)
},
onParallelBranchStarted: (params) => {
// console.log(params, 'parallel start')
},

View File

@ -57,6 +57,7 @@ import {
import I18n from '@/context/i18n'
import { CollectionType } from '@/app/components/tools/types'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { useWorkflowConfig } from '@/service/use-workflow'
export const useIsChatMode = () => {
const appDetail = useAppStore(s => s.appDetail)
@ -69,7 +70,9 @@ export const useWorkflow = () => {
const { locale } = useContext(I18n)
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const appId = useStore(s => s.appId)
const nodesExtraData = useNodesExtraData()
const { data: workflowConfig } = useWorkflowConfig(appId)
const setPanelWidth = useCallback((width: number) => {
localStorage.setItem('workflow-node-panel-width', `${width}`)
workflowStore.setState({ panelWidth: width })
@ -336,15 +339,15 @@ export const useWorkflow = () => {
for (let i = 0; i < parallelList.length; i++) {
const parallel = parallelList[i]
if (parallel.depth > PARALLEL_DEPTH_LIMIT) {
if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
const { setShowTips } = workflowStore.getState()
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: PARALLEL_DEPTH_LIMIT }))
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
return false
}
}
return true
}, [t, workflowStore])
}, [t, workflowStore, workflowConfig?.parallel_depth_limit])
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
const {

View File

@ -180,7 +180,7 @@ const Workflow: FC<WorkflowProps> = memo(({
return () => {
handleSyncWorkflowDraft(true, true)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
@ -281,7 +281,7 @@ const Workflow: FC<WorkflowProps> = memo(({
<div
id='workflow-container'
className={`
relative w-full min-w-[960px] h-full bg-[#F0F2F7]
relative w-full min-w-[960px] h-full
${workflowReadOnly && 'workflow-panel-animation'}
${nodeAnimation && 'workflow-node-animation'}
`}
@ -373,7 +373,8 @@ const Workflow: FC<WorkflowProps> = memo(({
<Background
gap={[14, 14]}
size={2}
color='#E4E5E7'
className="bg-workflow-canvas-workflow-bg"
color='var(--color-workflow-canvas-workflow-dot-color)'
/>
</ReactFlow>
</div>
@ -403,7 +404,7 @@ const WorkflowWrap = memo(() => {
if (!data || isLoading) {
return (
<div className='flex justify-center items-center relative w-full h-full bg-[#F0F2F7]'>
<div className='flex justify-center items-center relative w-full h-full'>
<Loading />
</div>
)

View File

@ -17,17 +17,25 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import type { NodeTracing } from '@/types/workflow'
import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { Emoji } from '@/app/components/tools/types'
const i18nPrefix = 'workflow.singleRun'
type BeforeRunFormProps = {
nodeName: string
nodeType?: BlockEnum
toolIcon?: string | Emoji
onHide: () => void
onRun: (submitData: Record<string, any>) => void
onStop: () => void
runningStatus: NodeRunningStatus
result?: JSX.Element
forms: FormProps[]
retryDetails?: NodeTracing[]
onRetryDetailBack?: any
}
function formatValue(value: string | any, type: InputVarType) {
@ -50,12 +58,16 @@ function formatValue(value: string | any, type: InputVarType) {
}
const BeforeRunForm: FC<BeforeRunFormProps> = ({
nodeName,
nodeType,
toolIcon,
onHide,
onRun,
onStop,
runningStatus,
result,
forms,
retryDetails,
onRetryDetailBack = () => { },
}) => {
const { t } = useTranslation()
@ -122,48 +134,69 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
<div className='text-base font-semibold text-gray-900 truncate'>
{t(`${i18nPrefix}.testRun`)} {nodeName}
</div>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={() => {
onHide()
}}>
<RiCloseLine className='w-4 h-4 text-gray-500 ' />
</div>
</div>
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
{
retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'>
<RetryResultPanel
list={retryDetails.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
node_type: nodeType!,
extras: {
icon: toolIcon!,
},
}))}
onBack={onRetryDetailBack}
/>
</div>
)
}
{
!retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => (
<div key={index}>
<Form
key={index}
className={cn(index < forms.length - 1 && 'mb-4')}
{...form}
/>
{index < forms.length - 1 && <Split />}
</div>
))}
</div>
))}
</div>
<div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && (
<div
className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer'
onClick={onStop}
>
<StopCircle className='w-4 h-4 text-gray-500' />
<div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && (
<div
className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer'
onClick={onStop}
>
<StopCircle className='w-4 h-4 text-gray-500' />
</div>
)}
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
)}
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
{isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />}
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
</Button>
</div>
{isRunning && (
<ResultPanel status='running' showSteps={false} />
)}
{isFinished && (
<>
{result}
</>
)}
</div>
{isRunning && (
<ResultPanel status='running' showSteps={false} />
)}
{isFinished && (
<>
{result}
</>
)}
</div>
)
}
</div>
</div>
)

View File

@ -14,7 +14,6 @@ import type {
CommonNodeType,
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import Tooltip from '@/app/components/base/tooltip'
type ErrorHandleProps = Pick<Node, 'id' | 'data'>
@ -45,7 +44,6 @@ const ErrorHandle = ({
return (
<>
<Split />
<div className='py-4'>
<Collapse
disabled={!error_strategy}

View File

@ -0,0 +1,41 @@
import {
useCallback,
useState,
} from 'react'
import type { WorkflowRetryConfig } from './types'
import {
useNodeDataUpdate,
} from '@/app/components/workflow/hooks'
import type { NodeTracing } from '@/types/workflow'
export const useRetryConfig = (
id: string,
) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
retry_config: value,
},
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
handleRetryConfigChange,
}
}
export const useRetryDetailShowInSingleRun = () => {
const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>()
const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => {
setRetryDetails(details)
}, [])
return {
retryDetails,
handleRetryDetailsChange,
}
}

View File

@ -0,0 +1,88 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAlertFill,
RiCheckboxCircleFill,
RiLoader2Line,
} from '@remixicon/react'
import type { Node } from '@/app/components/workflow/types'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type RetryOnNodeProps = Pick<Node, 'id' | 'data'>
const RetryOnNode = ({
data,
}: RetryOnNodeProps) => {
const { t } = useTranslation()
const { retry_config } = data
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
const {
isRunning,
isSuccessful,
isException,
isFailed,
} = useMemo(() => {
return {
isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
}
}, [data._runningStatus, showSelectedBorder])
const showDefault = !isRunning && !isSuccessful && !isException && !isFailed
if (!retry_config)
return null
return (
<div className='px-3'>
<div className={cn(
'flex items-center justify-between px-[5px] py-1 bg-workflow-block-parma-bg border-[0.5px] border-transparent rounded-md system-xs-medium-uppercase text-text-tertiary',
isRunning && 'bg-state-accent-hover border-state-accent-active text-text-accent',
isSuccessful && 'bg-state-success-hover border-state-success-active text-text-success',
(isException || isFailed) && 'bg-state-warning-hover border-state-warning-active text-text-warning',
)}>
<div className='flex items-center'>
{
showDefault && (
t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries })
)
}
{
isRunning && (
<>
<RiLoader2Line className='animate-spin mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrying')}
</>
)
}
{
isSuccessful && (
<>
<RiCheckboxCircleFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrySuccessful')}
</>
)
}
{
(isFailed || isException) && (
<>
<RiAlertFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retryFailed')}
</>
)
}
</div>
{
!showDefault && (
<div>
{data._retryIndex}/{data.retry_config?.max_retries}
</div>
)
}
</div>
</div>
)
}
export default RetryOnNode

View File

@ -0,0 +1,117 @@
import { useTranslation } from 'react-i18next'
import { useRetryConfig } from './hooks'
import s from './style.module.css'
import Switch from '@/app/components/base/switch'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import type {
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
type RetryOnPanelProps = Pick<Node, 'id' | 'data'>
const RetryOnPanel = ({
id,
data,
}: RetryOnPanelProps) => {
const { t } = useTranslation()
const { handleRetryConfigChange } = useRetryConfig(id)
const { retry_config } = data
const handleRetryEnabledChange = (value: boolean) => {
handleRetryConfigChange({
retry_enabled: value,
max_retries: retry_config?.max_retries || 3,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleMaxRetriesChange = (value: number) => {
if (value > 10)
value = 10
else if (value < 1)
value = 1
handleRetryConfigChange({
retry_enabled: true,
max_retries: value,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleRetryIntervalChange = (value: number) => {
if (value > 5000)
value = 5000
else if (value < 100)
value = 100
handleRetryConfigChange({
retry_enabled: true,
max_retries: retry_config?.max_retries || 3,
retry_interval: value,
})
}
return (
<>
<div className='pt-2'>
<div className='flex items-center justify-between px-4 py-2 h-10'>
<div className='flex items-center'>
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div>
</div>
<Switch
defaultValue={retry_config?.retry_enabled}
onChange={v => handleRetryEnabledChange(v)}
/>
</div>
{
retry_config?.retry_enabled && (
<div className='px-4 pb-2'>
<div className='flex items-center mb-1 w-full'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.maxRetries')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.max_retries || 3}
onChange={handleMaxRetriesChange}
min={1}
max={10}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.max_retries || 3}
onChange={e => handleMaxRetriesChange(e.target.value as any)}
min={1}
max={10}
unit={t('workflow.nodes.common.retry.times') || ''}
className={s.input}
/>
</div>
<div className='flex items-center'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.retryInterval')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.retry_interval || 1000}
onChange={handleRetryIntervalChange}
min={100}
max={5000}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.retry_interval || 1000}
onChange={e => handleRetryIntervalChange(e.target.value as any)}
min={100}
max={5000}
unit={t('workflow.nodes.common.retry.ms') || ''}
className={s.input}
/>
</div>
</div>
)
}
</div>
<Split className='mx-4 mt-2' />
</>
)
}
export default RetryOnPanel

View File

@ -0,0 +1,5 @@
.input::-webkit-inner-spin-button,
.input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@ -0,0 +1,5 @@
export type WorkflowRetryConfig = {
max_retries: number
retry_interval: number
retry_enabled: boolean
}

View File

@ -25,7 +25,10 @@ import {
useNodesReadOnly,
useToolIcon,
} from '../../hooks'
import { hasErrorHandleNode } from '../../utils'
import {
hasErrorHandleNode,
hasRetryNode,
} from '../../utils'
import { useNodeIterationInteractions } from '../iteration/use-interactions'
import type { IterationNodeType } from '../iteration/types'
import {
@ -35,6 +38,7 @@ import {
import NodeResizer from './components/node-resizer'
import NodeControl from './components/node-control'
import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
import RetryOnNode from './components/retry/retry-on-node'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
@ -237,6 +241,14 @@ const BaseNode: FC<BaseNodeProps> = ({
</div>
)
}
{
hasRetryNode(data.type) && (
<RetryOnNode
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnNode

View File

@ -21,9 +21,11 @@ import {
TitleInput,
} from './components/title-description-input'
import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
import RetryOnPanel from './components/retry/retry-on-panel'
import { useResizePanel } from './hooks/use-resize-panel'
import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import {
WorkflowHistoryEvent,
useAvailableBlocks,
@ -38,6 +40,7 @@ import {
import {
canRunBySingle,
hasErrorHandleNode,
hasRetryNode,
} from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip'
import type { Node } from '@/app/components/workflow/types'
@ -168,6 +171,15 @@ const BasePanel: FC<BasePanelProps> = ({
<div>
{cloneElement(children, { id, data })}
</div>
<Split />
{
hasRetryNode(data.type) && (
<RetryOnPanel
id={id}
data={data}
/>
)
}
{
hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel

View File

@ -2,7 +2,10 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import { AuthorizationType, BodyType, Method } from './types'
import type { BodyPayload, HttpNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
const nodeDefault: NodeDefault<HttpNodeType> = {
defaultValue: {
@ -24,6 +27,11 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
max_read_timeout: 0,
max_write_timeout: 0,
},
retry_config: {
retry_enabled: true,
max_retries: 3,
retry_interval: 100,
},
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode

View File

@ -1,5 +1,5 @@
import type { FC } from 'react'
import React from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import useConfig from './use-config'
import ApiInput from './components/api-input'
@ -18,6 +18,7 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.http'
@ -60,6 +61,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
// To prevent prompt editor in body not update data.
if (!isDataReady)
return null
@ -181,6 +186,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={[
{
@ -192,7 +198,9 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
{(isShowCurlPanel && !readOnly) && (
@ -207,4 +215,4 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
)
}
export default React.memo(Panel)
export default memo(Panel)

View File

@ -129,9 +129,6 @@ export const getMultipleRetrievalConfig = (
reranking_enable: ((allInternal && allEconomic) || allExternal) ? reranking_enable : true,
}
if (!rerankModelIsValid)
result.reranking_model = undefined
const setDefaultWeights = () => {
result.weights = {
vector_setting: {
@ -198,7 +195,6 @@ export const getMultipleRetrievalConfig = (
setDefaultWeights()
}
}
if (reranking_mode === RerankingModeEnum.RerankingModel && !rerankModelIsValid && shouldSetWeightDefaultValue) {
result.reranking_mode = RerankingModeEnum.WeightedScore
setDefaultWeights()

View File

@ -19,6 +19,7 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c
import ResultPanel from '@/app/components/workflow/run/result-panel'
import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.llm'
@ -69,6 +70,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
runResult,
filterJinjia2InputVar,
} = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
const model = inputs.model
@ -282,12 +287,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
</div>

View File

@ -207,7 +207,7 @@ const InputVarList: FC<Props> = ({
readonly={readOnly}
isShowNodeName
nodeId={nodeId}
value={varInput?.type === VarKindType.constant ? (varInput?.value || '') : (varInput?.value || [])}
value={varInput?.type === VarKindType.constant ? (varInput?.value ?? '') : (varInput?.value ?? [])}
onChange={handleNotMixedTypeChange(variable)}
onOpen={handleOpen(index)}
defaultVarKindType={varInput?.type || (isNumber ? VarKindType.constant : VarKindType.variable)}

View File

@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
import { useToolIcon } from '@/app/components/workflow/hooks'
const i18nPrefix = 'workflow.nodes.tool'
@ -48,6 +50,11 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleStop,
runResult,
} = useConfig(id, data)
const toolIcon = useToolIcon(data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'>
@ -143,12 +150,16 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
{isShowSingleRun && (
<BeforeRunForm
nodeName={inputs.title}
nodeType={inputs.type}
toolIcon={toolIcon}
onHide={hideSingleRun}
forms={singleRunForms}
runningStatus={runningStatus}
onRun={handleRun}
onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />}
retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/>
)}
</div>

View File

@ -78,9 +78,9 @@ const AddBlock = ({
title={t('workflow.common.addBlock')}
>
<div className={cn(
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
open && '!bg-black/5',
'flex items-center justify-center w-8 h-8 rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
open && 'bg-state-accent-active text-text-accent',
)}>
<RiAddCircleFill className='w-4 h-4' />
</div>

View File

@ -18,6 +18,7 @@ import {
ControlMode,
} from '../types'
import { useStore } from '../store'
import Divider from '../../base/divider'
import AddBlock from './add-block'
import TipPopup from './tip-popup'
import { useOperator } from './hooks'
@ -43,26 +44,26 @@ const Control = () => {
}
return (
<div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'>
<div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-lg text-text-tertiary'>
<AddBlock />
<TipPopup title={t('workflow.nodes.note.addNote')}>
<div
className={cn(
'flex items-center justify-center ml-[1px] w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
'flex items-center justify-center ml-[1px] w-8 h-8 rounded-lg hover:bg-state-base-hover hover:text-text-secondary cursor-pointer',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={addNote}
>
<RiStickyNoteAddLine className='w-4 h-4' />
</div>
</TipPopup>
<div className='mx-[3px] w-[1px] h-3.5 bg-gray-200'></div>
<Divider type='vertical' className='h-3.5 mx-0.5' />
<TipPopup title={t('workflow.common.pointerMode')} shortcuts={['v']}>
<div
className={cn(
'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer',
controlMode === ControlMode.Pointer ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
controlMode === ControlMode.Pointer ? 'bg-state-accent-active text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={handleModePointer}
>
@ -73,20 +74,20 @@ const Control = () => {
<div
className={cn(
'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer',
controlMode === ControlMode.Hand ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
controlMode === ControlMode.Hand ? 'bg-state-accent-active text-text-accent' : 'hover:bg-state-base-hover hover:text-text-secondary',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={handleModeHand}
>
<RiHand className='w-4 h-4' />
</div>
</TipPopup>
<div className='mx-[3px] w-[1px] h-3.5 bg-gray-200'></div>
<Divider type='vertical' className='h-3.5 mx-0.5' />
<TipPopup title={t('workflow.panel.organizeBlocks')} shortcuts={['ctrl', 'o']}>
<div
className={cn(
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-state-base-hover hover:text-text-secondary cursor-pointer',
`${nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled'}`,
)}
onClick={handleLayout}
>

View File

@ -17,7 +17,9 @@ const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
width: 102,
height: 72,
}}
className='!absolute !left-4 !bottom-14 z-[9] !m-0 !w-[102px] !h-[72px] !border-[0.5px] !border-black/8 !rounded-lg !shadow-lg'
maskColor='var(--color-shadow-shadow-5)'
className='!absolute !left-4 !bottom-14 z-[9] !m-0 !w-[102px] !h-[72px] !border-[0.5px] !border-divider-subtle
!rounded-lg !shadow-md !shadow-shadow-shadow-5 !bg-workflow-minimap-bg'
/>
<div className='flex items-center mt-1 gap-2 absolute left-4 bottom-4 z-[9]'>
<ZoomInOut />

View File

@ -15,12 +15,12 @@ const TipPopup = ({
return (
<Tooltip
offset={4}
popupClassName='!p-0 !bg-gray-25'
popupClassName='p-0 bg-transparent'
popupContent={
<div className='flex items-center gap-1 px-2 h-6 text-xs font-medium text-gray-700 rounded-lg border-[0.5px] border-black/5'>
{title}
<div className='flex items-center gap-1 p-1.5 backdrop-blur-[5px] shadow-lg rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg'>
<span className='system-xs-medium text-text-secondary'>{title}</span>
{
shortcuts && <ShortcutsName keys={shortcuts} className='!text-[11px]' />
shortcuts && <ShortcutsName keys={shortcuts} />
}
</div>
}

View File

@ -18,10 +18,9 @@ import {
useNodesSyncDraft,
useWorkflowReadOnly,
} from '../hooks'
import {
getKeyboardKeyNameBySystem,
} from '../utils'
import ShortcutsName from '../shortcuts-name'
import Divider from '../../base/divider'
import TipPopup from './tip-popup'
import cn from '@/utils/classnames'
import {
@ -132,53 +131,54 @@ const ZoomInOut: FC = () => {
>
<PortalToFollowElemTrigger asChild onClick={handleTrigger}>
<div className={`
p-0.5 h-9 cursor-pointer text-[13px] text-gray-500 font-medium rounded-lg bg-white shadow-lg border-[0.5px] border-gray-100
p-0.5 h-9 cursor-pointer text-[13px] backdrop-blur-[5px] rounded-lg
bg-components-actionbar-bg shadow-lg border-[0.5px] border-components-actionbar-border
hover:bg-state-base-hover
${workflowReadOnly && '!cursor-not-allowed opacity-50'}
`}>
<div className={cn(
'flex items-center justify-between w-[98px] h-8 hover:bg-gray-50 rounded-lg',
open && 'bg-gray-50',
'flex items-center justify-between w-[98px] h-8 rounded-lg',
)}>
<TipPopup
title={t('workflow.operator.zoomOut')}
shortcuts={['ctrl', '-']}
>
<div
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={(e) => {
e.stopPropagation()
zoomOut()
}}
>
<RiZoomOutLine className='w-4 h-4' />
<RiZoomOutLine className='w-4 h-4 text-text-tertiary hover:text-text-secondary' />
</div>
</TipPopup>
<div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
<div className={cn('w-[34px] system-sm-medium text-text-tertiary hover:text-text-secondary')}>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
<TipPopup
title={t('workflow.operator.zoomIn')}
shortcuts={['ctrl', '+']}
>
<div
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={(e) => {
e.stopPropagation()
zoomIn()
}}
>
<RiZoomInLine className='w-4 h-4' />
<RiZoomInLine className='w-4 h-4 text-text-tertiary hover:text-text-secondary' />
</div>
</TipPopup>
</div>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='w-[145px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'>
<div className='w-[145px] backdrop-blur-[5px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
{
ZOOM_IN_OUT_OPTIONS.map((options, i) => (
<Fragment key={i}>
{
i !== 0 && (
<div className='h-[1px] bg-gray-100' />
<Divider className='m-0' />
)
}
<div className='p-1'>
@ -186,25 +186,27 @@ const ZoomInOut: FC = () => {
options.map(option => (
<div
key={option.key}
className='flex items-center justify-between px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700'
className='flex items-center justify-between space-x-1 py-1.5 pl-3 pr-2 h-8 rounded-lg hover:bg-state-base-hover cursor-pointer system-md-regular text-text-secondary'
onClick={() => handleZoom(option.key)}
>
{option.text}
{
option.key === ZoomType.zoomToFit && (
<ShortcutsName keys={[`${getKeyboardKeyNameBySystem('ctrl')}`, '1']} />
)
}
{
option.key === ZoomType.zoomTo50 && (
<ShortcutsName keys={['shift', '5']} />
)
}
{
option.key === ZoomType.zoomTo100 && (
<ShortcutsName keys={['shift', '1']} />
)
}
<span>{option.text}</span>
<div className='flex items-center space-x-0.5'>
{
option.key === ZoomType.zoomToFit && (
<ShortcutsName keys={['ctrl', '1']} />
)
}
{
option.key === ZoomType.zoomTo50 && (
<ShortcutsName keys={['shift', '5']} />
)
}
{
option.key === ZoomType.zoomTo100 && (
<ShortcutsName keys={['shift', '1']} />
)
}
</div>
</div>
))
}

View File

@ -5,6 +5,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { useClickAway } from 'ahooks'
import Divider from '../base/divider'
import ShortcutsName from './shortcuts-name'
import { useStore } from './store'
import {
@ -41,7 +42,7 @@ const PanelContextmenu = () => {
const renderTrigger = () => {
return (
<div
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
>
{t('workflow.common.addBlock')}
</div>
@ -53,7 +54,7 @@ const PanelContextmenu = () => {
return (
<div
className='absolute w-[200px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xl z-[9]'
className='absolute w-[200px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg z-[9]'
style={{
left: panelMenu.left,
top: panelMenu.top,
@ -69,7 +70,7 @@ const PanelContextmenu = () => {
}}
/>
<div
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={(e) => {
e.stopPropagation()
handleAddNote()
@ -79,7 +80,7 @@ const PanelContextmenu = () => {
{t('workflow.nodes.note.addNote')}
</div>
<div
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => {
handleStartWorkflowRun()
handlePaneContextmenuCancel()
@ -89,12 +90,12 @@ const PanelContextmenu = () => {
<ShortcutsName keys={['alt', 'r']} />
</div>
</div>
<div className='h-[1px] bg-gray-100'></div>
<Divider className='m-0' />
<div className='p-1'>
<div
className={cn(
'flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer',
!clipboardElements.length ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50',
'flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer',
!clipboardElements.length ? 'opacity-50 cursor-not-allowed' : 'hover:bg-state-base-hover',
)}
onClick={() => {
if (clipboardElements.length) {
@ -107,16 +108,16 @@ const PanelContextmenu = () => {
<ShortcutsName keys={['ctrl', 'v']} />
</div>
</div>
<div className='h-[1px] bg-gray-100'></div>
<Divider className='m-0' />
<div className='p-1'>
<div
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => exportCheck()}
>
{t('app.export')}
</div>
<div
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
className='flex items-center justify-between px-3 h-8 text-sm text-text-secondary rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => setShowImportDSLModal(true)}
>
{t('workflow.common.importDSL')}

View File

@ -27,6 +27,7 @@ import {
getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { NodeTracing } from '@/types/workflow'
type GetAbortController = (abortController: AbortController) => void
interface SendCallback {
@ -381,6 +382,28 @@ export const useChat = (
}
}))
},
onNodeRetry: ({ data }) => {
if (data.iteration_id)
return
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
if (!item.execution_metadata?.parallel_id)
return item.node_id === data.node_id
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id)
})
if (responseItem.workflowProcess!.tracing[currentIndex].retryDetail)
responseItem.workflowProcess!.tracing[currentIndex].retryDetail?.push(data as NodeTracing)
else
responseItem.workflowProcess!.tracing[currentIndex].retryDetail = [data as NodeTracing]
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeFinished: ({ data }) => {
if (data.iteration_id)
return
@ -394,6 +417,9 @@ export const useChat = (
...(responseItem.workflowProcess!.tracing[currentIndex]?.extras
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
: {}),
...(responseItem.workflowProcess!.tracing[currentIndex]?.retryDetail
? { retryDetail: responseItem.workflowProcess!.tracing[currentIndex].retryDetail }
: {}),
...data,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {

View File

@ -25,6 +25,7 @@ import {
import { SimpleBtn } from '../../app/text-generate/item'
import Toast from '../../base/toast'
import IterationResultPanel from '../run/iteration-result-panel'
import RetryResultPanel from '../run/retry-result-panel'
import InputsPanel from './inputs-panel'
import cn from '@/utils/classnames'
import Loading from '@/app/components/base/loading'
@ -53,11 +54,16 @@ const WorkflowPreview = () => {
}, [workflowRunningData])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)
const [isShowRetryDetail, {
setTrue: doShowRetryDetail,
setFalse: doHideRetryDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => {
setIterDurationMap(iterationDurationMap)
@ -65,6 +71,11 @@ const WorkflowPreview = () => {
doShowIterationDetail()
}, [doShowIterationDetail])
const handleRetryDetail = useCallback((detail: NodeTracing[]) => {
setRetryRunResult(detail)
doShowRetryDetail()
}, [doShowRetryDetail])
if (isShowIterationDetail) {
return (
<div className={`
@ -201,11 +212,12 @@ const WorkflowPreview = () => {
<Loading />
</div>
)}
{currentTab === 'TRACING' && (
{currentTab === 'TRACING' && !isShowRetryDetail && (
<TracingPanel
className='bg-background-section-burn'
list={workflowRunningData?.tracing || []}
onShowIterationDetail={handleShowIterationDetail}
onShowRetryDetail={handleRetryDetail}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
@ -213,7 +225,14 @@ const WorkflowPreview = () => {
<Loading />
</div>
)}
{
currentTab === 'TRACING' && isShowRetryDetail && (
<RetryResultPanel
list={retryRunResult}
onBack={doHideRetryDetail}
/>
)
}
</div>
</>
)}

View File

@ -9,6 +9,7 @@ import OutputPanel from './output-panel'
import ResultPanel from './result-panel'
import TracingPanel from './tracing-panel'
import IterationResultPanel from './iteration-result-panel'
import RetryResultPanel from './retry-result-panel'
import cn from '@/utils/classnames'
import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
@ -107,6 +108,18 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
const processNonIterationNode = (item: NodeTracing) => {
const { execution_metadata } = item
if (!execution_metadata?.iteration_id) {
if (item.status === 'retry') {
const retryNode = result.find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
return
}
result.push(item)
return
}
@ -181,10 +194,15 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
const [isShowIterationDetail, {
setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail,
}] = useBoolean(false)
const [isShowRetryDetail, {
setTrue: doShowRetryDetail,
setFalse: doHideRetryDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
setIterationRunResult(detail)
@ -192,6 +210,11 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
setIterDurationMap(iterDurationMap)
}, [doShowIterationDetail, setIterationRunResult, setIterDurationMap])
const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => {
setRetryRunResult(detail)
doShowRetryDetail()
}, [doShowRetryDetail, setRetryRunResult])
if (isShowIterationDetail) {
return (
<div className='grow relative flex flex-col'>
@ -261,13 +284,22 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
exceptionCounts={runDetail.exceptions_count}
/>
)}
{!loading && currentTab === 'TRACING' && (
{!loading && currentTab === 'TRACING' && !isShowRetryDetail && (
<TracingPanel
className='bg-background-section-burn'
list={list}
onShowIterationDetail={handleShowIterationDetail}
onShowRetryDetail={handleShowRetryDetail}
/>
)}
{
!loading && currentTab === 'TRACING' && isShowRetryDetail && (
<RetryResultPanel
list={retryRunResult}
onBack={doHideRetryDetail}
/>
)
}
</div>
</div>
)

View File

@ -8,6 +8,7 @@ import {
RiCheckboxCircleFill,
RiErrorWarningLine,
RiLoader2Line,
RiRestartFill,
} from '@remixicon/react'
import BlockIcon from '../block-icon'
import { BlockEnum } from '../types'
@ -20,6 +21,7 @@ import Button from '@/app/components/base/button'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import { hasRetryNode } from '@/app/components/workflow/utils'
type Props = {
className?: string
@ -28,8 +30,10 @@ type Props = {
hideInfo?: boolean
hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
notShowIterationNav?: boolean
justShowIterationNavArrow?: boolean
justShowRetryNavArrow?: boolean
}
const NodePanel: FC<Props> = ({
@ -39,6 +43,7 @@ const NodePanel: FC<Props> = ({
hideInfo = false,
hideProcessDetail,
onShowIterationDetail,
onShowRetryDetail,
notShowIterationNav,
justShowIterationNavArrow,
}) => {
@ -88,11 +93,17 @@ const NodePanel: FC<Props> = ({
}, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
}
const handleOnShowRetryDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowRetryDetail?.(nodeInfo.retryDetail || [])
}
return (
<div className={cn('px-2 py-1', className)}>
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'>
@ -169,6 +180,19 @@ const NodePanel: FC<Props> = ({
<Split className='mt-2' />
</div>
)}
{isRetryNode && (
<Button
className='flex items-center justify-between mb-1 w-full'
variant='tertiary'
onClick={handleOnShowRetryDetail}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: nodeInfo.retryDetail?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
)}
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
{(nodeInfo.status === 'stopped') && (
<StatusContainer status='stopped'>

View File

@ -1,11 +1,17 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiRestartFill,
} from '@remixicon/react'
import StatusPanel from './status'
import MetaData from './meta'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import type { NodeTracing } from '@/types/workflow'
import Button from '@/app/components/base/button'
interface ResultPanelProps {
inputs?: string
@ -22,6 +28,8 @@ interface ResultPanelProps {
showSteps?: boolean
exceptionCounts?: number
execution_metadata?: any
retry_events?: NodeTracing[]
onShowRetryDetail?: (retries: NodeTracing[]) => void
}
const ResultPanel: FC<ResultPanelProps> = ({
@ -38,8 +46,11 @@ const ResultPanel: FC<ResultPanelProps> = ({
showSteps,
exceptionCounts,
execution_metadata,
retry_events,
onShowRetryDetail,
}) => {
const { t } = useTranslation()
return (
<div className='bg-components-panel-bg py-2'>
<div className='px-4 py-2'>
@ -51,6 +62,23 @@ const ResultPanel: FC<ResultPanelProps> = ({
exceptionCounts={exceptionCounts}
/>
</div>
{
retry_events?.length && onShowRetryDetail && (
<div className='px-4'>
<Button
className='flex items-center justify-between w-full'
variant='tertiary'
onClick={() => onShowRetryDetail(retry_events)}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: retry_events?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
</div>
)
}
<div className='px-4 py-2 flex flex-col gap-2'>
<CodeEditor
readOnly

View File

@ -0,0 +1,46 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import TracingPanel from './tracing-panel'
import type { NodeTracing } from '@/types/workflow'
type Props = {
list: NodeTracing[]
onBack: () => void
}
const RetryResultPanel: FC<Props> = ({
list,
onBack,
}) => {
const { t } = useTranslation()
return (
<div>
<div
className='flex items-center px-4 h-8 text-text-accent-secondary bg-components-panel-bg system-sm-medium cursor-pointer'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 w-4 h-4' />
{t('workflow.singleRun.back')}
</div>
<TracingPanel
list={list.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
}))}
className='bg-background-section-burn'
/>
</div >
)
}
export default memo(RetryResultPanel)

View File

@ -21,6 +21,7 @@ import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
type TracingPanelProps = {
list: NodeTracing[]
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
className?: string
hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean
@ -160,6 +161,7 @@ function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): Tracing
const TracingPanel: FC<TracingPanelProps> = ({
list,
onShowIterationDetail,
onShowRetryDetail,
className,
hideNodeInfo = false,
hideNodeProcessDetail = false,
@ -251,7 +253,9 @@ const TracingPanel: FC<TracingPanelProps> = ({
<NodePanel
nodeInfo={node.data!}
onShowIterationDetail={onShowIterationDetail}
onShowRetryDetail={onShowRetryDetail}
justShowIterationNavArrow={true}
justShowRetryNavArrow={true}
hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail}
/>

View File

@ -12,14 +12,14 @@ const ShortcutsName = ({
}: ShortcutsNameProps) => {
return (
<div className={cn(
'flex items-center gap-0.5 h-4 text-xs text-gray-400',
'flex items-center gap-0.5',
className,
)}>
{
keys.map(key => (
<div
key={key}
className='capitalize'
className='w-4 h-4 flex items-center justify-center bg-components-kbd-bg-gray rounded-[4px] system-kbd capitalize'
>
{getKeyboardKeyNameBySystem(key)}
</div>

View File

@ -19,4 +19,6 @@
#workflow-container .react-flow__node-custom-note {
z-index: -1000 !important;
}
}
#workflow-container .react-flow {}

View File

@ -13,6 +13,7 @@ import type {
DefaultValueForm,
ErrorHandleTypeEnum,
} from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
export enum BlockEnum {
Start = 'start',
@ -68,6 +69,7 @@ export type CommonNodeType<T = {}> = {
_iterationIndex?: number
_inParallelHovering?: boolean
_waitingRun?: boolean
_retryIndex?: number
isInIteration?: boolean
iteration_id?: string
selected?: boolean
@ -77,6 +79,7 @@ export type CommonNodeType<T = {}> = {
width?: number
height?: number
error_strategy?: ErrorHandleTypeEnum
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
@ -293,6 +296,7 @@ export enum NodeRunningStatus {
Succeeded = 'succeeded',
Failed = 'failed',
Exception = 'exception',
Retry = 'retry',
}
export type OnNodeAdd = (

View File

@ -26,6 +26,8 @@ import {
} from './types'
import {
CUSTOM_NODE,
DEFAULT_RETRY_INTERVAL,
DEFAULT_RETRY_MAX,
ITERATION_CHILDREN_Z_INDEX,
ITERATION_NODE_Z_INDEX,
NODE_WIDTH_X_OFFSET,
@ -292,6 +294,13 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
if (node.data.type === BlockEnum.ParameterExtractor)
(node as any).data.model.provider = correctProvider((node as any).data.model.provider)
if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
node.data.retry_config = {
retry_enabled: true,
max_retries: DEFAULT_RETRY_MAX,
retry_interval: DEFAULT_RETRY_INTERVAL,
}
}
return node
})
@ -563,6 +572,7 @@ export const isMac = () => {
const specialKeysNameMap: Record<string, string | undefined> = {
ctrl: '⌘',
alt: '⌥',
shift: '⇧',
}
export const getKeyboardKeyNameBySystem = (key: string) => {
@ -810,3 +820,7 @@ export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
return false
}
export const hasRetryNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}

View File

@ -34,7 +34,7 @@ const LocaleLayout = ({
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
</head>
<body
className="h-full select-auto"
className="h-full select-auto color-scheme"
data-api-prefix={process.env.NEXT_PUBLIC_API_PREFIX}
data-pubic-api-prefix={process.env.NEXT_PUBLIC_PUBLIC_API_PREFIX}
data-marketplace-api-prefix={process.env.NEXT_PUBLIC_MARKETPLACE_API_PREFIX}

View File

@ -12,12 +12,13 @@ import I18NContext from '@/context/i18n'
type MailAndPasswordAuthProps = {
isInvite: boolean
isEmailSetup: boolean
allowRegistration: boolean
}
const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function MailAndPasswordAuth({ isInvite, allowRegistration }: MailAndPasswordAuthProps) {
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration }: MailAndPasswordAuthProps) {
const { t } = useTranslation()
const { locale } = useContext(I18NContext)
const router = useRouter()
@ -124,7 +125,12 @@ export default function MailAndPasswordAuth({ isInvite, allowRegistration }: Mai
<div className='mb-3'>
<label htmlFor="password" className="my-2 flex items-center justify-between">
<span className='system-md-semibold text-text-secondary'>{t('login.password')}</span>
<Link href={`/reset-password?${searchParams.toString()}`} className='system-xs-regular text-components-button-secondary-accent-text'>
<Link
href={`/reset-password?${searchParams.toString()}`}
className={`system-xs-regular ${isEmailSetup ? 'text-components-button-secondary-accent-text' : 'text-components-button-secondary-accent-text-disabled pointer-events-none'}`}
tabIndex={isEmailSetup ? 0 : -1}
aria-disabled={!isEmailSetup}
>
{t('login.forget')}
</Link>
</label>

View File

@ -163,7 +163,7 @@ const NormalForm = () => {
</div>}
</>}
{systemFeatures.enable_email_password_login && authType === 'password' && <>
<MailAndPasswordAuth isInvite={isInviteLink} allowRegistration={systemFeatures.is_allow_register} />
<MailAndPasswordAuth isInvite={isInviteLink} isEmailSetup={systemFeatures.is_email_setup} allowRegistration={systemFeatures.is_allow_register} />
{systemFeatures.enable_email_code_login && <div className='cursor-pointer py-1 text-center' onClick={() => { updateAuthType('code') }}>
<span className='system-xs-medium text-components-button-secondary-accent-text'>{t('login.useVerificationCode')}</span>
</div>}

View File

@ -7,6 +7,14 @@
@import "../../themes/manual-light.css";
@import "../../themes/manual-dark.css";
html {
color-scheme: light;
}
html[data-theme='dark'] {
color-scheme: dark;
}
html[data-changing-theme] * {
transition: none !important;
}