This commit is contained in:
Joel
2025-06-23 14:54:30 +08:00
1158 changed files with 37363 additions and 13240 deletions

View File

@ -3,9 +3,11 @@ import {
RiExternalLinkLine,
RiPuzzle2Line,
} from '@remixicon/react'
import { useDocLink } from '@/context/i18n'
const Empty = () => {
const { t } = useTranslation()
const docLink = useDocLink()
return (
<div className='mb-2 rounded-xl bg-background-section p-6'>
@ -15,7 +17,7 @@ const Empty = () => {
<div className='system-sm-medium mb-1 text-text-secondary'>{t('common.apiBasedExtension.title')}</div>
<a
className='system-xs-regular flex items-center text-text-accent'
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
>
{t('common.apiBasedExtension.link')}

View File

@ -1,6 +1,7 @@
import type { FC } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
@ -29,6 +30,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
onSave,
}) => {
const { t } = useTranslation()
const docLink = useDocLink()
const [localeData, setLocaleData] = useState(data)
const [loading, setLoading] = useState(false)
const { notify } = useToastContext()
@ -100,7 +102,7 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({
<div className='flex h-9 items-center justify-between text-sm font-medium text-text-primary'>
{t('common.apiBasedExtension.modal.apiEndpoint.title')}
<a
href={t('common.apiBasedExtension.linkUrl') || '/'}
href={docLink('/guides/extension/api-based-extension/README')}
target='_blank' rel='noopener noreferrer'
className='group flex items-center text-xs font-normal text-text-accent'
>

View File

@ -25,6 +25,7 @@ import { LanguagesSupported } from '@/i18n/language'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import { RiPencilLine } from '@remixicon/react'
import { useGlobalPublicStore } from '@/context/global-public-context'
dayjs.extend(relativeTime)
const MembersPage = () => {
@ -38,7 +39,7 @@ const MembersPage = () => {
}
const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { data, mutate } = useSWR(
{
url: '/workspaces/current/members',
@ -46,6 +47,7 @@ const MembersPage = () => {
},
fetchMembers,
)
const { systemFeatures } = useGlobalPublicStore()
const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
const [invitedModalVisible, setInvitedModalVisible] = useState(false)

View File

@ -1,5 +1,5 @@
'use client'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useContext } from 'use-context-selector'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
@ -18,6 +18,7 @@ import I18n from '@/context/i18n'
import 'react-multi-email/dist/style.css'
import { noop } from 'lodash-es'
import { useProviderContextSelector } from '@/context/provider-context'
type IInviteModalProps = {
isEmailSetup: boolean
onCancel: () => void
@ -30,13 +31,27 @@ const InviteModal = ({
onSend,
}: IInviteModalProps) => {
const { t } = useTranslation()
const licenseLimit = useProviderContextSelector(s => s.licenseLimit)
const refreshLicenseLimit = useProviderContextSelector(s => s.refreshLicenseLimit)
const [emails, setEmails] = useState<string[]>([])
const { notify } = useContext(ToastContext)
const [isLimited, setIsLimited] = useState(false)
const [isLimitExceeded, setIsLimitExceeded] = useState(false)
const [usedSize, setUsedSize] = useState(licenseLimit.workspace_members.size ?? 0)
useEffect(() => {
const limited = licenseLimit.workspace_members.limit > 0
const used = emails.length + licenseLimit.workspace_members.size
setIsLimited(limited)
setUsedSize(used)
setIsLimitExceeded(limited && (used > licenseLimit.workspace_members.limit))
}, [licenseLimit, emails])
const { locale } = useContext(I18n)
const [role, setRole] = useState<string>('normal')
const handleSend = useCallback(async () => {
if (isLimitExceeded)
return
if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
try {
const { result, invitation_results } = await inviteMember({
@ -45,6 +60,7 @@ const InviteModal = ({
})
if (result === 'success') {
refreshLicenseLimit()
onCancel()
onSend(invitation_results)
}
@ -54,7 +70,7 @@ const InviteModal = ({
else {
notify({ type: 'error', message: t('common.members.emailInvalid') })
}
}, [role, emails, notify, onCancel, onSend, t])
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t])
return (
<div className={cn(s.wrap)}>
@ -82,7 +98,7 @@ const InviteModal = ({
<div>
<div className='mb-2 text-sm font-medium text-text-primary'>{t('common.members.email')}</div>
<div className='mb-8 flex h-36 items-stretch'>
<div className='mb-8 flex h-36 flex-col items-stretch'>
<ReactMultiEmail
className={cn('w-full border-components-input-border-active !bg-components-input-bg-normal px-3 pt-2 outline-none',
'appearance-none overflow-y-auto rounded-lg text-sm !text-text-primary',
@ -101,6 +117,14 @@ const InviteModal = ({
}
placeholder={t('common.members.emailPlaceholder') || ''}
/>
<div className={
cn('system-xs-regular flex items-center justify-end text-text-tertiary',
(isLimited && usedSize > licenseLimit.workspace_members.limit) ? 'text-text-destructive' : '')}
>
<span>{usedSize}</span>
<span>/</span>
<span>{isLimited ? licenseLimit.workspace_members.limit : t('common.license.unlimited')}</span>
</div>
</div>
<div className='mb-6'>
<RoleSelector value={role} onChange={setRole} />
@ -109,7 +133,7 @@ const InviteModal = ({
tabIndex={0}
className='w-full'
onClick={handleSend}
disabled={!emails.length}
disabled={!emails.length || isLimitExceeded}
variant='primary'
>
{t('common.members.sendInvite')}

View File

@ -1,6 +1,5 @@
'use client'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { basePath } from '@/utils/var'
import { t } from 'i18next'
import copy from 'copy-to-clipboard'
import s from './index.module.css'
@ -19,7 +18,8 @@ const InvitationLink = ({
const selector = useRef(`invite-link-${randomString(4)}`)
const copyHandle = useCallback(() => {
copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${basePath}${value.url}`)
// No prefix is needed here because the backend has already processed it
copy(`${!value.url.startsWith('http') ? window.location.origin : ''}${value.url}`)
setIsCopied(true)
}, [value])
@ -42,7 +42,7 @@ const InvitationLink = ({
<Tooltip
popupContent={isCopied ? `${t('appApi.copied')}` : `${t('appApi.copy')}`}
>
<div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2' onClick={copyHandle}>{basePath + value.url}</div>
<div className='r-0 absolute left-0 top-0 w-full cursor-pointer truncate pl-2 pr-2 text-text-primary' onClick={copyHandle}>{value.url}</div>
</Tooltip>
</div>
<div className="h-4 shrink-0 border bg-divider-regular" />

View File

@ -23,7 +23,7 @@ import {
import InstallFromMarketplace from './install-from-marketplace'
import { useProviderContext } from '@/context/provider-context'
import cn from '@/utils/classnames'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
searchText: string
@ -40,7 +40,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
const configuredProviders: ModelProvider[] = []

View File

@ -1,4 +1,5 @@
import { useCallback, useState } from 'react'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
@ -16,9 +17,9 @@ import Loading from '@/app/components/base/loading'
import ProviderCard from '@/app/components/plugins/provider-card'
import List from '@/app/components/plugins/marketplace/list'
import type { Plugin } from '@/app/components/plugins/types'
import { MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
import { getLocaleOnClient } from '@/i18n'
import { getMarketplaceUrl } from '@/utils/var'
type InstallFromMarketplaceProps = {
providers: ModelProvider[]
@ -29,6 +30,7 @@ const InstallFromMarketplace = ({
searchText,
}: InstallFromMarketplaceProps) => {
const { t } = useTranslation()
const { theme } = useTheme()
const [collapse, setCollapse] = useState(false)
const locale = getLocaleOnClient()
const {
@ -53,7 +55,7 @@ const InstallFromMarketplace = ({
</div>
<div className='mb-2 flex items-center pt-2'>
<span className='system-sm-regular pr-1 text-text-tertiary'>{t('common.modelProvider.discoverMore')}</span>
<Link target="_blank" href={`${MARKETPLACE_URL_PREFIX}`} className='system-sm-medium inline-flex items-center text-text-accent'>
<Link target="_blank" href={getMarketplaceUrl('', { theme })} className='system-sm-medium inline-flex items-center text-text-accent'>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='h-4 w-4' />
</Link>

View File

@ -5,7 +5,7 @@ import type {
} from '../declarations'
import { useLanguage } from '../hooks'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { OpenaiBlue, OpenaiViolet } from '@/app/components/base/icons/src/public/llm'
import { OpenaiBlue, OpenaiTeal, OpenaiViolet, OpenaiYellow } from '@/app/components/base/icons/src/public/llm'
import cn from '@/utils/classnames'
import { renderI18nObject } from '@/i18n'
@ -22,6 +22,10 @@ const ModelIcon: FC<ModelIconProps> = ({
isDeprecated = false,
}) => {
const language = useLanguage()
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('o'))
return <div className='flex items-center justify-center'><OpenaiYellow className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4.1'))
return <div className='flex items-center justify-center'><OpenaiTeal className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.includes('gpt-4o'))
return <div className='flex items-center justify-center'><OpenaiBlue className={cn('h-5 w-5', className)} /></div>
if (provider?.provider && ['openai', 'langgenius/openai/openai'].includes(provider.provider) && modelName?.startsWith('gpt-4'))

View File

@ -1,6 +1,5 @@
import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import { basePath } from '@/utils/var'
import { useLanguage } from '../hooks'
import { Openai } from '@/app/components/base/icons/src/vender/other'
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
@ -41,7 +40,7 @@ const ProviderIcon: FC<ProviderIconProps> = ({
<div className={cn('inline-flex items-center gap-2', className)}>
<img
alt='provider-icon'
src={basePath + renderI18nObject(provider.icon_small, language)}
src={renderI18nObject(provider.icon_small, language)}
className='h-6 w-6'
/>
<div className='system-md-semibold text-text-primary'>