Compare commits
14 Commits
main
...
feat/edu-v
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5fa8709989 | ||
![]() |
ce180706d0 | ||
![]() |
f1efaabf97 | ||
![]() |
358b70821a | ||
![]() |
05e25096ba | ||
![]() |
9f673dab0d | ||
![]() |
4a347b92ab | ||
![]() |
08c00ff71d | ||
![]() |
8b53254de5 | ||
![]() |
f17a76a00e | ||
![]() |
bed9407045 | ||
![]() |
233dfd3c79 | ||
![]() |
b2780f7c4b | ||
![]() |
ed83f5f1ca |
@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
useRouter,
|
||||
} from 'next/navigation'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
@ -26,6 +28,8 @@ import { useStore as useTagStore } from '@/app/components/base/tag-management/st
|
||||
import TagManagementModal from '@/app/components/base/tag-management'
|
||||
import TagFilter from '@/app/components/base/tag-management/filter'
|
||||
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/components/constants'
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
@ -132,6 +136,14 @@ const Apps = () => {
|
||||
setQuery(prev => ({ ...prev, isCreatedByMe: newValue }))
|
||||
}, [isCreatedByMe, setQuery])
|
||||
|
||||
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
useEffect(() => {
|
||||
if (educationVerifying === 'yes')
|
||||
setShowAccountSettingModal({ payload: 'billing' })
|
||||
}, [setShowAccountSettingModal, educationVerifying])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
|
||||
|
@ -1,15 +1,28 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import style from '../list.module.css'
|
||||
import Apps from './Apps'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
import {
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
} from '@/app/education-apply/components/constants'
|
||||
|
||||
const AppList = () => {
|
||||
const { t } = useTranslation()
|
||||
const searchParams = useSearchParams()
|
||||
const educationVerifyAction = searchParams.get('action')
|
||||
|
||||
useEffect(() => {
|
||||
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes')
|
||||
}, [educationVerifyAction])
|
||||
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,9 @@
|
||||
'use client'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import DeleteAccount from '../delete-account'
|
||||
import s from './index.module.css'
|
||||
@ -12,10 +14,12 @@ import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { updateUserProfile } from '@/service/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import Input from '@/app/components/base/input'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
|
||||
const titleClassName = `
|
||||
system-sm-semibold text-text-secondary
|
||||
@ -30,6 +34,7 @@ export default function AccountPage() {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useAppContext()
|
||||
const { mutateUserProfile, userProfile, apps } = useAppContext()
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||
const [editName, setEditName] = useState('')
|
||||
@ -135,7 +140,15 @@ export default function AccountPage() {
|
||||
<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'>
|
||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
|
||||
<div className='ml-4'>
|
||||
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
||||
<p className='system-xl-semibold text-text-primary'>
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
|
||||
<RiGraduationCapFill className='w-3 h-3 mr-1' />
|
||||
<span className='system-2xs-medium'>EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</p>
|
||||
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,13 +2,18 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Fragment } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { logout } from '@/service/common'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
|
||||
export interface IAppSelector {
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
@ -16,6 +21,7 @@ export default function AppSelector() {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout({
|
||||
@ -68,7 +74,15 @@ export default function AppSelector() {
|
||||
<div className='p-1'>
|
||||
<div className='flex flex-nowrap items-center px-3 py-2'>
|
||||
<div className='grow'>
|
||||
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
||||
<div className='system-md-medium text-text-primary break-all'>
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
|
||||
<RiGraduationCapFill className='w-3 h-3 mr-1' />
|
||||
<span className='system-2xs-medium'>EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} />
|
||||
|
@ -22,7 +22,7 @@ const Header = () => {
|
||||
<div className='w-[1px] h-4 bg-divider-regular' />
|
||||
<p className='text-text-primary title-3xl-semi-bold'>{t('common.account.account')}</p>
|
||||
</div>
|
||||
<div className='flex items-center flex-shrink-0 gap-3'>
|
||||
<div className='flex items-center shrink-0 gap-3'>
|
||||
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
|
||||
<RiRobot2Line className='w-4 h-4' />
|
||||
<p>{t('common.account.studio')}</p>
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path id="Rectangle 979" d="M0 0H16L9.91493 16.7339C8.76529 19.8955 5.76063 22 2.39658 22H0V0Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 214 B |
@ -0,0 +1,5 @@
|
||||
<svg width="18" height="16" viewBox="0 0 18 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="loop">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M2.02915 5.34506C3.50752 3.88498 5.9006 3.88498 7.37896 5.34506L8.99983 6.94588L10.6207 5.34506C12.0991 3.88499 14.4921 3.88498 15.9705 5.34506C17.454 6.81027 17.454 9.18971 15.9705 10.6549C14.4921 12.115 12.0991 12.115 10.6207 10.655L8.99983 9.05413L7.37896 10.655C5.9006 12.115 3.50753 12.115 2.02916 10.655C0.545627 9.18974 0.545611 6.81028 2.02915 5.34506ZM7.93251 8L6.32492 6.4123C5.4308 5.52924 3.97732 5.52923 3.08319 6.4123C2.19426 7.29026 2.19426 8.70975 3.0832 9.58772C3.97733 10.4708 5.4308 10.4707 6.32492 9.58771C6.32492 9.58772 6.32492 9.58771 6.32492 9.58771L7.93251 8ZM10.0671 8L11.6747 9.5877C11.6747 9.58769 11.6747 9.58771 11.6747 9.5877C12.5688 10.4707 14.0223 10.4707 14.9165 9.58773C15.8054 8.70975 15.8054 7.29024 14.9165 6.41229C14.0223 5.52923 12.5689 5.52924 11.6747 6.4123C11.6747 6.4123 11.6747 6.41229 11.6747 6.4123L10.0671 8Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,27 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "22",
|
||||
"viewBox": "0 0 16 22",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Rectangle 979",
|
||||
"d": "M0 0H16L9.91493 16.7339C8.76529 19.8955 5.76063 22 2.39658 22H0V0Z",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Triangle"
|
||||
}
|
@ -2,7 +2,7 @@
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './LoopStart.json'
|
||||
import data from './Triangle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'LoopStart'
|
||||
Icon.displayName = 'Triangle'
|
||||
|
||||
export default Icon
|
@ -0,0 +1 @@
|
||||
export { default as Triangle } from './Triangle'
|
@ -4,9 +4,9 @@
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "40",
|
||||
"height": "40",
|
||||
"viewBox": "0 0 40 40",
|
||||
"width": "18",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 18 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
@ -15,46 +15,18 @@
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"filter": "url(#filter0_dd_10886_10012)",
|
||||
"style": "transform: scale(2.5) translate(-12px, -8px)"
|
||||
"id": "loop"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"x": "8",
|
||||
"y": "5",
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"rx": "8",
|
||||
"fill": "#06AED4"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"x": "8.25",
|
||||
"y": "5.25",
|
||||
"width": "23.5",
|
||||
"height": "23.5",
|
||||
"rx": "7.75",
|
||||
"stroke": "#101828",
|
||||
"stroke-opacity": "0.04",
|
||||
"stroke-width": "0.5"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M13.0293 14.3451C14.5076 12.885 16.9007 12.885 18.3791 14.3451L19.9999 15.9459L21.6208 14.3451C23.0992 12.885 25.4922 12.885 26.9706 14.3451C28.4541 15.8103 28.4541 18.1897 26.9707 19.6549C25.4923 21.115 23.0992 21.115 21.6208 19.655L19.9999 18.0541L18.3791 19.655C16.9007 21.115 14.5076 21.115 13.0293 19.655C11.5457 18.1897 11.5457 15.8103 13.0293 14.3451ZM18.9326 17L17.325 15.4123C16.4309 14.5292 14.9774 14.5292 14.0833 15.4123C13.1944 16.2903 13.1944 17.7097 14.0833 18.5877C14.9774 19.4708 16.4309 19.4707 17.325 18.5877C17.325 18.5877 17.325 18.5877 17.325 18.5877L18.9326 17ZM21.0673 17L22.6748 18.5877C22.6748 18.5877 22.6748 18.5877 22.6748 18.5877C23.569 19.4707 25.0224 19.4707 25.9166 18.5877C26.8055 17.7098 26.8055 16.2902 25.9166 15.4123C25.0224 14.5292 23.569 14.5292 22.6748 15.4123C22.6748 15.4123 22.6748 15.4123 22.6748 15.4123L21.0673 17Z",
|
||||
"fill": "white"
|
||||
"d": "M2.02915 5.34506C3.50752 3.88498 5.9006 3.88498 7.37896 5.34506L8.99983 6.94588L10.6207 5.34506C12.0991 3.88499 14.4921 3.88498 15.9705 5.34506C17.454 6.81027 17.454 9.18971 15.9705 10.6549C14.4921 12.115 12.0991 12.115 10.6207 10.655L8.99983 9.05413L7.37896 10.655C5.9006 12.115 3.50753 12.115 2.02916 10.655C0.545627 9.18974 0.545611 6.81028 2.02915 5.34506ZM7.93251 8L6.32492 6.4123C5.4308 5.52924 3.97732 5.52923 3.08319 6.4123C2.19426 7.29026 2.19426 8.70975 3.0832 9.58772C3.97733 10.4708 5.4308 10.4707 6.32492 9.58771C6.32492 9.58772 6.32492 9.58771 6.32492 9.58771L7.93251 8ZM10.0671 8L11.6747 9.5877C11.6747 9.58769 11.6747 9.58771 11.6747 9.5877C12.5688 10.4707 14.0223 10.4707 14.9165 9.58773C15.8054 8.70975 15.8054 7.29024 14.9165 6.41229C14.0223 5.52923 12.5689 5.52924 11.6747 6.4123C11.6747 6.4123 11.6747 6.41229 11.6747 6.4123L10.0671 8Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
@ -63,4 +35,4 @@
|
||||
]
|
||||
},
|
||||
"name": "Loop"
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "icons/block-start"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M6.8498 1.72732C6.3379 1.3754 5.6621 1.3754 5.1502 1.72732L2.1502 3.78982C1.74317 4.06965 1.5 4.53193 1.5 5.02588V8.99983C1.5 9.82828 2.17158 10.4998 3 10.4998H4.25C4.52614 10.4998 4.75 10.276 4.75 9.99983V8.24983C4.75 7.55948 5.30965 6.99983 6 6.99983C6.69035 6.99983 7.25 7.55948 7.25 8.24983V9.99983C7.25 10.276 7.47385 10.4998 7.75 10.4998H9C9.82845 10.4998 10.5 9.82828 10.5 8.99983V5.02588C10.5 4.53193 10.2568 4.06965 9.8498 3.78982L6.8498 1.72732Z",
|
||||
"fill": "red"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "LoopStart"
|
||||
}
|
@ -9,12 +9,11 @@ export { default as Http } from './Http'
|
||||
export { default as IfElse } from './IfElse'
|
||||
export { default as IterationStart } from './IterationStart'
|
||||
export { default as Iteration } from './Iteration'
|
||||
export { default as LoopStart } from './LoopStart'
|
||||
export { default as Loop } from './Loop'
|
||||
export { default as Jinja } from './Jinja'
|
||||
export { default as KnowledgeRetrieval } from './KnowledgeRetrieval'
|
||||
export { default as ListFilter } from './ListFilter'
|
||||
export { default as Llm } from './Llm'
|
||||
export { default as Loop } from './Loop'
|
||||
export { default as ParameterExtractor } from './ParameterExtractor'
|
||||
export { default as QuestionClassifier } from './QuestionClassifier'
|
||||
export { default as TemplatingTransform } from './TemplatingTransform'
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
flip,
|
||||
offset,
|
||||
shift,
|
||||
size,
|
||||
useDismiss,
|
||||
useFloating,
|
||||
useFocus,
|
||||
@ -27,6 +28,7 @@ export type PortalToFollowElemOptions = {
|
||||
open?: boolean
|
||||
offset?: number | OffsetOptions
|
||||
onOpenChange?: (open: boolean) => void
|
||||
triggerPopupSameWidth?: boolean
|
||||
}
|
||||
|
||||
export function usePortalToFollowElem({
|
||||
@ -34,6 +36,7 @@ export function usePortalToFollowElem({
|
||||
open,
|
||||
offset: offsetValue = 0,
|
||||
onOpenChange: setControlledOpen,
|
||||
triggerPopupSameWidth,
|
||||
}: PortalToFollowElemOptions = {}) {
|
||||
const setOpen = setControlledOpen
|
||||
|
||||
@ -50,6 +53,12 @@ export function usePortalToFollowElem({
|
||||
padding: 5,
|
||||
}),
|
||||
shift({ padding: 5 }),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
if (triggerPopupSameWidth)
|
||||
elements.floating.style.width = `${rects.reference.width}px`
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import { Plan } from '../type'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Props = {
|
||||
onClick?: () => void
|
||||
isDisplayOnly?: boolean
|
||||
}
|
||||
|
||||
const HeaderBillingBtn: FC<Props> = ({
|
||||
onClick,
|
||||
isDisplayOnly = false,
|
||||
}) => {
|
||||
const { plan, enableBilling, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
type,
|
||||
} = plan
|
||||
|
||||
const name = (() => {
|
||||
if (type === Plan.professional)
|
||||
return 'pro'
|
||||
return type
|
||||
})()
|
||||
const classNames = (() => {
|
||||
if (type === Plan.professional)
|
||||
return `border-[#E0F2FE] ${!isDisplayOnly ? 'hover:border-[#B9E6FE]' : ''} bg-[#E0F2FE] text-[#026AA2]`
|
||||
if (type === Plan.team)
|
||||
return `border-[#E0EAFF] ${!isDisplayOnly ? 'hover:border-[#C7D7FE]' : ''} bg-[#E0EAFF] text-[#3538CD]`
|
||||
return ''
|
||||
})()
|
||||
|
||||
if (!enableBilling || !isFetchedPlan)
|
||||
return null
|
||||
|
||||
if (type === Plan.sandbox)
|
||||
return <UpgradeBtn onClick={isDisplayOnly ? undefined : onClick} isShort />
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDisplayOnly && onClick)
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
classNames,
|
||||
'flex items-center h-[22px] px-2 rounded-md border text-xs font-semibold uppercase',
|
||||
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(HeaderBillingBtn)
|
@ -2,10 +2,12 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
RiBook2Line,
|
||||
RiBox3Line,
|
||||
RiFileEditLine,
|
||||
RiGraduationCapLine,
|
||||
RiGroup3Line,
|
||||
RiGroupLine,
|
||||
RiSquareLine,
|
||||
@ -15,7 +17,12 @@ import VectorSpaceInfo from '../usage-info/vector-space-info'
|
||||
import AppsInfo from '../usage-info/apps-info'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Button from '@/app/components/base/button'
|
||||
import UsageInfo from '@/app/components/billing/usage-info'
|
||||
import VerifyStateModal from '@/app/education-apply/components/verify-state-modal'
|
||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/components/constants'
|
||||
import { useEducationVerify } from '@/service/use-education'
|
||||
|
||||
type Props = {
|
||||
loc: string
|
||||
@ -25,7 +32,9 @@ const PlanComp: FC<Props> = ({
|
||||
loc,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const router = useRouter()
|
||||
const { userProfile } = useAppContext()
|
||||
const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
|
||||
const {
|
||||
type,
|
||||
} = plan
|
||||
@ -35,6 +44,16 @@ const PlanComp: FC<Props> = ({
|
||||
total,
|
||||
} = plan
|
||||
|
||||
const [showModal, setShowModal] = React.useState(false)
|
||||
const { mutateAsync } = useEducationVerify()
|
||||
const handleVerify = () => {
|
||||
mutateAsync().then((res) => {
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
router.push(`/education-apply?token=${res.token}`)
|
||||
}).catch(() => {
|
||||
setShowModal(true)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className='bg-background-section-burn rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off'>
|
||||
<div className='p-6 pb-2'>
|
||||
@ -58,14 +77,22 @@ const PlanComp: FC<Props> = ({
|
||||
</div>
|
||||
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
|
||||
</div>
|
||||
{(plan.type as any) !== SelfHostedPlan.enterprise && (
|
||||
<UpgradeBtn
|
||||
className='shrink-0'
|
||||
isPlain={type === Plan.team}
|
||||
isShort
|
||||
loc={loc}
|
||||
/>
|
||||
)}
|
||||
<div className='shrink-0 flex items-center gap-1'>
|
||||
{enableEducationPlan && !isEducationAccount && (
|
||||
<Button variant='ghost' onClick={handleVerify}>
|
||||
<RiGraduationCapLine className='w-4 h-4 mr-1'/>
|
||||
{t('education.toVerified')}
|
||||
</Button>
|
||||
)}
|
||||
{(plan.type as any) !== SelfHostedPlan.enterprise && (
|
||||
<UpgradeBtn
|
||||
className='shrink-0'
|
||||
isPlain={type === Plan.team}
|
||||
isShort
|
||||
loc={loc}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Plan detail */}
|
||||
@ -92,6 +119,15 @@ const PlanComp: FC<Props> = ({
|
||||
/>
|
||||
|
||||
</div>
|
||||
<VerifyStateModal
|
||||
showLink
|
||||
email={userProfile.email}
|
||||
isShow={showModal}
|
||||
title={t('education.rejectTitle')}
|
||||
content={t('education.rejectContent')}
|
||||
onConfirm={() => setShowModal(false)}
|
||||
onCancel={() => setShowModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -86,6 +86,10 @@ export type CurrentPlanInfoBackend = {
|
||||
can_replace_logo: boolean
|
||||
model_load_balancing_enabled: boolean
|
||||
dataset_operator_enabled: boolean
|
||||
education: {
|
||||
enabled: boolean
|
||||
activated: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type SubscriptionItem = {
|
||||
|
@ -3,7 +3,18 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { RiAccountCircleLine, RiArrowDownSLine, RiArrowRightUpLine, RiBookOpenLine, RiGithubLine, RiInformation2Line, RiLogoutBoxRLine, RiMap2Line, RiSettings3Line, RiStarLine } from '@remixicon/react'
|
||||
import {
|
||||
RiAccountCircleLine,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
RiGithubLine,
|
||||
RiGraduationCapFill,
|
||||
RiInformation2Line,
|
||||
RiLogoutBoxRLine,
|
||||
RiMap2Line,
|
||||
RiSettings3Line,
|
||||
RiStarLine,
|
||||
} from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import Indicator from '../indicator'
|
||||
@ -11,21 +22,19 @@ import AccountAbout from '../account-about'
|
||||
import GithubStar from '../github-star'
|
||||
import Support from './support'
|
||||
import Compliance from './compliance'
|
||||
import classNames from '@/utils/classnames'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import I18n from '@/context/i18n'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { logout } from '@/service/common'
|
||||
import AppContext, { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
export default function AppSelector() {
|
||||
const itemClassName = `
|
||||
flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular
|
||||
rounded-lg hover:bg-state-base-hover cursor-pointer gap-1
|
||||
@ -37,6 +46,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
||||
const handleLogout = async () => {
|
||||
@ -58,20 +68,8 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
{
|
||||
({ open }) => (
|
||||
<>
|
||||
<Menu.Button
|
||||
className={`
|
||||
inline-flex items-center
|
||||
rounded-[20px] py-1 pr-2.5 pl-1 text-sm
|
||||
text-text-secondary hover:bg-state-base-hover
|
||||
mobile:px-1
|
||||
${open && 'bg-state-base-hover'}
|
||||
`}
|
||||
>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='sm:mr-2 mr-0' size={32} />
|
||||
{!isMobile && <>
|
||||
{userProfile.name}
|
||||
<RiArrowDownSLine className="w-3 h-3 ml-1 text-text-tertiary" />
|
||||
</>}
|
||||
<Menu.Button className={cn('inline-flex items-center rounded-[20px] p-0.5 hover:bg-background-default-dodge', open && 'bg-background-default-dodge')}>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
@ -92,7 +90,15 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<Menu.Item disabled>
|
||||
<div className='flex flex-nowrap items-center pl-3 pr-2 py-[13px]'>
|
||||
<div className='grow'>
|
||||
<div className='system-md-medium text-text-primary break-all'>{userProfile.name}</div>
|
||||
<div className='system-md-medium text-text-primary break-all'>
|
||||
{userProfile.name}
|
||||
{isEducationAccount && (
|
||||
<PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
|
||||
<RiGraduationCapFill className='w-3 h-3 mr-1' />
|
||||
<span className='system-2xs-medium'>EDU</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary break-all'>{userProfile.email}</div>
|
||||
</div>
|
||||
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' />
|
||||
@ -101,7 +107,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group',
|
||||
className={cn(itemClassName, 'group',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='/account'
|
||||
@ -112,7 +118,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className={classNames(itemClassName,
|
||||
{({ active }) => <div className={cn(itemClassName,
|
||||
active && 'bg-state-base-hover',
|
||||
)} onClick={() => setShowAccountSettingModal({ payload: 'members' })}>
|
||||
<RiSettings3Line className='size-4 shrink-0 text-text-tertiary' />
|
||||
@ -123,7 +129,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<div className='p-1'>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
className={cn(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href={
|
||||
@ -141,7 +147,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<div className='p-1'>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
className={cn(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://roadmap.dify.ai'
|
||||
@ -153,7 +159,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
</Menu.Item>
|
||||
{systemFeatures.license.status === LicenseStatus.NONE && <Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
className={cn(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://github.com/langgenius/dify'
|
||||
@ -169,7 +175,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
{
|
||||
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
|
||||
{({ active }) => <div className={cn(itemClassName, 'justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)} onClick={() => setAboutVisible(true)}>
|
||||
<RiInformation2Line className='shrink-0 size-4 text-text-tertiary' />
|
||||
@ -186,7 +192,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className='p-1' onClick={() => handleLogout()}>
|
||||
<div
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
className={cn(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
|
@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import PlanBadge from '@/app/components/header/plan-badge'
|
||||
import { switchWorkspace } from '@/service/common'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import PlanBadge from '../../plan-badge'
|
||||
import type { Plan } from '@/app/components/billing/type'
|
||||
|
||||
const WorkplaceSelector = () => {
|
||||
|
@ -94,10 +94,10 @@ const Header = () => {
|
||||
}
|
||||
<div className='flex items-center shrink-0'>
|
||||
<EnvNav />
|
||||
<div className='mr-3'>
|
||||
<div className='mr-2'>
|
||||
<PluginsNav />
|
||||
</div>
|
||||
<AccountDropdown isMobile={isMobile} />
|
||||
<AccountDropdown />
|
||||
</div>
|
||||
{
|
||||
(isMobile && isShowNavMenu) && (
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiGraduationCapFill,
|
||||
} from '@remixicon/react'
|
||||
import { SparklesSoft } from '../../base/icons/src/public/common'
|
||||
import PremiumBadge from '../../base/premium-badge'
|
||||
import { Plan } from '../../billing/type'
|
||||
@ -13,7 +16,7 @@ type PlanBadgeProps = {
|
||||
}
|
||||
|
||||
const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => {
|
||||
const { isFetchedPlan } = useProviderContext()
|
||||
const { isFetchedPlan, isEducationWorkspace } = useProviderContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!isFetchedPlan) return null
|
||||
@ -39,7 +42,8 @@ const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = fa
|
||||
if (plan === Plan.professional) {
|
||||
return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}>
|
||||
<div className='system-2xs-medium-uppercase'>
|
||||
<span className='p-1'>
|
||||
<span className='p-1 inline-flex items-center gap-1'>
|
||||
{isEducationWorkspace && <RiGraduationCapFill className='w-3 h-3' />}
|
||||
pro
|
||||
</span>
|
||||
</div>
|
||||
|
2
web/app/education-apply/components/constants.ts
Normal file
2
web/app/education-apply/components/constants.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify'
|
||||
export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
|
170
web/app/education-apply/components/education-apply-page.tsx
Normal file
170
web/app/education-apply/components/education-apply-page.tsx
Normal file
@ -0,0 +1,170 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from 'next/navigation'
|
||||
import UserInfo from './user-info'
|
||||
import SearchInput from './search-input'
|
||||
import RoleSelector from './role-selector'
|
||||
import Confirm from './verify-state-modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import {
|
||||
useEducationAdd,
|
||||
useInvalidateEducationStatus,
|
||||
} from '@/service/use-education'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/components/constants'
|
||||
|
||||
const EducationApplyAge = () => {
|
||||
const { t } = useTranslation()
|
||||
const [schoolName, setSchoolName] = useState('')
|
||||
const [role, setRole] = useState('Student')
|
||||
const [ageChecked, setAgeChecked] = useState(false)
|
||||
const [inSchoolChecked, setInSchoolChecked] = useState(false)
|
||||
const {
|
||||
isPending,
|
||||
mutateAsync: educationAdd,
|
||||
} = useEducationAdd({ onSuccess: () => {} })
|
||||
const [modalShow, setShowModal] = useState<undefined | { title: string; desc: string; onConfirm?: () => void }>(undefined)
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
const updateEducationStatus = useInvalidateEducationStatus()
|
||||
const { notify } = useToastContext()
|
||||
const router = useRouter()
|
||||
|
||||
const handleModalConfirm = () => {
|
||||
setShowModal(undefined)
|
||||
onPlanInfoChanged()
|
||||
updateEducationStatus()
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const handleSubmit = () => {
|
||||
educationAdd({
|
||||
token: token || '',
|
||||
role,
|
||||
institution: schoolName,
|
||||
}).then((res) => {
|
||||
if (res.message === 'success') {
|
||||
setShowModal({
|
||||
title: t('education.successTitle'),
|
||||
desc: t('education.successContent'),
|
||||
onConfirm: handleModalConfirm,
|
||||
})
|
||||
}
|
||||
else {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('education.submitError'),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-center p-6 w-full h-full'>
|
||||
<div className='relative max-w-[1408px] w-full border border-effects-highlight bg-background-default-subtle rounded-2xl'>
|
||||
<div
|
||||
className="absolute top-0 w-full h-[349px] rounded-t-2xl overflow-hidden bg-no-repeat bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: 'url(/education/bg.png)',
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
<div className='relative flex items-center justify-between px-8 py-6 h-[88px] z-10'>
|
||||
<img
|
||||
src='/logo/logo-site-dark.png'
|
||||
alt='dify logo'
|
||||
className='h-10'
|
||||
/>
|
||||
</div>
|
||||
<div className='relative m-auto px-8 max-w-[720px] z-10'>
|
||||
<div className='flex flex-col justify-end mb-2 pt-3 pb-4 h-[192px] text-text-primary-on-surface'>
|
||||
<div className='mb-2 title-5xl-bold shadow-xs'>{t('education.toVerified')}</div>
|
||||
<div className='system-md-medium shadow-xs'>
|
||||
{t('education.toVerifiedTip.front')}
|
||||
<span className='system-md-semibold underline'>{t('education.toVerifiedTip.coupon')}</span>
|
||||
{t('education.toVerifiedTip.end')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mb-7'>
|
||||
<UserInfo />
|
||||
</div>
|
||||
<div className='mb-7'>
|
||||
<div className='flex items-center mb-1 h-6 system-md-semibold text-text-secondary'>
|
||||
{t('education.form.schoolName.title')}
|
||||
</div>
|
||||
<SearchInput
|
||||
value={schoolName}
|
||||
onChange={setSchoolName}
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-7'>
|
||||
<div className='flex items-center mb-1 h-6 system-md-semibold text-text-secondary'>
|
||||
{t('education.form.schoolRole.title')}
|
||||
</div>
|
||||
<RoleSelector
|
||||
value={role}
|
||||
onChange={setRole}
|
||||
/>
|
||||
</div>
|
||||
<div className='mb-7'>
|
||||
<div className='flex items-center mb-1 h-6 system-md-semibold text-text-secondary'>
|
||||
{t('education.form.terms.title')}
|
||||
</div>
|
||||
<div className='mb-1 system-md-regular text-text-tertiary'>
|
||||
{t('education.form.terms.desc.front')}
|
||||
<a href='https://dify.ai/terms' target='_blank' className='text-text-secondary hover:underline'>{t('education.form.terms.desc.termsOfService')}</a>
|
||||
{t('education.form.terms.desc.and')}
|
||||
<a href='https://dify.ai/privacy' target='_blank' className='text-text-secondary hover:underline'>{t('education.form.terms.desc.privacyPolicy')}</a>
|
||||
{t('education.form.terms.desc.end')}
|
||||
</div>
|
||||
<div className='py-2 system-md-regular text-text-primary'>
|
||||
<div className='flex mb-2'>
|
||||
<Checkbox
|
||||
className='shrink-0 mr-2'
|
||||
checked={ageChecked}
|
||||
onCheck={() => setAgeChecked(!ageChecked)}
|
||||
/>
|
||||
{t('education.form.terms.option.age')}
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<Checkbox
|
||||
className='shrink-0 mr-2'
|
||||
checked={inSchoolChecked}
|
||||
onCheck={() => setInSchoolChecked(!inSchoolChecked)}
|
||||
/>
|
||||
{t('education.form.terms.option.inSchool')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant='primary'
|
||||
disabled={!ageChecked || !inSchoolChecked || !schoolName || !role || isPending}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{t('education.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Confirm
|
||||
isShow={!!modalShow}
|
||||
title={modalShow?.title || ''}
|
||||
content={modalShow?.desc}
|
||||
onConfirm={modalShow?.onConfirm || (() => {})}
|
||||
onCancel={modalShow?.onConfirm || (() => {})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EducationApplyAge
|
44
web/app/education-apply/components/hooks.ts
Normal file
44
web/app/education-apply/components/hooks.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import type { SearchParams } from './types'
|
||||
import { useEducationAutocomplete } from '@/service/use-education'
|
||||
|
||||
export const useEducation = () => {
|
||||
const {
|
||||
mutateAsync,
|
||||
isPending,
|
||||
data,
|
||||
} = useEducationAutocomplete()
|
||||
|
||||
const [prevSchools, setPrevSchools] = useState<string[]>([])
|
||||
const handleUpdateSchools = useCallback((searchParams: SearchParams) => {
|
||||
if (searchParams.keywords) {
|
||||
mutateAsync(searchParams).then((res) => {
|
||||
const currentPage = searchParams.page || 0
|
||||
const resSchools = res.data
|
||||
if (currentPage > 0)
|
||||
setPrevSchools(prevSchools => [...(prevSchools || []), ...resSchools])
|
||||
else
|
||||
setPrevSchools(resSchools)
|
||||
})
|
||||
}
|
||||
}, [mutateAsync])
|
||||
|
||||
const { run: querySchoolsWithDebounced } = useDebounceFn((searchParams: SearchParams) => {
|
||||
handleUpdateSchools(searchParams)
|
||||
}, {
|
||||
wait: 300,
|
||||
})
|
||||
|
||||
return {
|
||||
schools: prevSchools,
|
||||
setSchools: setPrevSchools,
|
||||
querySchoolsWithDebounced,
|
||||
handleUpdateSchools,
|
||||
isLoading: isPending,
|
||||
hasNext: data?.has_next,
|
||||
}
|
||||
}
|
53
web/app/education-apply/components/role-selector.tsx
Normal file
53
web/app/education-apply/components/role-selector.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type RoleSelectorProps = {
|
||||
onChange: (value: string) => void
|
||||
value: string
|
||||
}
|
||||
|
||||
const RoleSelector = ({
|
||||
onChange,
|
||||
value,
|
||||
}: RoleSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const options = [
|
||||
{
|
||||
key: 'Student',
|
||||
value: t('education.form.schoolRole.option.student'),
|
||||
},
|
||||
{
|
||||
key: 'Teacher',
|
||||
value: t('education.form.schoolRole.option.teacher'),
|
||||
},
|
||||
{
|
||||
key: 'School-Administrator',
|
||||
value: t('education.form.schoolRole.option.administrator'),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='flex'>
|
||||
{
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className='flex items-center mr-6 h-5 cursor-pointer system-md-regular text-text-primary'
|
||||
onClick={() => onChange(option.key)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 w-4 h-4 bg-components-radio-bg rounded-full border border-components-radio-border shadow-xs',
|
||||
option.key === value && 'border-[5px] border-components-radio-border-checked ',
|
||||
)}
|
||||
>
|
||||
</div>
|
||||
{option.value}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoleSelector
|
121
web/app/education-apply/components/search-input.tsx
Normal file
121
web/app/education-apply/components/search-input.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEducation } from './hooks'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type SearchInputProps = {
|
||||
value?: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
const SearchInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: SearchInputProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const {
|
||||
schools,
|
||||
setSchools,
|
||||
querySchoolsWithDebounced,
|
||||
handleUpdateSchools,
|
||||
hasNext,
|
||||
} = useEducation()
|
||||
const pageRef = useRef(0)
|
||||
const valueRef = useRef(value)
|
||||
|
||||
const handleSearch = useCallback((debounced?: boolean) => {
|
||||
const keywords = valueRef.current
|
||||
const page = pageRef.current
|
||||
if (debounced) {
|
||||
querySchoolsWithDebounced({
|
||||
keywords,
|
||||
page,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handleUpdateSchools({
|
||||
keywords,
|
||||
page,
|
||||
})
|
||||
}, [querySchoolsWithDebounced, handleUpdateSchools])
|
||||
|
||||
const handleValueChange = useCallback((e: any) => {
|
||||
setOpen(true)
|
||||
setSchools([])
|
||||
pageRef.current = 0
|
||||
const inputValue = e.target.value
|
||||
valueRef.current = inputValue
|
||||
onChange(inputValue)
|
||||
handleSearch(true)
|
||||
}, [onChange, handleSearch, setSchools])
|
||||
|
||||
const handleScroll = useCallback((e: Event) => {
|
||||
const target = e.target as HTMLDivElement
|
||||
const {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
} = target
|
||||
if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0 && hasNext) {
|
||||
pageRef.current += 1
|
||||
handleSearch()
|
||||
}
|
||||
}, [handleSearch, hasNext])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom'
|
||||
offset={4}
|
||||
triggerPopupSameWidth
|
||||
>
|
||||
<PortalToFollowElemTrigger className='block w-full'>
|
||||
<Input
|
||||
className='w-full'
|
||||
placeholder={t('education.form.schoolName.placeholder')}
|
||||
value={value}
|
||||
onChange={handleValueChange}
|
||||
/>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
{
|
||||
!!schools.length && value && (
|
||||
<div
|
||||
className='p-1 max-h-[330px] overflow-y-auto border-[0.5px] border-components-panel-border bg-components-panel-bg-blur rounded-xl'
|
||||
onScroll={handleScroll as any}
|
||||
>
|
||||
{
|
||||
schools.map((school, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center px-2 py-1.5 h-8 system-md-regular text-text-secondary truncate cursor-pointer hover:bg-state-base-hover rounded-lg'
|
||||
title={school}
|
||||
onClick={() => {
|
||||
onChange(school)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{school}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
11
web/app/education-apply/components/types.ts
Normal file
11
web/app/education-apply/components/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type SearchParams = {
|
||||
keywords?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export type EducationAddParams = {
|
||||
token: string
|
||||
institution: string
|
||||
role: string
|
||||
}
|
61
web/app/education-apply/components/user-info.tsx
Normal file
61
web/app/education-apply/components/user-info.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { logout } from '@/service/common'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { Triangle } from '@/app/components/base/icons/src/public/education'
|
||||
|
||||
const UserInfo = () => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const { userProfile } = useAppContext()
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout({
|
||||
url: '/logout',
|
||||
params: {},
|
||||
})
|
||||
|
||||
localStorage.removeItem('setup_status')
|
||||
localStorage.removeItem('console_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
|
||||
router.push('/signin')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative flex items-center justify-between pl-6 pt-9 pr-8 pb-6 bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 border-[4px] border-components-panel-on-panel-item-bg rounded-xl shadow-shadow-shadow-5'>
|
||||
<div className='absolute top-0 left-0 flex items-center'>
|
||||
<div className='flex items-center pl-2 pt-1 h-[22px] bg-components-panel-on-panel-item-bg system-2xs-semibold-uppercase text-text-accent-light-mode-only'>
|
||||
{t('education.currentSigned')}
|
||||
</div>
|
||||
<Triangle className='w-4 h-[22px] text-components-panel-on-panel-item-bg' />
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<Avatar
|
||||
className='mr-4'
|
||||
avatar={userProfile.avatar_url}
|
||||
name={userProfile.name}
|
||||
size={48}
|
||||
/>
|
||||
<div className='pt-1.5'>
|
||||
<div className='system-md-semibold text-text-primary'>
|
||||
{userProfile.name}
|
||||
</div>
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
{userProfile.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={handleLogout}
|
||||
>
|
||||
{t('common.userProfile.logout')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserInfo
|
108
web/app/education-apply/components/verify-state-modal.tsx
Normal file
108
web/app/education-apply/components/verify-state-modal.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiExternalLinkLine,
|
||||
} from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IConfirm = {
|
||||
className?: string
|
||||
isShow: boolean
|
||||
title: string
|
||||
content?: React.ReactNode
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
maskClosable?: boolean
|
||||
email?: string
|
||||
showLink?: boolean
|
||||
}
|
||||
|
||||
function Confirm({
|
||||
isShow,
|
||||
title,
|
||||
content,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
maskClosable = true,
|
||||
showLink,
|
||||
email,
|
||||
}: IConfirm) {
|
||||
const { t } = useTranslation()
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
const [isVisible, setIsVisible] = useState(isShow)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape')
|
||||
onCancel()
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (maskClosable && dialogRef.current && !dialogRef.current.contains(event.target as Node))
|
||||
onCancel()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [maskClosable])
|
||||
|
||||
useEffect(() => {
|
||||
if (isShow) {
|
||||
setIsVisible(true)
|
||||
}
|
||||
else {
|
||||
const timer = setTimeout(() => setIsVisible(false), 200)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isShow])
|
||||
|
||||
if (!isVisible)
|
||||
return null
|
||||
|
||||
return createPortal(
|
||||
<div className={'fixed inset-0 flex items-center justify-center z-[10000000] bg-background-overlay'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div ref={dialogRef} className={'relative w-full max-w-[481px] overflow-hidden'}>
|
||||
<div className='flex flex-col items-start max-w-full rounded-2xl border-[0.5px] border-solid border-components-panel-border shadows-shadow-lg bg-components-panel-bg'>
|
||||
<div className='flex pt-6 pl-6 pr-6 pb-4 flex-col items-start gap-2 self-stretch'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{title}</div>
|
||||
<div className='system-md-regular text-text-tertiary w-full'>{content}</div>
|
||||
</div>
|
||||
{email && (
|
||||
<div className='px-6 py-3 space-y-1 w-full'>
|
||||
<div className='text-text-secondary system-sm-semibold py-1'>{t('education.emailLabel')}</div>
|
||||
<div className='px-3 py-2 bg-components-input-bg-disabled rounded-lg text-components-input-text-filled-disabled system-sm-regular'>{email}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex p-6 gap-2 justify-between items-center self-stretch'>
|
||||
<div className='flex items-center gap-1'>
|
||||
{showLink && (
|
||||
<>
|
||||
<a href='' className='text-text-accent system-xs-regular cursor-pointer'>{t('education.learn')}</a>
|
||||
<RiExternalLinkLine className='w-3 h-3 text-text-accent' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button variant='primary' className='!w-20' onClick={onConfirm}>{t('common.operation.ok')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>, document.body,
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Confirm)
|
33
web/app/education-apply/layout.tsx
Normal file
33
web/app/education-apply/layout.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import SwrInitor from '@/app/components/swr-initor'
|
||||
import { AppContextProvider } from '@/context/app-context'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<SwrInitor>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<ModalContextProvider>
|
||||
{children}
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
</SwrInitor>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dify',
|
||||
}
|
||||
|
||||
export default Layout
|
29
web/app/education-apply/page.tsx
Normal file
29
web/app/education-apply/page.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import {
|
||||
useRouter,
|
||||
useSearchParams,
|
||||
} from 'next/navigation'
|
||||
import EducationApplyAge from './components/education-apply-page'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
export default function EducationApply() {
|
||||
const router = useRouter()
|
||||
const { enableEducationPlan, isEducationAccount } = useProviderContext()
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const showEducationApplyPage = useMemo(() => {
|
||||
return enableEducationPlan && !isEducationAccount && token
|
||||
}, [enableEducationPlan, isEducationAccount, token])
|
||||
|
||||
useEffect(() => {
|
||||
if (!showEducationApplyPage)
|
||||
router.replace('/')
|
||||
}, [showEducationApplyPage, router])
|
||||
|
||||
return <EducationApplyAge />
|
||||
}
|
@ -17,6 +17,9 @@ import type {
|
||||
ModelLoadBalancingConfigEntry,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/components/constants'
|
||||
|
||||
import Pricing from '@/app/components/billing/pricing'
|
||||
import type { ModerationConfig, PromptVariable } from '@/models/debug'
|
||||
@ -121,6 +124,10 @@ export const ModalContextProvider = ({
|
||||
const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
if (educationVerifying === 'yes')
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
setShowAccountSettingModal(null)
|
||||
if (showAccountSettingModal?.onCancelCallback)
|
||||
showAccountSettingModal?.onCancelCallback()
|
||||
|
@ -22,6 +22,9 @@ import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||
import { parseCurrentPlan } from '@/app/components/billing/utils'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
useEducationStatus,
|
||||
} from '@/service/use-education'
|
||||
|
||||
type ProviderContextState = {
|
||||
modelProviders: ModelProvider[]
|
||||
@ -40,6 +43,9 @@ type ProviderContextState = {
|
||||
enableReplaceWebAppLogo: boolean
|
||||
modelLoadBalancingEnabled: boolean
|
||||
datasetOperatorEnabled: boolean
|
||||
enableEducationPlan: boolean
|
||||
isEducationWorkspace: boolean
|
||||
isEducationAccount: boolean
|
||||
}
|
||||
const ProviderContext = createContext<ProviderContextState>({
|
||||
modelProviders: [],
|
||||
@ -70,6 +76,9 @@ const ProviderContext = createContext<ProviderContextState>({
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
datasetOperatorEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
isEducationWorkspace: false,
|
||||
isEducationAccount: false,
|
||||
})
|
||||
|
||||
export const useProviderContext = () => useContext(ProviderContext)
|
||||
@ -97,13 +106,19 @@ export const ProviderContextProvider = ({
|
||||
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
|
||||
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
|
||||
|
||||
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
|
||||
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
|
||||
const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan)
|
||||
|
||||
const fetchPlan = async () => {
|
||||
const data = await fetchCurrentPlanInfo()
|
||||
const enabled = data.billing.enabled
|
||||
setEnableBilling(enabled)
|
||||
setEnableEducationPlan(data.education.enabled)
|
||||
setIsEducationWorkspace(data.education.activated)
|
||||
setEnableReplaceWebAppLogo(data.can_replace_logo)
|
||||
if (enabled) {
|
||||
setPlan(parseCurrentPlan(data))
|
||||
setPlan(parseCurrentPlan(data) as any)
|
||||
setIsFetchedPlan(true)
|
||||
}
|
||||
if (data.model_load_balancing_enabled)
|
||||
@ -155,6 +170,9 @@ export const ProviderContextProvider = ({
|
||||
enableReplaceWebAppLogo,
|
||||
modelLoadBalancingEnabled,
|
||||
datasetOperatorEnabled,
|
||||
enableEducationPlan,
|
||||
isEducationWorkspace,
|
||||
isEducationAccount: isEducationAccount?.result || false,
|
||||
}}>
|
||||
{children}
|
||||
</ProviderContext.Provider>
|
||||
|
47
web/i18n/en-US/education.ts
Normal file
47
web/i18n/en-US/education.ts
Normal file
@ -0,0 +1,47 @@
|
||||
const translation = {
|
||||
toVerified: 'Get Education Verified',
|
||||
toVerifiedTip: {
|
||||
front: 'You are now eligible for Education Verified status. Please enter your education information below to complete the process and receive an',
|
||||
coupon: 'exclusive 50% coupon',
|
||||
end: 'for the Dify Professional Plan.',
|
||||
},
|
||||
currentSigned: 'CURRENTLY SIGNED IN AS',
|
||||
form: {
|
||||
schoolName: {
|
||||
title: 'Your School Name',
|
||||
placeholder: 'Enter the official, unabbreviated name of your school',
|
||||
},
|
||||
schoolRole: {
|
||||
title: 'Your School Role',
|
||||
option: {
|
||||
student: 'Student',
|
||||
teacher: 'Teacher',
|
||||
administrator: 'School Administrator',
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
title: 'Terms & Agreements',
|
||||
desc: {
|
||||
front: 'Your information and use of Education Verified status are subject to our',
|
||||
and: 'and',
|
||||
end: '. By submitting:',
|
||||
termsOfService: 'Terms of Service',
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
},
|
||||
option: {
|
||||
age: 'I confirm I am at least 18 years old',
|
||||
inSchool: 'I confirm I am enrolled or employed at the institution provided. Dify may request proof of enrollment/employment. If I misrepresent my eligibility, I agree to pay any fees initially waived based on my education status.',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: 'Submit',
|
||||
submitError: 'Form submission failed. Please try again later.',
|
||||
learn: 'Learn how to get education verified',
|
||||
successTitle: 'You Have Got Dify Education Verified',
|
||||
successContent: 'We have issued a 50% discount coupon for the Dify Professional plan to your account. The coupon is valid for one year, please use it within the validity period.',
|
||||
rejectTitle: 'Your Dify Education Verified Has Been Rejected',
|
||||
rejectContent: 'Unfortunately, you are not eligible for Education Verified status and therefore cannot receive the exclusive 50% coupon for the Dify Professional Plan if you use this email address.',
|
||||
emailLabel: 'Your current email',
|
||||
}
|
||||
|
||||
export default translation
|
@ -4,6 +4,18 @@ import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
|
||||
const requireSilent = (lang: string) => {
|
||||
let res
|
||||
try {
|
||||
res = require(`./${lang}/education`).default
|
||||
}
|
||||
catch {
|
||||
res = require('./en-US/education').default
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
const loadLangResources = (lang: string) => ({
|
||||
translation: {
|
||||
common: require(`./${lang}/common`).default,
|
||||
@ -31,6 +43,7 @@ const loadLangResources = (lang: string) => ({
|
||||
plugin: require(`./${lang}/plugin`).default,
|
||||
pluginTags: require(`./${lang}/plugin-tags`).default,
|
||||
time: require(`./${lang}/time`).default,
|
||||
education: requireSilent(lang),
|
||||
},
|
||||
})
|
||||
|
||||
|
47
web/i18n/ja-JP/education.ts
Normal file
47
web/i18n/ja-JP/education.ts
Normal file
@ -0,0 +1,47 @@
|
||||
const translation = {
|
||||
toVerified: '教育認証を取得',
|
||||
toVerifiedTip: {
|
||||
front: '現在、教育認証ステータスを取得する資格があります。以下に教育情報を入力し、認証プロセスを完了すると、Difyプロフェッショナルプランの',
|
||||
coupon: '50%割引クーポン',
|
||||
end: 'を受け取ることができます。',
|
||||
},
|
||||
currentSigned: '現在ログイン中のアカウントは',
|
||||
form: {
|
||||
schoolName: {
|
||||
title: '学校名',
|
||||
placeholder: '学校の正式名称(省略不可)を入力してください。',
|
||||
},
|
||||
schoolRole: {
|
||||
title: '学校での役割',
|
||||
option: {
|
||||
student: '学生',
|
||||
teacher: '教師',
|
||||
administrator: '学校管理者',
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
title: '利用規約と同意事項',
|
||||
desc: {
|
||||
front: 'お客様の情報および 教育認証ステータス の利用は、当社の ',
|
||||
and: 'および',
|
||||
end: 'に従うものとします。送信することで以下を確認します:',
|
||||
termsOfService: '利用規約',
|
||||
privacyPolicy: 'プライバシーポリシー',
|
||||
},
|
||||
option: {
|
||||
age: '18歳以上であることを確認します。',
|
||||
inSchool: '提供した教育機関に在籍または勤務している ことを確認します。Difyは在籍/雇用証明の提出を求める場合があります。不正な情報を申告した場合、教育認証に基づき免除された費用を支払うことに同意します。',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: '送信',
|
||||
submitError: 'フォームの送信に失敗しました。しばらくしてから再度ご提出ください。',
|
||||
learn: '教育認証の取得方法はこちら',
|
||||
successTitle: 'Dify教育認証を取得しました!',
|
||||
successContent: 'お客様のアカウントに Difyプロフェッショナルプランの50%割引クーポン を発行しました。有効期間は 1年間 ですので、期限内にご利用ください。',
|
||||
rejectTitle: 'Dify教育認証が拒否されました',
|
||||
rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Difyプロフェッショナルプランの50%割引クーポン を受け取ることはできません。',
|
||||
emailLabel: '現在のメールアドレス',
|
||||
}
|
||||
|
||||
export default translation
|
48
web/i18n/zh-Hans/education.ts
Normal file
48
web/i18n/zh-Hans/education.ts
Normal file
@ -0,0 +1,48 @@
|
||||
const translation = {
|
||||
toVerified: '获取教育版认证',
|
||||
toVerifiedTip: {
|
||||
front: '您现在符合教育版认证的资格。请在下方输入您的教育信息,以完成认证流程,并领取 Dify Professional 版的',
|
||||
coupon: '50% 独家优惠券',
|
||||
end: '。',
|
||||
},
|
||||
currentSigned: '您当前登录的账户是',
|
||||
form: {
|
||||
schoolName: {
|
||||
title: '您的学校名称',
|
||||
placeholder: '请输入您的学校的官方全称(不得缩写)',
|
||||
},
|
||||
schoolRole: {
|
||||
title: '您在学校的身份',
|
||||
option: {
|
||||
student: '学生',
|
||||
teacher: '教师',
|
||||
administrator: '学校管理员',
|
||||
},
|
||||
},
|
||||
terms: {
|
||||
title: '条款与协议',
|
||||
desc: {
|
||||
front: '您的信息和教育版认证资格的使用需遵守我们的',
|
||||
and: '和',
|
||||
end: '。提交即表示:',
|
||||
termsOfService: '服务条款',
|
||||
privacyPolicy: '隐私政策',
|
||||
},
|
||||
option: {
|
||||
age: '我确认我已年满 18 周岁。',
|
||||
inSchool: '我确认我目前已在提供的学校入学或受雇。Dify 可能会要求提供入学/雇佣证明。如我虚报资格,我同意支付因教育版认证而被减免的费用。',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: '提交',
|
||||
submitError: '提交表单失败,请稍后重新提交问卷。',
|
||||
learn: '了解如何获取教育版认证',
|
||||
successTitle: '您已成功获得 Dify 教育版认证!',
|
||||
successContent: '我们已向您的账户发放 Dify Professional 版 50% 折扣优惠券。该优惠券有效期为一年,请在有效期内使用。',
|
||||
rejectTitle: '您的 Dify 教育版认证已被拒绝',
|
||||
rejectContent: '非常遗憾,您无法使用此电子邮件以获得教育版认证资格,也无法领取 Dify Professional 版的 50% 独家优惠券。',
|
||||
emailLabel: '您当前的邮箱',
|
||||
|
||||
}
|
||||
|
||||
export default translation
|
BIN
web/public/education/bg.png
Normal file
BIN
web/public/education/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
67
web/service/use-education.ts
Normal file
67
web/service/use-education.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { get, post } from './base'
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { useInvalid } from './use-base'
|
||||
import type { EducationAddParams } from '@/app/education-apply/components/types'
|
||||
|
||||
const NAME_SPACE = 'education'
|
||||
|
||||
export const useEducationVerify = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'education-verify'],
|
||||
mutationFn: () => {
|
||||
return get<{ token: string }>('/account/education/verify')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useEducationAdd = ({
|
||||
onSuccess,
|
||||
}: {
|
||||
onSuccess?: () => void
|
||||
}) => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'education-add'],
|
||||
mutationFn: (params: EducationAddParams) => {
|
||||
return post<{ message: string }>('/account/education', {
|
||||
body: params,
|
||||
})
|
||||
},
|
||||
onSuccess,
|
||||
})
|
||||
}
|
||||
|
||||
type SearchParams = {
|
||||
keywords?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
}
|
||||
export const useEducationAutocomplete = () => {
|
||||
return useMutation({
|
||||
mutationFn: (searchParams: SearchParams) => {
|
||||
const {
|
||||
keywords = '',
|
||||
page = 0,
|
||||
limit = 40,
|
||||
} = searchParams
|
||||
return get<{ data: string[]; has_next: boolean; curr_page: number }>(`/account/education/autocomplete?keywords=${keywords}&page=${page}&limit=${limit}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useEducationStatus = (disable?: boolean) => {
|
||||
return useQuery({
|
||||
enabled: !disable,
|
||||
queryKey: [NAME_SPACE, 'education-status'],
|
||||
queryFn: () => {
|
||||
return get<{ result: boolean }>('/account/education')
|
||||
},
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidateEducationStatus = () => {
|
||||
return useInvalid([NAME_SPACE, 'education-status'])
|
||||
}
|
Loading…
Reference in New Issue
Block a user