Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com>tags/1.2.0
| - "main" | - "main" | ||||
| - "deploy/dev" | - "deploy/dev" | ||||
| - "deploy/enterprise" | - "deploy/enterprise" | ||||
| - release/1.1.3-fix1 | |||||
| tags: | tags: | ||||
| - "*" | - "*" | ||||
| 'use client' | 'use client' | ||||
| import { useCallback, useEffect, useRef, useState } from 'react' | import { useCallback, useEffect, useRef, useState } from 'react' | ||||
| import { useRouter } from 'next/navigation' | |||||
| import { | |||||
| useRouter, | |||||
| } from 'next/navigation' | |||||
| import useSWRInfinite from 'swr/infinite' | import useSWRInfinite from 'swr/infinite' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useDebounceFn } from 'ahooks' | import { useDebounceFn } from 'ahooks' |
| import Apps from './Apps' | import Apps from './Apps' | ||||
| import AppContext from '@/context/app-context' | import AppContext from '@/context/app-context' | ||||
| import { LicenseStatus } from '@/types/feature' | import { LicenseStatus } from '@/types/feature' | ||||
| import { useEducationInit } from '@/app/education-apply/hooks' | |||||
| const AppList = () => { | const AppList = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| useEducationInit() | |||||
| const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) | const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) | ||||
| return ( | return ( |
| 'use client' | |||||
| import { | |||||
| useEffect, | |||||
| useMemo, | |||||
| } from 'react' | |||||
| import { | |||||
| useRouter, | |||||
| useSearchParams, | |||||
| } from 'next/navigation' | |||||
| import EducationApplyPage from '@/app/education-apply/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 <EducationApplyPage /> | |||||
| } |
| 'use client' | 'use client' | ||||
| import { useState } from 'react' | import { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { | |||||
| RiGraduationCapFill, | |||||
| } from '@remixicon/react' | |||||
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import DeleteAccount from '../delete-account' | import DeleteAccount from '../delete-account' | ||||
| import s from './index.module.css' | import s from './index.module.css' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { updateUserProfile } from '@/service/common' | import { updateUserProfile } from '@/service/common' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| import { useProviderContext } from '@/context/provider-context' | |||||
| import { ToastContext } from '@/app/components/base/toast' | import { ToastContext } from '@/app/components/base/toast' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import { IS_CE_EDITION } from '@/config' | import { IS_CE_EDITION } from '@/config' | ||||
| import Input from '@/app/components/base/input' | import Input from '@/app/components/base/input' | ||||
| import PremiumBadge from '@/app/components/base/premium-badge' | |||||
| const titleClassName = ` | const titleClassName = ` | ||||
| system-sm-semibold text-text-secondary | system-sm-semibold text-text-secondary | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { systemFeatures } = useAppContext() | const { systemFeatures } = useAppContext() | ||||
| const { mutateUserProfile, userProfile, apps } = useAppContext() | const { mutateUserProfile, userProfile, apps } = useAppContext() | ||||
| const { isEducationAccount } = useProviderContext() | |||||
| const { notify } = useContext(ToastContext) | const { notify } = useContext(ToastContext) | ||||
| const [editNameModalVisible, setEditNameModalVisible] = useState(false) | const [editNameModalVisible, setEditNameModalVisible] = useState(false) | ||||
| const [editName, setEditName] = useState('') | const [editName, setEditName] = useState('') | ||||
| <div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'> | <div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'> | ||||
| <AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} /> | <AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} /> | ||||
| <div className='ml-4'> | <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> | <p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { Fragment } from 'react' | import { Fragment } from 'react' | ||||
| import { useRouter } from 'next/navigation' | import { useRouter } from 'next/navigation' | ||||
| import { | |||||
| RiGraduationCapFill, | |||||
| } from '@remixicon/react' | |||||
| import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' | import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' | ||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| import { logout } from '@/service/common' | import { logout } from '@/service/common' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| import { useProviderContext } from '@/context/provider-context' | |||||
| import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general' | import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general' | ||||
| import PremiumBadge from '@/app/components/base/premium-badge' | |||||
| export type IAppSelector = { | export type IAppSelector = { | ||||
| isMobile: boolean | isMobile: boolean | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { userProfile } = useAppContext() | const { userProfile } = useAppContext() | ||||
| const { isEducationAccount } = useProviderContext() | |||||
| const handleLogout = async () => { | const handleLogout = async () => { | ||||
| await logout({ | await logout({ | ||||
| <div className='p-1'> | <div className='p-1'> | ||||
| <div className='flex flex-nowrap items-center px-3 py-2'> | <div className='flex flex-nowrap items-center px-3 py-2'> | ||||
| <div className='grow'> | <div className='grow'> | ||||
| <div className='system-md-medium break-all text-text-primary'>{userProfile.name}</div> | |||||
| <div className='system-md-medium break-all 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> | |||||
| )} | |||||
| </div> | |||||
| <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> | <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> | ||||
| </div> | </div> | ||||
| <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} /> | <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={32} /> |
| }, [setDatasetConfigs, datasetConfigsRef]) | }, [setDatasetConfigs, datasetConfigsRef]) | ||||
| const handleUpdateCondition = useCallback<HandleUpdateCondition>((id, newCondition) => { | const handleUpdateCondition = useCallback<HandleUpdateCondition>((id, newCondition) => { | ||||
| console.log(newCondition, 'newCondition') | |||||
| const conditions = datasetConfigsRef.current!.metadata_filtering_conditions?.conditions || [] | const conditions = datasetConfigsRef.current!.metadata_filtering_conditions?.conditions || [] | ||||
| const index = conditions.findIndex(c => c.id === id) | const index = conditions.findIndex(c => c.id === id) | ||||
| const newInputs = produce(datasetConfigsRef.current!, (draft) => { | const newInputs = produce(datasetConfigsRef.current!, (draft) => { |
| const handleConfirmDate = () => { | const handleConfirmDate = () => { | ||||
| // debugger | // debugger | ||||
| console.log(selectedDate, selectedDate?.tz(timezone)) | |||||
| onChange(selectedDate ? selectedDate.tz(timezone) : undefined) | onChange(selectedDate ? selectedDate.tz(timezone) : undefined) | ||||
| setIsOpen(false) | setIsOpen(false) | ||||
| } | } |
| <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> |
| { | |||||
| "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" | |||||
| } |
| // GENERATE BY script | |||||
| // DON NOT EDIT IT MANUALLY | |||||
| import * as React from 'react' | |||||
| import data from './Triangle.json' | |||||
| import IconBase from '@/app/components/base/icons/IconBase' | |||||
| import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' | |||||
| const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( | |||||
| props, | |||||
| ref, | |||||
| ) => <IconBase {...props} ref={ref} data={data as IconData} />) | |||||
| Icon.displayName = 'Triangle' | |||||
| export default Icon |
| export { default as Triangle } from './Triangle' |
| flip, | flip, | ||||
| offset, | offset, | ||||
| shift, | shift, | ||||
| size, | |||||
| useDismiss, | useDismiss, | ||||
| useFloating, | useFloating, | ||||
| useFocus, | useFocus, | ||||
| open?: boolean | open?: boolean | ||||
| offset?: number | OffsetOptions | offset?: number | OffsetOptions | ||||
| onOpenChange?: (open: boolean) => void | onOpenChange?: (open: boolean) => void | ||||
| triggerPopupSameWidth?: boolean | |||||
| } | } | ||||
| export function usePortalToFollowElem({ | export function usePortalToFollowElem({ | ||||
| open, | open, | ||||
| offset: offsetValue = 0, | offset: offsetValue = 0, | ||||
| onOpenChange: setControlledOpen, | onOpenChange: setControlledOpen, | ||||
| triggerPopupSameWidth, | |||||
| }: PortalToFollowElemOptions = {}) { | }: PortalToFollowElemOptions = {}) { | ||||
| const setOpen = setControlledOpen | const setOpen = setControlledOpen | ||||
| padding: 5, | padding: 5, | ||||
| }), | }), | ||||
| shift({ padding: 5 }), | shift({ padding: 5 }), | ||||
| size({ | |||||
| apply({ rects, elements }) { | |||||
| if (triggerPopupSameWidth) | |||||
| elements.floating.style.width = `${rects.reference.width}px` | |||||
| }, | |||||
| }), | |||||
| ], | ], | ||||
| }) | }) | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React from 'react' | import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useRouter } from 'next/navigation' | |||||
| import { | import { | ||||
| RiBook2Line, | RiBook2Line, | ||||
| RiBox3Line, | RiBox3Line, | ||||
| RiFileEditLine, | RiFileEditLine, | ||||
| RiGraduationCapLine, | |||||
| RiGroup3Line, | RiGroup3Line, | ||||
| RiGroupLine, | RiGroupLine, | ||||
| RiSquareLine, | RiSquareLine, | ||||
| import AppsInfo from '../usage-info/apps-info' | import AppsInfo from '../usage-info/apps-info' | ||||
| import UpgradeBtn from '../upgrade-btn' | import UpgradeBtn from '../upgrade-btn' | ||||
| import { useProviderContext } from '@/context/provider-context' | 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 UsageInfo from '@/app/components/billing/usage-info' | ||||
| import VerifyStateModal from '@/app/education-apply/verify-state-modal' | |||||
| import { EDUCATION_VERIFYING_LOCALSTORAGE_ITEM } from '@/app/education-apply/constants' | |||||
| import { useEducationVerify } from '@/service/use-education' | |||||
| import { useModalContextSelector } from '@/context/modal-context' | |||||
| type Props = { | type Props = { | ||||
| loc: string | loc: string | ||||
| loc, | loc, | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { plan } = useProviderContext() | |||||
| const router = useRouter() | |||||
| const { userProfile } = useAppContext() | |||||
| const { plan, enableEducationPlan, isEducationAccount } = useProviderContext() | |||||
| const { | const { | ||||
| type, | type, | ||||
| } = plan | } = plan | ||||
| total, | total, | ||||
| } = plan | } = plan | ||||
| const [showModal, setShowModal] = React.useState(false) | |||||
| const { mutateAsync } = useEducationVerify() | |||||
| const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) | |||||
| const handleVerify = () => { | |||||
| mutateAsync().then((res) => { | |||||
| localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) | |||||
| router.push(`/education-apply?token=${res.token}`) | |||||
| setShowAccountSettingModal(null) | |||||
| }).catch(() => { | |||||
| setShowModal(true) | |||||
| }) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className='rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn'> | <div className='rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn'> | ||||
| <div className='p-6 pb-2'> | <div className='p-6 pb-2'> | ||||
| </div> | </div> | ||||
| <div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div> | <div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div> | ||||
| </div> | </div> | ||||
| {(plan.type as any) !== SelfHostedPlan.enterprise && ( | |||||
| <UpgradeBtn | |||||
| className='shrink-0' | |||||
| isPlain={type === Plan.team} | |||||
| isShort | |||||
| loc={loc} | |||||
| /> | |||||
| )} | |||||
| <div className='flex shrink-0 items-center gap-1'> | |||||
| {enableEducationPlan && !isEducationAccount && ( | |||||
| <Button variant='ghost' onClick={handleVerify}> | |||||
| <RiGraduationCapLine className='mr-1 h-4 w-4'/> | |||||
| {t('education.toVerified')} | |||||
| </Button> | |||||
| )} | |||||
| {(plan.type as any) !== SelfHostedPlan.enterprise && ( | |||||
| <UpgradeBtn | |||||
| className='shrink-0' | |||||
| isPlain={type === Plan.team} | |||||
| isShort | |||||
| loc={loc} | |||||
| /> | |||||
| )} | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {/* Plan detail */} | {/* Plan detail */} | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| <VerifyStateModal | |||||
| showLink | |||||
| email={userProfile.email} | |||||
| isShow={showModal} | |||||
| title={t('education.rejectTitle')} | |||||
| content={t('education.rejectContent')} | |||||
| onConfirm={() => setShowModal(false)} | |||||
| onCancel={() => setShowModal(false)} | |||||
| /> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| can_replace_logo: boolean | can_replace_logo: boolean | ||||
| model_load_balancing_enabled: boolean | model_load_balancing_enabled: boolean | ||||
| dataset_operator_enabled: boolean | dataset_operator_enabled: boolean | ||||
| education: { | |||||
| enabled: boolean | |||||
| activated: boolean | |||||
| } | |||||
| } | } | ||||
| export type SubscriptionItem = { | export type SubscriptionItem = { |
| import { Fragment, useState } from 'react' | import { Fragment, useState } from 'react' | ||||
| import { useRouter } from 'next/navigation' | import { useRouter } from 'next/navigation' | ||||
| import { useContext, useContextSelector } from 'use-context-selector' | 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 Link from 'next/link' | ||||
| import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' | import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' | ||||
| import Indicator from '../indicator' | import Indicator from '../indicator' | ||||
| import GithubStar from '../github-star' | import GithubStar from '../github-star' | ||||
| import Support from './support' | import Support from './support' | ||||
| import Compliance from './compliance' | import Compliance from './compliance' | ||||
| import classNames from '@/utils/classnames' | |||||
| import PremiumBadge from '@/app/components/base/premium-badge' | |||||
| import I18n from '@/context/i18n' | import I18n from '@/context/i18n' | ||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| import { logout } from '@/service/common' | import { logout } from '@/service/common' | ||||
| import AppContext, { useAppContext } from '@/context/app-context' | import AppContext, { useAppContext } from '@/context/app-context' | ||||
| import { useProviderContext } from '@/context/provider-context' | |||||
| import { useModalContext } from '@/context/modal-context' | import { useModalContext } from '@/context/modal-context' | ||||
| import { LanguagesSupported } from '@/i18n/language' | import { LanguagesSupported } from '@/i18n/language' | ||||
| import { LicenseStatus } from '@/types/feature' | import { LicenseStatus } from '@/types/feature' | ||||
| import { IS_CLOUD_EDITION } from '@/config' | 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 = ` | const itemClassName = ` | ||||
| flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular | 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 | rounded-lg hover:bg-state-base-hover cursor-pointer gap-1 | ||||
| const { locale } = useContext(I18n) | const { locale } = useContext(I18n) | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() | const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext() | ||||
| const { isEducationAccount } = useProviderContext() | |||||
| const { setShowAccountSettingModal } = useModalContext() | const { setShowAccountSettingModal } = useModalContext() | ||||
| const handleLogout = async () => { | const handleLogout = async () => { | ||||
| { | { | ||||
| ({ open }) => ( | ({ open }) => ( | ||||
| <> | <> | ||||
| <MenuButton | |||||
| className={` | |||||
| inline-flex items-center | |||||
| rounded-[20px] py-1 pl-1 pr-2.5 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='mr-0 sm:mr-2' size={32} /> | |||||
| {!isMobile && <> | |||||
| {userProfile.name} | |||||
| <RiArrowDownSLine className="ml-1 h-3 w-3 text-text-tertiary" /> | |||||
| </>} | |||||
| <MenuButton 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} /> | |||||
| </MenuButton> | </MenuButton> | ||||
| <Transition | <Transition | ||||
| as={Fragment} | as={Fragment} | ||||
| <MenuItem disabled> | <MenuItem disabled> | ||||
| <div className='flex flex-nowrap items-center py-[13px] pl-3 pr-2'> | <div className='flex flex-nowrap items-center py-[13px] pl-3 pr-2'> | ||||
| <div className='grow'> | <div className='grow'> | ||||
| <div className='system-md-medium break-all text-text-primary'>{userProfile.name}</div> | |||||
| <div className='system-md-medium break-all 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> | |||||
| )} | |||||
| </div> | |||||
| <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> | <div className='system-xs-regular break-all text-text-tertiary'>{userProfile.email}</div> | ||||
| </div> | </div> | ||||
| <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' /> | <Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={36} className='mr-3' /> | ||||
| <div className="px-1 py-1"> | <div className="px-1 py-1"> | ||||
| <MenuItem> | <MenuItem> | ||||
| <Link | <Link | ||||
| className={classNames(itemClassName, 'group', | |||||
| className={cn(itemClassName, 'group', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} | )} | ||||
| href='/account' | href='/account' | ||||
| </Link> | </Link> | ||||
| </MenuItem> | </MenuItem> | ||||
| <MenuItem> | <MenuItem> | ||||
| <div className={classNames(itemClassName, | |||||
| <div className={cn(itemClassName, | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} onClick={() => setShowAccountSettingModal({ payload: 'members' })}> | )} onClick={() => setShowAccountSettingModal({ payload: 'members' })}> | ||||
| <RiSettings3Line className='size-4 shrink-0 text-text-tertiary' /> | <RiSettings3Line className='size-4 shrink-0 text-text-tertiary' /> | ||||
| <div className='p-1'> | <div className='p-1'> | ||||
| <MenuItem> | <MenuItem> | ||||
| <Link | <Link | ||||
| className={classNames(itemClassName, 'group justify-between', | |||||
| className={cn(itemClassName, 'group justify-between', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} | )} | ||||
| href={ | href={ | ||||
| <div className='p-1'> | <div className='p-1'> | ||||
| <MenuItem> | <MenuItem> | ||||
| <Link | <Link | ||||
| className={classNames(itemClassName, 'group justify-between', | |||||
| className={cn(itemClassName, 'group justify-between', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} | )} | ||||
| href='https://roadmap.dify.ai' | href='https://roadmap.dify.ai' | ||||
| </MenuItem> | </MenuItem> | ||||
| {systemFeatures.license.status === LicenseStatus.NONE && <MenuItem> | {systemFeatures.license.status === LicenseStatus.NONE && <MenuItem> | ||||
| <Link | <Link | ||||
| className={classNames(itemClassName, 'group justify-between', | |||||
| className={cn(itemClassName, 'group justify-between', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} | )} | ||||
| href='https://github.com/langgenius/dify' | href='https://github.com/langgenius/dify' | ||||
| { | { | ||||
| document?.body?.getAttribute('data-public-site-about') !== 'hide' && ( | document?.body?.getAttribute('data-public-site-about') !== 'hide' && ( | ||||
| <MenuItem> | <MenuItem> | ||||
| <div className={classNames(itemClassName, 'justify-between', | |||||
| <div className={cn(itemClassName, 'justify-between', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} onClick={() => setAboutVisible(true)}> | )} onClick={() => setAboutVisible(true)}> | ||||
| <RiInformation2Line className='size-4 shrink-0 text-text-tertiary' /> | <RiInformation2Line className='size-4 shrink-0 text-text-tertiary' /> | ||||
| <MenuItem> | <MenuItem> | ||||
| <div className='p-1' onClick={() => handleLogout()}> | <div className='p-1' onClick={() => handleLogout()}> | ||||
| <div | <div | ||||
| className={classNames(itemClassName, 'group justify-between', | |||||
| className={cn(itemClassName, 'group justify-between', | |||||
| 'data-[active]:bg-state-base-hover', | 'data-[active]:bg-state-base-hover', | ||||
| )} | )} | ||||
| > | > |
| import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react' | import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react' | ||||
| import { RiArrowDownSLine } from '@remixicon/react' | import { RiArrowDownSLine } from '@remixicon/react' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import PlanBadge from '@/app/components/header/plan-badge' | |||||
| import { switchWorkspace } from '@/service/common' | import { switchWorkspace } from '@/service/common' | ||||
| import { useWorkspacesContext } from '@/context/workspace-context' | import { useWorkspacesContext } from '@/context/workspace-context' | ||||
| import { ToastContext } from '@/app/components/base/toast' | import { ToastContext } from '@/app/components/base/toast' | ||||
| import PlanBadge from '../../plan-badge' | |||||
| import type { Plan } from '@/app/components/billing/type' | import type { Plan } from '@/app/components/billing/type' | ||||
| const WorkplaceSelector = () => { | const WorkplaceSelector = () => { |
| } | } | ||||
| <div className='flex shrink-0 items-center'> | <div className='flex shrink-0 items-center'> | ||||
| <EnvNav /> | <EnvNav /> | ||||
| <div className='mr-3'> | |||||
| <div className='mr-2'> | |||||
| <PluginsNav /> | <PluginsNav /> | ||||
| </div> | </div> | ||||
| <AccountDropdown isMobile={isMobile} /> | |||||
| <AccountDropdown /> | |||||
| </div> | </div> | ||||
| { | { | ||||
| (isMobile && isShowNavMenu) && ( | (isMobile && isShowNavMenu) && ( |
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { | |||||
| RiGraduationCapFill, | |||||
| } from '@remixicon/react' | |||||
| import { SparklesSoft } from '../../base/icons/src/public/common' | import { SparklesSoft } from '../../base/icons/src/public/common' | ||||
| import PremiumBadge from '../../base/premium-badge' | import PremiumBadge from '../../base/premium-badge' | ||||
| import { Plan } from '../../billing/type' | import { Plan } from '../../billing/type' | ||||
| } | } | ||||
| const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => { | const PlanBadge: FC<PlanBadgeProps> = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => { | ||||
| const { isFetchedPlan } = useProviderContext() | |||||
| const { isFetchedPlan, isEducationWorkspace } = useProviderContext() | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| if (!isFetchedPlan) return null | if (!isFetchedPlan) return null | ||||
| if (plan === Plan.professional) { | if (plan === Plan.professional) { | ||||
| return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}> | return <PremiumBadge className='select-none' size='s' color='blue' allowHover={allowHover} onClick={onClick}> | ||||
| <div className='system-2xs-medium-uppercase'> | <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 | pro | ||||
| </span> | </span> | ||||
| </div> | </div> |
| import type { ReactNode } from 'react' | import type { ReactNode } from 'react' | ||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | import { usePathname, useRouter, useSearchParams } from 'next/navigation' | ||||
| import { fetchSetupStatus } from '@/service/common' | import { fetchSetupStatus } from '@/service/common' | ||||
| import { | |||||
| EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, | |||||
| EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, | |||||
| } from '@/app/education-apply/constants' | |||||
| type SwrInitorProps = { | type SwrInitorProps = { | ||||
| children: ReactNode | children: ReactNode | ||||
| useEffect(() => { | useEffect(() => { | ||||
| (async () => { | (async () => { | ||||
| const action = searchParams.get('action') | |||||
| if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) | |||||
| localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes') | |||||
| try { | try { | ||||
| const isFinished = await isSetupFinished() | const isFinished = await isSetupFinished() | ||||
| if (!isFinished) { | if (!isFinished) { |
| export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify' | |||||
| export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying' |
| 'use client' | |||||
| import { | |||||
| useMemo, | |||||
| useState, | |||||
| } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { RiExternalLinkLine } from '@remixicon/react' | |||||
| 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/constants' | |||||
| import { getLocaleOnClient } from '@/i18n' | |||||
| const EducationApplyAge = () => { | |||||
| const { t } = useTranslation() | |||||
| const locale = getLocaleOnClient() | |||||
| 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 docLink = useMemo(() => { | |||||
| if (locale === 'zh-Hans') | |||||
| return 'https://docs.dify.ai/zh-hans/getting-started/dify-for-education' | |||||
| if (locale === 'ja-JP') | |||||
| return 'https://docs.dify.ai/ja-jp/getting-started/dify-for-education' | |||||
| return 'https://docs.dify.ai/getting-started/dify-for-education' | |||||
| }, [locale]) | |||||
| 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='fixed inset-0 z-[31] overflow-y-auto bg-background-body p-6'> | |||||
| <div className='mx-auto w-full max-w-[1408px] rounded-2xl border border-effects-highlight bg-background-default-subtle'> | |||||
| <div | |||||
| className="h-[349px] w-full overflow-hidden rounded-t-2xl bg-cover bg-center bg-no-repeat" | |||||
| style={{ | |||||
| backgroundImage: 'url(/education/bg.png)', | |||||
| }} | |||||
| > | |||||
| </div> | |||||
| <div className='mt-[-349px] flex h-[88px] items-center justify-between px-8 py-6'> | |||||
| <img | |||||
| src='/logo/logo-site-dark.png' | |||||
| alt='dify logo' | |||||
| className='h-10' | |||||
| /> | |||||
| </div> | |||||
| <div className='mx-auto max-w-[720px] px-8 pb-[180px]'> | |||||
| <div className='mb-2 flex h-[192px] flex-col justify-end pb-4 pt-3 text-text-primary-on-surface'> | |||||
| <div className='title-5xl-bold mb-2 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='system-md-semibold mb-1 flex h-6 items-center text-text-secondary'> | |||||
| {t('education.form.schoolName.title')} | |||||
| </div> | |||||
| <SearchInput | |||||
| value={schoolName} | |||||
| onChange={setSchoolName} | |||||
| /> | |||||
| </div> | |||||
| <div className='mb-7'> | |||||
| <div className='system-md-semibold mb-1 flex h-6 items-center text-text-secondary'> | |||||
| {t('education.form.schoolRole.title')} | |||||
| </div> | |||||
| <RoleSelector | |||||
| value={role} | |||||
| onChange={setRole} | |||||
| /> | |||||
| </div> | |||||
| <div className='mb-7'> | |||||
| <div className='system-md-semibold mb-1 flex h-6 items-center text-text-secondary'> | |||||
| {t('education.form.terms.title')} | |||||
| </div> | |||||
| <div className='system-md-regular mb-1 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='system-md-regular py-2 text-text-primary'> | |||||
| <div className='mb-2 flex'> | |||||
| <Checkbox | |||||
| className='mr-2 shrink-0' | |||||
| checked={ageChecked} | |||||
| onCheck={() => setAgeChecked(!ageChecked)} | |||||
| /> | |||||
| {t('education.form.terms.option.age')} | |||||
| </div> | |||||
| <div className='flex'> | |||||
| <Checkbox | |||||
| className='mr-2 shrink-0' | |||||
| 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 className='mb-4 mt-5 h-[1px] bg-gradient-to-r from-[rgba(16,24,40,0.08)]'></div> | |||||
| <a | |||||
| className='system-xs-regular flex items-center text-text-accent' | |||||
| href={docLink} | |||||
| target='_blank' | |||||
| > | |||||
| {t('education.learn')} | |||||
| <RiExternalLinkLine className='ml-1 h-3 w-3' /> | |||||
| </a> | |||||
| </div> | |||||
| </div> | |||||
| <Confirm | |||||
| isShow={!!modalShow} | |||||
| title={modalShow?.title || ''} | |||||
| content={modalShow?.desc} | |||||
| onConfirm={modalShow?.onConfirm || (() => {})} | |||||
| onCancel={modalShow?.onConfirm || (() => {})} | |||||
| /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default EducationApplyAge |
| import { | |||||
| useCallback, | |||||
| useEffect, | |||||
| useState, | |||||
| } from 'react' | |||||
| import { useDebounceFn } from 'ahooks' | |||||
| import { useSearchParams } from 'next/navigation' | |||||
| import type { SearchParams } from './types' | |||||
| import { | |||||
| EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, | |||||
| EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, | |||||
| } from './constants' | |||||
| import { useEducationAutocomplete } from '@/service/use-education' | |||||
| import { useModalContextSelector } from '@/context/modal-context' | |||||
| 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, | |||||
| } | |||||
| } | |||||
| export const useEducationInit = () => { | |||||
| const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) | |||||
| const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) | |||||
| const searchParams = useSearchParams() | |||||
| const educationVerifyAction = searchParams.get('action') | |||||
| useEffect(() => { | |||||
| if (educationVerifying === 'yes' || educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) { | |||||
| setShowAccountSettingModal({ payload: 'billing' }) | |||||
| if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) | |||||
| localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes') | |||||
| } | |||||
| }, [setShowAccountSettingModal, educationVerifying, educationVerifyAction]) | |||||
| } |
| 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='system-md-regular mr-6 flex h-5 cursor-pointer items-center text-text-primary' | |||||
| onClick={() => onChange(option.key)} | |||||
| > | |||||
| <div | |||||
| className={cn( | |||||
| 'mr-2 h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs', | |||||
| option.key === value && 'border-[5px] border-components-radio-border-checked ', | |||||
| )} | |||||
| > | |||||
| </div> | |||||
| {option.value} | |||||
| </div> | |||||
| )) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default RoleSelector |
| 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-[32]'> | |||||
| { | |||||
| !!schools.length && value && ( | |||||
| <div | |||||
| className='max-h-[330px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1' | |||||
| onScroll={handleScroll as any} | |||||
| > | |||||
| { | |||||
| schools.map((school, index) => ( | |||||
| <div | |||||
| key={index} | |||||
| className='system-md-regular flex h-8 cursor-pointer items-center truncate rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover' | |||||
| title={school} | |||||
| onClick={() => { | |||||
| onChange(school) | |||||
| setOpen(false) | |||||
| }} | |||||
| > | |||||
| {school} | |||||
| </div> | |||||
| )) | |||||
| } | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </PortalToFollowElemContent> | |||||
| </PortalToFollowElem> | |||||
| ) | |||||
| } | |||||
| export default SearchInput |
| export type SearchParams = { | |||||
| keywords?: string | |||||
| page?: number | |||||
| limit?: number | |||||
| } | |||||
| export type EducationAddParams = { | |||||
| token: string | |||||
| institution: string | |||||
| role: string | |||||
| } |
| 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 rounded-xl border-[4px] border-components-panel-on-panel-item-bg bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 pb-6 pl-6 pr-8 pt-9 shadow-shadow-shadow-5'> | |||||
| <div className='absolute left-0 top-0 flex items-center'> | |||||
| <div className='system-2xs-semibold-uppercase flex h-[22px] items-center bg-components-panel-on-panel-item-bg pl-2 pt-1 text-text-accent-light-mode-only'> | |||||
| {t('education.currentSigned')} | |||||
| </div> | |||||
| <Triangle className='h-[22px] w-4 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 |
| import React, { useEffect, useMemo, 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' | |||||
| import { getLocaleOnClient } from '@/i18n' | |||||
| 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 locale = getLocaleOnClient() | |||||
| const dialogRef = useRef<HTMLDivElement>(null) | |||||
| const [isVisible, setIsVisible] = useState(isShow) | |||||
| const docLink = useMemo(() => { | |||||
| if (locale === 'zh-Hans') | |||||
| return 'https://docs.dify.ai/zh-hans/getting-started/dify-for-education' | |||||
| if (locale === 'ja-JP') | |||||
| return 'https://docs.dify.ai/ja-jp/getting-started/dify-for-education' | |||||
| return 'https://docs.dify.ai/getting-started/dify-for-education' | |||||
| }, [locale]) | |||||
| const handleClick = () => { | |||||
| window.open(docLink, '_blank', 'noopener,noreferrer') | |||||
| } | |||||
| 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 z-[10000000] flex items-center justify-center bg-background-overlay'} | |||||
| onClick={(e) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| }} | |||||
| > | |||||
| <div ref={dialogRef} className={'relative w-full max-w-[481px] overflow-hidden'}> | |||||
| <div className='shadows-shadow-lg flex max-w-full flex-col items-start rounded-2xl border-[0.5px] border-solid border-components-panel-border bg-components-panel-bg'> | |||||
| <div className='flex flex-col items-start gap-2 self-stretch pb-4 pl-6 pr-6 pt-6'> | |||||
| <div className='title-2xl-semi-bold text-text-primary'>{title}</div> | |||||
| <div className='system-md-regular w-full text-text-tertiary'>{content}</div> | |||||
| </div> | |||||
| {email && ( | |||||
| <div className='w-full space-y-1 px-6 py-3'> | |||||
| <div className='system-sm-semibold py-1 text-text-secondary'>{t('education.emailLabel')}</div> | |||||
| <div className='system-sm-regular rounded-lg bg-components-input-bg-disabled px-3 py-2 text-components-input-text-filled-disabled'>{email}</div> | |||||
| </div> | |||||
| )} | |||||
| <div className='flex items-center justify-between gap-2 self-stretch p-6'> | |||||
| <div className='flex items-center gap-1'> | |||||
| {showLink && ( | |||||
| <> | |||||
| <a onClick={handleClick} href={docLink} target='_blank' className='system-xs-regular cursor-pointer text-text-accent'>{t('education.learn')}</a> | |||||
| <RiExternalLinkLine className='h-3 w-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) |
| ModelLoadBalancingConfigEntry, | ModelLoadBalancingConfigEntry, | ||||
| ModelProvider, | ModelProvider, | ||||
| } from '@/app/components/header/account-setting/model-provider-page/declarations' | } from '@/app/components/header/account-setting/model-provider-page/declarations' | ||||
| import { | |||||
| EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, | |||||
| } from '@/app/education-apply/constants' | |||||
| import Pricing from '@/app/components/billing/pricing' | import Pricing from '@/app/components/billing/pricing' | ||||
| import type { ModerationConfig, PromptVariable } from '@/models/debug' | import type { ModerationConfig, PromptVariable } from '@/models/debug' | ||||
| import type { | import type { | ||||
| import type { InputVar } from '@/app/components/workflow/types' | import type { InputVar } from '@/app/components/workflow/types' | ||||
| import type { UpdatePluginPayload } from '@/app/components/plugins/types' | import type { UpdatePluginPayload } from '@/app/components/plugins/types' | ||||
| import UpdatePlugin from '@/app/components/plugins/update-plugin' | import UpdatePlugin from '@/app/components/plugins/update-plugin' | ||||
| import { removeSpecificQueryParam } from '@/utils' | |||||
| export type ModalState<T> = { | export type ModalState<T> = { | ||||
| payload: T | payload: T | ||||
| const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') | const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') | ||||
| const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) | const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) | ||||
| const handleCancelAccountSettingModal = () => { | const handleCancelAccountSettingModal = () => { | ||||
| const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) | |||||
| if (educationVerifying === 'yes') | |||||
| localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) | |||||
| removeSpecificQueryParam('action') | |||||
| setShowAccountSettingModal(null) | setShowAccountSettingModal(null) | ||||
| if (showAccountSettingModal?.onCancelCallback) | if (showAccountSettingModal?.onCancelCallback) | ||||
| showAccountSettingModal?.onCancelCallback() | showAccountSettingModal?.onCancelCallback() |
| import { parseCurrentPlan } from '@/app/components/billing/utils' | import { parseCurrentPlan } from '@/app/components/billing/utils' | ||||
| import { defaultPlan } from '@/app/components/billing/config' | import { defaultPlan } from '@/app/components/billing/config' | ||||
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import { | |||||
| useEducationStatus, | |||||
| } from '@/service/use-education' | |||||
| type ProviderContextState = { | type ProviderContextState = { | ||||
| modelProviders: ModelProvider[] | modelProviders: ModelProvider[] | ||||
| enableReplaceWebAppLogo: boolean | enableReplaceWebAppLogo: boolean | ||||
| modelLoadBalancingEnabled: boolean | modelLoadBalancingEnabled: boolean | ||||
| datasetOperatorEnabled: boolean | datasetOperatorEnabled: boolean | ||||
| enableEducationPlan: boolean | |||||
| isEducationWorkspace: boolean | |||||
| isEducationAccount: boolean | |||||
| } | } | ||||
| const ProviderContext = createContext<ProviderContextState>({ | const ProviderContext = createContext<ProviderContextState>({ | ||||
| modelProviders: [], | modelProviders: [], | ||||
| enableReplaceWebAppLogo: false, | enableReplaceWebAppLogo: false, | ||||
| modelLoadBalancingEnabled: false, | modelLoadBalancingEnabled: false, | ||||
| datasetOperatorEnabled: false, | datasetOperatorEnabled: false, | ||||
| enableEducationPlan: false, | |||||
| isEducationWorkspace: false, | |||||
| isEducationAccount: false, | |||||
| }) | }) | ||||
| export const useProviderContext = () => useContext(ProviderContext) | export const useProviderContext = () => useContext(ProviderContext) | ||||
| const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) | const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) | ||||
| const [datasetOperatorEnabled, setDatasetOperatorEnabled] = 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 fetchPlan = async () => { | ||||
| const data = await fetchCurrentPlanInfo() | const data = await fetchCurrentPlanInfo() | ||||
| const enabled = data.billing.enabled | const enabled = data.billing.enabled | ||||
| setEnableBilling(enabled) | setEnableBilling(enabled) | ||||
| setEnableEducationPlan(data.education.enabled) | |||||
| setIsEducationWorkspace(data.education.activated) | |||||
| setEnableReplaceWebAppLogo(data.can_replace_logo) | setEnableReplaceWebAppLogo(data.can_replace_logo) | ||||
| if (enabled) { | if (enabled) { | ||||
| setPlan(parseCurrentPlan(data)) | |||||
| setPlan(parseCurrentPlan(data) as any) | |||||
| setIsFetchedPlan(true) | setIsFetchedPlan(true) | ||||
| } | } | ||||
| if (data.model_load_balancing_enabled) | if (data.model_load_balancing_enabled) | ||||
| enableReplaceWebAppLogo, | enableReplaceWebAppLogo, | ||||
| modelLoadBalancingEnabled, | modelLoadBalancingEnabled, | ||||
| datasetOperatorEnabled, | datasetOperatorEnabled, | ||||
| enableEducationPlan, | |||||
| isEducationWorkspace, | |||||
| isEducationAccount: isEducationAccount?.result || false, | |||||
| }}> | }}> | ||||
| {children} | {children} | ||||
| </ProviderContext.Provider> | </ProviderContext.Provider> |
| 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 Educational Verification 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 |
| import { LanguagesSupported } from '@/i18n/language' | 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) => ({ | const loadLangResources = (lang: string) => ({ | ||||
| translation: { | translation: { | ||||
| common: require(`./${lang}/common`).default, | common: require(`./${lang}/common`).default, | ||||
| plugin: require(`./${lang}/plugin`).default, | plugin: require(`./${lang}/plugin`).default, | ||||
| pluginTags: require(`./${lang}/plugin-tags`).default, | pluginTags: require(`./${lang}/plugin-tags`).default, | ||||
| time: require(`./${lang}/time`).default, | time: require(`./${lang}/time`).default, | ||||
| education: requireSilent(lang), | |||||
| }, | }, | ||||
| }) | }) | ||||
| 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 |
| 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 |
| import { get, post } from './base' | |||||
| import { | |||||
| useMutation, | |||||
| useQuery, | |||||
| } from '@tanstack/react-query' | |||||
| import { useInvalid } from './use-base' | |||||
| import type { EducationAddParams } from '@/app/education-apply/types' | |||||
| const NAME_SPACE = 'education' | |||||
| export const useEducationVerify = () => { | |||||
| return useMutation({ | |||||
| mutationKey: [NAME_SPACE, 'education-verify'], | |||||
| mutationFn: () => { | |||||
| return get<{ token: string }>('/account/education/verify', {}, { silent: true }) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| 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']) | |||||
| } |
| || providerId === `langgenius/${oldToolId}/${oldToolId}` | || providerId === `langgenius/${oldToolId}/${oldToolId}` | ||||
| || providerId === `langgenius/${oldToolId}_tool/${oldToolId}` | || providerId === `langgenius/${oldToolId}_tool/${oldToolId}` | ||||
| } | } | ||||
| export const removeSpecificQueryParam = (key: string | string[]) => { | |||||
| const url = new URL(window.location.href) | |||||
| if (Array.isArray(key)) | |||||
| key.forEach(k => url.searchParams.delete(k)) | |||||
| else | |||||
| url.searchParams.delete(key) | |||||
| window.history.replaceState(null, '', url.toString()) | |||||
| } |