From ddf05ca05945396bcffc8e1ff29045fff8a2437d Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 20 Aug 2025 15:37:46 +0800 Subject: [PATCH] feat: notice of the expire of education verify (#24210) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../(commonLayout)/education-apply/page.tsx | 6 +- .../public/common/sparkles-soft-accent.svg | 4 + .../src/public/common/SparklesSoftAccent.json | 36 ++++++ .../src/public/common/SparklesSoftAccent.tsx | 20 ++++ .../base/icons/src/public/common/index.ts | 1 + web/app/components/billing/plan/index.tsx | 5 +- .../header/account-dropdown/index.tsx | 5 + web/app/education-apply/constants.ts | 2 + .../education-apply/expire-notice-modal.tsx | 96 ++++++++++++++++ web/app/education-apply/hooks.ts | 105 +++++++++++++++++- web/context/modal-context.tsx | 18 ++- web/context/provider-context.tsx | 16 ++- web/i18n/de-DE/app-log.ts | 1 + web/i18n/en-US/app-log.ts | 1 + web/i18n/en-US/education.ts | 30 +++++ web/i18n/ja-JP/app-log.ts | 1 + web/i18n/ja-JP/education.ts | 30 +++++ web/i18n/zh-Hans/app-log.ts | 1 + web/i18n/zh-Hans/education.ts | 30 +++++ web/service/use-education.ts | 3 +- 20 files changed, 400 insertions(+), 11 deletions(-) create mode 100644 web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg create mode 100644 web/app/components/base/icons/src/public/common/SparklesSoftAccent.json create mode 100644 web/app/components/base/icons/src/public/common/SparklesSoftAccent.tsx create mode 100644 web/app/education-apply/expire-notice-modal.tsx diff --git a/web/app/(commonLayout)/education-apply/page.tsx b/web/app/(commonLayout)/education-apply/page.tsx index 873034452..5dd3c3551 100644 --- a/web/app/(commonLayout)/education-apply/page.tsx +++ b/web/app/(commonLayout)/education-apply/page.tsx @@ -13,12 +13,12 @@ import { useProviderContext } from '@/context/provider-context' export default function EducationApply() { const router = useRouter() - const { enableEducationPlan, isEducationAccount } = useProviderContext() + const { enableEducationPlan } = useProviderContext() const searchParams = useSearchParams() const token = searchParams.get('token') const showEducationApplyPage = useMemo(() => { - return enableEducationPlan && !isEducationAccount && token - }, [enableEducationPlan, isEducationAccount, token]) + return enableEducationPlan && token + }, [enableEducationPlan, token]) useEffect(() => { if (!showEducationApplyPage) diff --git a/web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg b/web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg new file mode 100644 index 000000000..344d2f7c2 --- /dev/null +++ b/web/app/components/base/icons/assets/public/common/sparkles-soft-accent.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/app/components/base/icons/src/public/common/SparklesSoftAccent.json b/web/app/components/base/icons/src/public/common/SparklesSoftAccent.json new file mode 100644 index 000000000..3c540faf4 --- /dev/null +++ b/web/app/components/base/icons/src/public/common/SparklesSoftAccent.json @@ -0,0 +1,36 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "opacity": "0.5", + "d": "M12.5674 1.56341C12.5532 1.43246 12.4469 1.33346 12.3203 1.33333C12.1938 1.3332 12.0873 1.43196 12.0728 1.56288C12.0053 2.1724 11.8316 2.59056 11.5593 2.87418C11.287 3.1578 10.8856 3.33881 10.3004 3.40911C10.1747 3.42421 10.08 3.53514 10.0801 3.66693C10.0802 3.79872 10.1752 3.90944 10.3009 3.92427C10.8762 3.99215 11.2868 4.17312 11.566 4.45869C11.8437 4.74271 12.0207 5.16027 12.0721 5.76368C12.0836 5.89756 12.1913 6.00015 12.3203 6C12.4494 5.99984 12.5569 5.897 12.568 5.7631C12.6174 5.16988 12.7943 4.74291 13.0737 4.45176C13.3533 4.1606 13.7632 3.9763 14.3326 3.92496C14.4612 3.91336 14.56 3.80136 14.5601 3.66696C14.5602 3.53255 14.4617 3.42032 14.3332 3.40842C13.7539 3.35482 13.3531 3.17038 13.0804 2.88113C12.8063 2.5903 12.6325 2.16262 12.5674 1.56341Z", + "fill": "#155AEF" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M8.15567 3.25831C8.11906 2.92157 7.84578 2.66702 7.52041 2.66667C7.19509 2.66633 6.92124 2.92029 6.88399 3.25695C6.71042 4.8243 6.2636 5.89953 5.56346 6.62885C4.86332 7.35814 3.83109 7.82361 2.32643 8.00441C2.00323 8.04321 1.75943 8.32847 1.75977 8.66734C1.7601 9.00627 2.00446 9.29094 2.32773 9.32907C3.80694 9.50361 4.86268 9.96901 5.58062 10.7033C6.29465 11.4337 6.74997 12.5073 6.88226 14.059C6.91164 14.4033 7.18869 14.6671 7.52047 14.6667C7.85231 14.6663 8.12879 14.4018 8.1574 14.0575C8.28412 12.5321 8.73909 11.4342 9.45781 10.6855C10.1766 9.93681 11.2305 9.46287 12.6949 9.33087C13.0255 9.30107 13.2794 9.01307 13.2798 8.66741C13.2801 8.32181 13.0269 8.03321 12.6964 8.00261C11.2068 7.86481 10.1761 7.39054 9.47497 6.64673C8.77001 5.89887 8.32322 4.79915 8.15567 3.25831Z", + "fill": "#155AEF" + }, + "children": [] + } + ] + }, + "name": "SparklesSoftAccent" +} diff --git a/web/app/components/base/icons/src/public/common/SparklesSoftAccent.tsx b/web/app/components/base/icons/src/public/common/SparklesSoftAccent.tsx new file mode 100644 index 000000000..a2bbc73b7 --- /dev/null +++ b/web/app/components/base/icons/src/public/common/SparklesSoftAccent.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './SparklesSoftAccent.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconData } from '@/app/components/base/icons/IconBase' + +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => + +Icon.displayName = 'SparklesSoftAccent' + +export default Icon diff --git a/web/app/components/base/icons/src/public/common/index.ts b/web/app/components/base/icons/src/public/common/index.ts index dba789a7a..e672e5261 100644 --- a/web/app/components/base/icons/src/public/common/index.ts +++ b/web/app/components/base/icons/src/public/common/index.ts @@ -12,4 +12,5 @@ export { default as MultiPathRetrieval } from './MultiPathRetrieval' export { default as NTo1Retrieval } from './NTo1Retrieval' export { default as Notion } from './Notion' export { default as Soc2 } from './Soc2' +export { default as SparklesSoftAccent } from './SparklesSoftAccent' export { default as SparklesSoft } from './SparklesSoft' diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx index 7badb3666..cebcf8212 100644 --- a/web/app/components/billing/plan/index.tsx +++ b/web/app/components/billing/plan/index.tsx @@ -35,7 +35,8 @@ const PlanComp: FC = ({ const { t } = useTranslation() const router = useRouter() const { userProfile } = useAppContext() - const { plan, enableEducationPlan, isEducationAccount } = useProviderContext() + const { plan, enableEducationPlan, allowRefreshEducationVerify, isEducationAccount } = useProviderContext() + const isAboutToExpire = allowRefreshEducationVerify const { type, } = plan @@ -81,7 +82,7 @@ const PlanComp: FC = ({
{t(`billing.plans.${type}.for`)}
- {enableEducationPlan && !isEducationAccount && ( + {enableEducationPlan && (!isEducationAccount || isAboutToExpire) && ( + ) : ( + + )} + +
+ + + ) +} + +export default React.memo(ExpireNoticeModal) diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts index 01fb36c7f..474b35516 100644 --- a/web/app/education-apply/hooks.ts +++ b/web/app/education-apply/hooks.ts @@ -3,16 +3,26 @@ import { useEffect, useState, } from 'react' -import { useDebounceFn } from 'ahooks' +import { useDebounceFn, useLocalStorageState } from 'ahooks' import { useSearchParams } from 'next/navigation' import type { SearchParams } from './types' import { + EDUCATION_PRICING_SHOW_ACTION, + EDUCATION_RE_VERIFY_ACTION, EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, } from './constants' -import { useEducationAutocomplete } from '@/service/use-education' +import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education' import { useModalContextSelector } from '@/context/modal-context' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import timezone from 'dayjs/plugin/timezone' +import { useAppContext } from '@/context/app-context' +import { useRouter } from 'next/navigation' +import { useProviderContext } from '@/context/provider-context' +dayjs.extend(utc) +dayjs.extend(timezone) export const useEducation = () => { const { mutateAsync, @@ -50,12 +60,99 @@ export const useEducation = () => { } } +type useEducationReverifyNoticeParams = { + onNotice: ({ + expireAt, + expired, + }: { + expireAt: number + expired: boolean + }) => void +} + +const isExpired = (expireAt?: number, timezone?: string) => { + if (!expireAt || !timezone) + return false + const today = dayjs().tz(timezone).startOf('day') + const expiredDay = dayjs.unix(expireAt).tz(timezone).startOf('day') + return today.isSame(expiredDay) || today.isAfter(expiredDay) +} + +const useEducationReverifyNotice = ({ + onNotice, +}: useEducationReverifyNoticeParams) => { + const { userProfile: { timezone } } = useAppContext() + // const [educationInfo, setEducationInfo] = useState<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null } | null>(null) + // const isLoading = !educationInfo + const { educationAccountExpireAt, allowRefreshEducationVerify, isLoadingEducationAccountInfo: isLoading } = useProviderContext() + const [prevExpireAt, setPrevExpireAt] = useLocalStorageState('education-reverify-prev-expire-at', { + defaultValue: 0, + }) + const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorageState('education-reverify-has-noticed', { + defaultValue: false, + }) + const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorageState('education-expired-has-noticed', { + defaultValue: false, + }) + + useEffect(() => { + if (isLoading || !timezone) + return + if (allowRefreshEducationVerify) { + const expired = isExpired(educationAccountExpireAt!, timezone) + const isExpireAtChanged = prevExpireAt !== educationAccountExpireAt + if (isExpireAtChanged) { + setPrevExpireAt(educationAccountExpireAt!) + setReverifyHasNoticed(false) + setExpiredHasNoticed(false) + } + const shouldNotice = (() => { + if (isExpireAtChanged) + return true + return expired ? !expiredHasNoticed : !reverifyHasNoticed + })() + if (shouldNotice) { + onNotice({ + expireAt: educationAccountExpireAt!, + expired, + }) + if (expired) + setExpiredHasNoticed(true) + else + setReverifyHasNoticed(true) + } + } + }, [allowRefreshEducationVerify, timezone]) + + return { + isLoading, + expireAt: educationAccountExpireAt!, + expired: isExpired(educationAccountExpireAt!, timezone), + } +} + export const useEducationInit = () => { const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal) + const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal) + const setShowEducationExpireNoticeModal = useModalContextSelector(s => s.setShowEducationExpireNoticeModal) const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) const searchParams = useSearchParams() const educationVerifyAction = searchParams.get('action') + useEducationReverifyNotice({ + onNotice: (payload) => { + setShowEducationExpireNoticeModal({ payload }) + }, + }) + + const router = useRouter() + const { mutateAsync } = useEducationVerify() + const handleVerify = async () => { + const { token } = await mutateAsync() + if (token) + router.push(`/education-apply?token=${token}`) + } + useEffect(() => { if (educationVerifying === 'yes' || educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) { setShowAccountSettingModal({ payload: 'billing' }) @@ -63,5 +160,9 @@ export const useEducationInit = () => { if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes') } + if (educationVerifyAction === EDUCATION_PRICING_SHOW_ACTION) + setShowPricingModal() + if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION) + handleVerify() }, [setShowAccountSettingModal, educationVerifying, educationVerifyAction]) } diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index f6425ec11..f1e5bb044 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -26,6 +26,7 @@ import type { UpdatePluginPayload } from '@/app/components/plugins/types' import { removeSpecificQueryParam } from '@/utils' import { noop } from 'lodash-es' import dynamic from 'next/dynamic' +import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal' const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), { ssr: false, @@ -64,6 +65,10 @@ const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugi ssr: false, }) +const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), { + ssr: false, +}) + export type ModalState = { payload: T onCancelCallback?: () => void @@ -102,6 +107,7 @@ export type ModalContextState = { onAutoAddPromptVariable?: (variable: PromptVariable[]) => void }> | null>> setShowUpdatePluginModal: Dispatch | null>> + setShowEducationExpireNoticeModal: Dispatch | null>> } const ModalContext = createContext({ setShowAccountSettingModal: noop, @@ -116,6 +122,7 @@ const ModalContext = createContext({ setShowModelLoadBalancingEntryModal: noop, setShowOpeningModal: noop, setShowUpdatePluginModal: noop, + setShowEducationExpireNoticeModal: noop, }) export const useModalContext = () => useContext(ModalContext) @@ -145,6 +152,7 @@ export const ModalContextProvider = ({ onAutoAddPromptVariable?: (variable: PromptVariable[]) => void }> | null>(null) const [showUpdatePluginModal, setShowUpdatePluginModal] = useState | null>(null) + const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState | null>(null) const searchParams = useSearchParams() const router = useRouter() @@ -272,6 +280,7 @@ export const ModalContextProvider = ({ setShowModelLoadBalancingEntryModal, setShowOpeningModal, setShowUpdatePluginModal, + setShowEducationExpireNoticeModal, }}> <> {children} @@ -318,7 +327,7 @@ export const ModalContextProvider = ({ { if (searchParams.get('show-pricing') === '1') router.push(location.pathname, { forceOptimisticNavigation: true } as any) - + removeSpecificQueryParam('action') setShowPricingModal(false) }} /> ) @@ -398,6 +407,13 @@ export const ModalContextProvider = ({ /> ) } + { + !!showEducationExpireNoticeModal && ( + setShowEducationExpireNoticeModal(null)} + /> + )} ) diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 70917f2cf..e4397d49e 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -48,6 +48,10 @@ type ProviderContextState = { enableEducationPlan: boolean isEducationWorkspace: boolean isEducationAccount: boolean + allowRefreshEducationVerify: boolean + educationAccountExpireAt: number | null + isLoadingEducationAccountInfo: boolean + isFetchingEducationAccountInfo: boolean webappCopyrightEnabled: boolean licenseLimit: { workspace_members: { @@ -90,6 +94,10 @@ const ProviderContext = createContext({ enableEducationPlan: false, isEducationWorkspace: false, isEducationAccount: false, + allowRefreshEducationVerify: false, + educationAccountExpireAt: null, + isLoadingEducationAccountInfo: false, + isFetchingEducationAccountInfo: false, webappCopyrightEnabled: false, licenseLimit: { workspace_members: { @@ -135,7 +143,7 @@ export const ProviderContextProvider = ({ const [enableEducationPlan, setEnableEducationPlan] = useState(false) const [isEducationWorkspace, setIsEducationWorkspace] = useState(false) - const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan) + const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan) const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false) const fetchPlan = async () => { @@ -223,7 +231,11 @@ export const ProviderContextProvider = ({ datasetOperatorEnabled, enableEducationPlan, isEducationWorkspace, - isEducationAccount: isEducationAccount?.result || false, + isEducationAccount: educationAccountInfo?.is_student || false, + allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false, + educationAccountExpireAt: educationAccountInfo?.expire_at || null, + isLoadingEducationAccountInfo, + isFetchingEducationAccountInfo, webappCopyrightEnabled, licenseLimit, refreshLicenseLimit: fetchPlan, diff --git a/web/i18n/de-DE/app-log.ts b/web/i18n/de-DE/app-log.ts index a73936044..2cbde8fdd 100644 --- a/web/i18n/de-DE/app-log.ts +++ b/web/i18n/de-DE/app-log.ts @@ -2,6 +2,7 @@ const translation = { title: 'Protokolle', description: 'Die Protokolle zeichnen den Betriebsstatus der Anwendung auf, einschließlich Benutzereingaben und KI-Antworten.', dateTimeFormat: 'MM/DD/YYYY hh:mm A', + dateFormat: 'MM/DD/YYYY', table: { header: { updatedTime: 'Aktualisierungszeit', diff --git a/web/i18n/en-US/app-log.ts b/web/i18n/en-US/app-log.ts index 0d1234050..c4fb7dada 100644 --- a/web/i18n/en-US/app-log.ts +++ b/web/i18n/en-US/app-log.ts @@ -2,6 +2,7 @@ const translation = { title: 'Logs', description: 'The logs record the running status of the application, including user inputs and AI replies.', dateTimeFormat: 'MM/DD/YYYY hh:mm A', + dateFormat: 'MM/DD/YYYY', table: { header: { updatedTime: 'Updated time', diff --git a/web/i18n/en-US/education.ts b/web/i18n/en-US/education.ts index dd7fedc10..fd15a6d4b 100644 --- a/web/i18n/en-US/education.ts +++ b/web/i18n/en-US/education.ts @@ -42,6 +42,36 @@ const translation = { rejectTitle: 'Your Dify Educational Verification Has Been Rejected', rejectContent: 'Unfortunately, you are not eligible for Education Verified status and therefore cannot receive the exclusive 100% coupon for the Dify Professional Plan if you use this email address.', emailLabel: 'Your current email', + notice: { + dateFormat: 'MM/DD/YYYY', + expired: { + title: 'Your education status has expired', + summary: { + line1: 'You can still access and use Dify. ', + line2: 'However, you\'re no longer eligible for new education discount coupons.', + }, + + }, + isAboutToExpire: { + title: 'Your education status will expire on {{date}}', + summary: 'Don\'t worry — this won\'t affect your current subscription, but you won\'t get the education discount when it renews unless you verify your status again.', + }, + stillInEducation: { + title: 'Still in education?', + expired: 'Re-verify now to get a new coupon for the upcoming academic year. We\'ll add it to your account and you can use it for the next upgrade.', + isAboutToExpire: 'Re-verify now to get a new coupon for the upcoming academic year. It\'ll be saved to your account and ready to use at your next renewal.', + }, + alreadyGraduated: { + title: 'Already graduated?', + expired: 'Feel free to upgrade anytime to get full access to paid features.', + isAboutToExpire: 'Your current subscription will still remain active. When it ends, you\'ll be moved to the Sandbox plan, or you can upgrade anytime to restore full access to paid features.', + }, + action: { + dismiss: 'Dismiss', + upgrade: 'Upgrade', + reVerify: 'Re-verify', + }, + }, } export default translation diff --git a/web/i18n/ja-JP/app-log.ts b/web/i18n/ja-JP/app-log.ts index dd4e3778d..b64b5af3e 100644 --- a/web/i18n/ja-JP/app-log.ts +++ b/web/i18n/ja-JP/app-log.ts @@ -2,6 +2,7 @@ const translation = { title: 'ログ', description: 'ログは、アプリケーションの実行状態を記録します。ユーザーの入力や AI の応答などが含まれます。', dateTimeFormat: 'YYYY/MM/DD hh:mm A', + dateFormat: 'YYYY/MM/DD', table: { header: { updatedTime: '更新時間', diff --git a/web/i18n/ja-JP/education.ts b/web/i18n/ja-JP/education.ts index 20544b1de..a028fbff1 100644 --- a/web/i18n/ja-JP/education.ts +++ b/web/i18n/ja-JP/education.ts @@ -42,6 +42,36 @@ const translation = { rejectTitle: 'Dify 教育認証が拒否されました', rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Dify プロフェッショナルプランの 100%割引クーポン を受け取ることはできません。', emailLabel: '現在のメールアドレス', + notice: { + dateFormat: 'YYYY/MM/DD', + expired: { + title: 'あなたの教育認証は失効しました', + summary: { + line1: 'Dify は引き続きご利用いただけますが、新しい教育割引クーポンの対象外となります。', + line2: '', + }, + + }, + isAboutToExpire: { + title: 'あなたの教育認証は {{date}} に有効期限を迎えます', + summary: 'ご安心ください。現在のサブスクリプションには影響ありません。ただし、再認証を行わない場合、次回の更新時に教育割引を受けることができません。', + }, + stillInEducation: { + title: 'まだ在学中ですか?', + expired: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンはあなたのアカウントに追加され、次回のアップグレード時にご利用いただけます。', + isAboutToExpire: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンは個人のアカウントに保存され、次回の更新時に使用できます。', + }, + alreadyGraduated: { + title: 'すでに卒業しましたか?', + expired: 'いつでもアップグレードして、すべての有料機能にアクセスすることができます。', + isAboutToExpire: '今すぐ再認証して、次の学年度向けの教育クーポンを取得してください。クーポンはあなたのアカウントに追加され、次回のアップグレード時にご利用いただけます。', + }, + action: { + dismiss: '無視', + upgrade: 'アップグレード', + reVerify: '再認証する', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index 4c1815787..8f55bd2e9 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/web/i18n/zh-Hans/app-log.ts @@ -2,6 +2,7 @@ const translation = { title: '日志', description: '日志记录了应用的运行情况,包括用户的输入和 AI 的回复。', dateTimeFormat: 'YYYY-MM-DD HH:mm', + dateFormat: 'YYYY-MM-DD', table: { header: { updatedTime: '更新时间', diff --git a/web/i18n/zh-Hans/education.ts b/web/i18n/zh-Hans/education.ts index 9d2769834..81b5462fe 100644 --- a/web/i18n/zh-Hans/education.ts +++ b/web/i18n/zh-Hans/education.ts @@ -42,6 +42,36 @@ const translation = { rejectTitle: '您的 Dify 教育版认证已被拒绝', rejectContent: '非常遗憾,您无法使用此电子邮件以获得教育版认证资格,也无法领取 Dify Professional 版的 100% 独家优惠券。', emailLabel: '您当前的邮箱', + notice: { + dateFormat: 'YYYY/MM/DD', + expired: { + title: '您的教育认证已过期', + summary: { + line1: '您仍可继续使用 Dify,但将无法再领取新的教育优惠券。', + line2: '', + }, + + }, + isAboutToExpire: { + title: '您的教育认证将于 {{date}} 过期', + summary: '别担心,这不会影响您当前的订阅。但续订时您将无法继续享受教育优惠,除非重新完成身份验证。', + }, + stillInEducation: { + title: '仍在就读?', + expired: '立即重新认证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次升级时使用。', + isAboutToExpire: '立即重新验证,获取新学年的教育优惠券。优惠券将发放至您的账户,并可在下次续订时使用。', + }, + alreadyGraduated: { + title: '已毕业?', + expired: '您可以随时升级以获得所有付费功能。', + isAboutToExpire: '您的当前订阅仍将保持有效。订阅结束后,空间将切换为 Sandbox 套餐,您也可以随时升级,恢复全部付费功能的使用。', + }, + action: { + dismiss: '忽略', + upgrade: '升级套餐', + reVerify: '重新认证', + }, + }, } export default translation diff --git a/web/service/use-education.ts b/web/service/use-education.ts index 4405db747..c71833c06 100644 --- a/web/service/use-education.ts +++ b/web/service/use-education.ts @@ -56,9 +56,10 @@ export const useEducationStatus = (disable?: boolean) => { enabled: !disable, queryKey: [NAME_SPACE, 'education-status'], queryFn: () => { - return get<{ result: boolean }>('/account/education') + return get<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null }>('/account/education') }, retry: false, + gcTime: 0, // No cache. Prevent switch account caused stale data }) }