Merge branch 'main' into feat/rag-2

This commit is contained in:
twwu
2025-08-21 09:43:51 +08:00
131 changed files with 3855 additions and 1081 deletions

View File

@ -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)

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path 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"/>
<path 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"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -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"
}

View File

@ -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<SVGSVGElement> & {
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>;
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'SparklesSoftAccent'
export default Icon

View File

@ -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'

View File

@ -33,7 +33,8 @@ const PlanComp: FC<Props> = ({
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
@ -79,7 +80,7 @@ const PlanComp: FC<Props> = ({
<div className='system-xs-regular text-util-colors-gray-gray-600'>{t(`billing.plans.${type}.for`)}</div>
</div>
<div className='flex shrink-0 items-center gap-1'>
{enableEducationPlan && !isEducationAccount && (
{enableEducationPlan && (!isEducationAccount || isAboutToExpire) && (
<Button variant='ghost' onClick={handleVerify}>
<RiGraduationCapLine className='mr-1 h-4 w-4' />
{t('education.toVerified')}

View File

@ -59,6 +59,11 @@ export default function AppSelector() {
localStorage.removeItem('console_token')
localStorage.removeItem('refresh_token')
// To avoid use other account's education notice info
localStorage.removeItem('education-reverify-prev-expire-at')
localStorage.removeItem('education-reverify-has-noticed')
localStorage.removeItem('education-expired-has-noticed')
router.push('/signin')
}

View File

@ -1,2 +1,4 @@
export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify'
export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
export const EDUCATION_PRICING_SHOW_ACTION = 'educationPricing'
export const EDUCATION_RE_VERIFY_ACTION = 'educationReVerify'

View File

@ -0,0 +1,96 @@
'use client'
import React from 'react'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n'
import Link from 'next/link'
import { useTranslation } from 'react-i18next'
import { RiExternalLinkLine } from '@remixicon/react'
import { SparklesSoftAccent } from '../components/base/icons/src/public/common'
import useTimestamp from '@/hooks/use-timestamp'
import { useModalContextSelector } from '@/context/modal-context'
import { useEducationVerify } from '@/service/use-education'
import { useRouter } from 'next/navigation'
export type ExpireNoticeModalPayloadProps = {
expireAt: number
expired: boolean
}
export type Props = {
onClose: () => void
} & ExpireNoticeModalPayloadProps
const i18nPrefix = 'education.notice'
const ExpireNoticeModal: React.FC<Props> = ({ expireAt, expired, onClose }) => {
const { t } = useTranslation()
const docLink = useDocLink()
const eduDocLink = docLink('/getting-started/dify-for-education')
const { formatTime } = useTimestamp()
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
const { mutateAsync } = useEducationVerify()
const router = useRouter()
const handleVerify = async () => {
const { token } = await mutateAsync()
if (token)
router.push(`/education-apply?token=${token}`)
}
const handleConfirm = async () => {
await handleVerify()
onClose()
}
return (
<Modal
isShow
onClose={onClose}
title={expired ? t(`${i18nPrefix}.expired.title`) : t(`${i18nPrefix}.isAboutToExpire.title`, { date: formatTime(expireAt, t(`${i18nPrefix}.dateFormat`) as string), interpolation: { escapeValue: false } })}
closable
className='max-w-[600px]'
>
<div className='body-md-regular mt-5 space-y-5 text-text-secondary'>
<div>
{expired ? (<>
<div>{t(`${i18nPrefix}.expired.summary.line1`)}</div>
<div>{t(`${i18nPrefix}.expired.summary.line2`)}</div>
</>
) : t(`${i18nPrefix}.isAboutToExpire.summary`)}
</div>
<div>
<strong className='title-md-semi-bold block'>{t(`${i18nPrefix}.stillInEducation.title`)}</strong>
{t(`${i18nPrefix}.stillInEducation.${expired ? 'expired' : 'isAboutToExpire'}`)}
</div>
<div>
<strong className='title-md-semi-bold block'>{t(`${i18nPrefix}.alreadyGraduated.title`)}</strong>
{t(`${i18nPrefix}.alreadyGraduated.${expired ? 'expired' : 'isAboutToExpire'}`)}
</div>
</div>
<div className="mt-7 flex items-center justify-between space-x-2">
<Link className='system-xs-regular flex items-center space-x-1 text-text-accent' href={eduDocLink} target="_blank" rel="noopener noreferrer">
<div>{t('education.learn')}</div>
<RiExternalLinkLine className='size-3' />
</Link>
<div className='flex space-x-2'>
{expired ? (
<Button onClick={() => {
onClose()
setShowPricingModal()
}} className='flex items-center space-x-1'>
<SparklesSoftAccent className='size-4' />
<div className='text-components-button-secondary-accent-text'>{t(`${i18nPrefix}.action.upgrade`)}</div>
</Button>
) : (
<Button onClick={onClose}>
{t(`${i18nPrefix}.action.dismiss`)}
</Button>
)}
<Button variant='primary' onClick={handleConfirm}>
{t(`${i18nPrefix}.action.reVerify`)}
</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(ExpireNoticeModal)

View File

@ -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<number | undefined>('education-reverify-prev-expire-at', {
defaultValue: 0,
})
const [reverifyHasNoticed, setReverifyHasNoticed] = useLocalStorageState<boolean | undefined>('education-reverify-has-noticed', {
defaultValue: false,
})
const [expiredHasNoticed, setExpiredHasNoticed] = useLocalStorageState<boolean | undefined>('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])
}

View File

@ -10,7 +10,7 @@ export default function RoutePrefixHandle() {
const addPrefixToImg = (e: HTMLImageElement) => {
const url = new URL(e.src)
const prefix = url.pathname.slice(0, basePath.length)
if (prefix !== basePath && !url.href.startsWith('blob:') && !url.href.startsWith('data:')) {
if (prefix !== basePath && !url.href.startsWith('blob:') && !url.href.startsWith('data:') && !url.href.startsWith('http')) {
url.pathname = basePath + url.pathname
e.src = url.toString()
}