diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml
index cc735ae67c..851621ee49 100644
--- a/.github/workflows/build-push.yml
+++ b/.github/workflows/build-push.yml
@@ -6,6 +6,7 @@ on:
- "main"
- "deploy/dev"
- "deploy/enterprise"
+ - release/1.1.3-fix1
tags:
- "*"
diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx
index be5031ab43..d98851c4e9 100644
--- a/web/app/(commonLayout)/apps/Apps.tsx
+++ b/web/app/(commonLayout)/apps/Apps.tsx
@@ -1,7 +1,9 @@
'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
-import { useRouter } from 'next/navigation'
+import {
+ useRouter,
+} from 'next/navigation'
import useSWRInfinite from 'swr/infinite'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
diff --git a/web/app/(commonLayout)/apps/page.tsx b/web/app/(commonLayout)/apps/page.tsx
index 85fe433446..4a146d9b65 100644
--- a/web/app/(commonLayout)/apps/page.tsx
+++ b/web/app/(commonLayout)/apps/page.tsx
@@ -7,9 +7,12 @@ import style from '../list.module.css'
import Apps from './Apps'
import AppContext from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
+import { useEducationInit } from '@/app/education-apply/hooks'
const AppList = () => {
const { t } = useTranslation()
+ useEducationInit()
+
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
return (
diff --git a/web/app/(commonLayout)/education-apply/page.tsx b/web/app/(commonLayout)/education-apply/page.tsx
new file mode 100644
index 0000000000..873034452e
--- /dev/null
+++ b/web/app/(commonLayout)/education-apply/page.tsx
@@ -0,0 +1,29 @@
+'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
+}
diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx
index 6176fe58af..d09a8c2cfe 100644
--- a/web/app/account/account-page/index.tsx
+++ b/web/app/account/account-page/index.tsx
@@ -1,7 +1,9 @@
'use client'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
-
+import {
+ RiGraduationCapFill,
+} from '@remixicon/react'
import { useContext } from 'use-context-selector'
import DeleteAccount from '../delete-account'
import s from './index.module.css'
@@ -12,10 +14,12 @@ import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { updateUserProfile } from '@/service/common'
import { useAppContext } from '@/context/app-context'
+import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
+import PremiumBadge from '@/app/components/base/premium-badge'
const titleClassName = `
system-sm-semibold text-text-secondary
@@ -30,6 +34,7 @@ export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useAppContext()
const { mutateUserProfile, userProfile, apps } = useAppContext()
+ const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
const [editName, setEditName] = useState('')
@@ -135,7 +140,15 @@ export default function AccountPage() {
-
{userProfile.name}
+
+ {userProfile.name}
+ {isEducationAccount && (
+
+
+ EDU
+
+ )}
+
{userProfile.email}
diff --git a/web/app/account/avatar.tsx b/web/app/account/avatar.tsx
index e37d15c6ae..63db0f37dc 100644
--- a/web/app/account/avatar.tsx
+++ b/web/app/account/avatar.tsx
@@ -2,11 +2,16 @@
import { useTranslation } from 'react-i18next'
import { Fragment } from 'react'
import { useRouter } from 'next/navigation'
+import {
+ RiGraduationCapFill,
+} from '@remixicon/react'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
+import { useProviderContext } from '@/context/provider-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
+import PremiumBadge from '@/app/components/base/premium-badge'
export type IAppSelector = {
isMobile: boolean
@@ -16,6 +21,7 @@ export default function AppSelector() {
const router = useRouter()
const { t } = useTranslation()
const { userProfile } = useAppContext()
+ const { isEducationAccount } = useProviderContext()
const handleLogout = async () => {
await logout({
@@ -68,7 +74,15 @@ export default function AppSelector() {
-
{userProfile.name}
+
+ {userProfile.name}
+ {isEducationAccount && (
+
+
+ EDU
+
+ )}
+
{userProfile.email}
diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx
index 87f14b0e31..01ba8c606d 100644
--- a/web/app/components/app/configuration/dataset-config/index.tsx
+++ b/web/app/components/app/configuration/dataset-config/index.tsx
@@ -182,7 +182,6 @@ const DatasetConfig: FC = () => {
}, [setDatasetConfigs, datasetConfigsRef])
const handleUpdateCondition = useCallback
((id, newCondition) => {
- console.log(newCondition, 'newCondition')
const conditions = datasetConfigsRef.current!.metadata_filtering_conditions?.conditions || []
const index = conditions.findIndex(c => c.id === id)
const newInputs = produce(datasetConfigsRef.current!, (draft) => {
diff --git a/web/app/components/base/date-and-time-picker/date-picker/index.tsx b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
index 03b04887d2..f4fc86101e 100644
--- a/web/app/components/base/date-and-time-picker/date-picker/index.tsx
+++ b/web/app/components/base/date-and-time-picker/date-picker/index.tsx
@@ -130,7 +130,6 @@ const DatePicker = ({
const handleConfirmDate = () => {
// debugger
- console.log(selectedDate, selectedDate?.tz(timezone))
onChange(selectedDate ? selectedDate.tz(timezone) : undefined)
setIsOpen(false)
}
diff --git a/web/app/components/base/icons/assets/public/education/triangle.svg b/web/app/components/base/icons/assets/public/education/triangle.svg
new file mode 100644
index 0000000000..e52c5c5a43
--- /dev/null
+++ b/web/app/components/base/icons/assets/public/education/triangle.svg
@@ -0,0 +1,3 @@
+
diff --git a/web/app/components/base/icons/src/public/education/Triangle.json b/web/app/components/base/icons/src/public/education/Triangle.json
new file mode 100644
index 0000000000..92d7c82c43
--- /dev/null
+++ b/web/app/components/base/icons/src/public/education/Triangle.json
@@ -0,0 +1,27 @@
+{
+ "icon": {
+ "type": "element",
+ "isRootNode": true,
+ "name": "svg",
+ "attributes": {
+ "width": "16",
+ "height": "22",
+ "viewBox": "0 0 16 22",
+ "fill": "none",
+ "xmlns": "http://www.w3.org/2000/svg"
+ },
+ "children": [
+ {
+ "type": "element",
+ "name": "path",
+ "attributes": {
+ "id": "Rectangle 979",
+ "d": "M0 0H16L9.91493 16.7339C8.76529 19.8955 5.76063 22 2.39658 22H0V0Z",
+ "fill": "white"
+ },
+ "children": []
+ }
+ ]
+ },
+ "name": "Triangle"
+}
\ No newline at end of file
diff --git a/web/app/components/base/icons/src/public/education/Triangle.tsx b/web/app/components/base/icons/src/public/education/Triangle.tsx
new file mode 100644
index 0000000000..34f2a50666
--- /dev/null
+++ b/web/app/components/base/icons/src/public/education/Triangle.tsx
@@ -0,0 +1,16 @@
+// 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, Omit>((
+ props,
+ ref,
+) => )
+
+Icon.displayName = 'Triangle'
+
+export default Icon
diff --git a/web/app/components/base/icons/src/public/education/index.ts b/web/app/components/base/icons/src/public/education/index.ts
new file mode 100644
index 0000000000..de505dbbdc
--- /dev/null
+++ b/web/app/components/base/icons/src/public/education/index.ts
@@ -0,0 +1 @@
+export { default as Triangle } from './Triangle'
diff --git a/web/app/components/base/portal-to-follow-elem/index.tsx b/web/app/components/base/portal-to-follow-elem/index.tsx
index a88e04889e..1e2e198775 100644
--- a/web/app/components/base/portal-to-follow-elem/index.tsx
+++ b/web/app/components/base/portal-to-follow-elem/index.tsx
@@ -6,6 +6,7 @@ import {
flip,
offset,
shift,
+ size,
useDismiss,
useFloating,
useFocus,
@@ -27,6 +28,7 @@ export type PortalToFollowElemOptions = {
open?: boolean
offset?: number | OffsetOptions
onOpenChange?: (open: boolean) => void
+ triggerPopupSameWidth?: boolean
}
export function usePortalToFollowElem({
@@ -34,6 +36,7 @@ export function usePortalToFollowElem({
open,
offset: offsetValue = 0,
onOpenChange: setControlledOpen,
+ triggerPopupSameWidth,
}: PortalToFollowElemOptions = {}) {
const setOpen = setControlledOpen
@@ -50,6 +53,12 @@ export function usePortalToFollowElem({
padding: 5,
}),
shift({ padding: 5 }),
+ size({
+ apply({ rects, elements }) {
+ if (triggerPopupSameWidth)
+ elements.floating.style.width = `${rects.reference.width}px`
+ },
+ }),
],
})
diff --git a/web/app/components/billing/plan/index.tsx b/web/app/components/billing/plan/index.tsx
index b6ca148ea3..7badb3666f 100644
--- a/web/app/components/billing/plan/index.tsx
+++ b/web/app/components/billing/plan/index.tsx
@@ -2,10 +2,12 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
+import { useRouter } from 'next/navigation'
import {
RiBook2Line,
RiBox3Line,
RiFileEditLine,
+ RiGraduationCapLine,
RiGroup3Line,
RiGroupLine,
RiSquareLine,
@@ -15,7 +17,13 @@ import VectorSpaceInfo from '../usage-info/vector-space-info'
import AppsInfo from '../usage-info/apps-info'
import UpgradeBtn from '../upgrade-btn'
import { useProviderContext } from '@/context/provider-context'
+import { useAppContext } from '@/context/app-context'
+import Button from '@/app/components/base/button'
import UsageInfo from '@/app/components/billing/usage-info'
+import VerifyStateModal from '@/app/education-apply/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 = {
loc: string
@@ -25,7 +33,9 @@ const PlanComp: FC = ({
loc,
}) => {
const { t } = useTranslation()
- const { plan } = useProviderContext()
+ const router = useRouter()
+ const { userProfile } = useAppContext()
+ const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
const {
type,
} = plan
@@ -35,6 +45,18 @@ const PlanComp: FC = ({
total,
} = 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 (
@@ -58,14 +80,22 @@ const PlanComp: FC
= ({
{t(`billing.plans.${type}.for`)}
- {(plan.type as any) !== SelfHostedPlan.enterprise && (
-
- )}
+
+ {enableEducationPlan && !isEducationAccount && (
+
+ )}
+ {(plan.type as any) !== SelfHostedPlan.enterprise && (
+
+ )}
+
{/* Plan detail */}
@@ -92,6 +122,15 @@ const PlanComp: FC = ({
/>
+ setShowModal(false)}
+ onCancel={() => setShowModal(false)}
+ />
)
}
diff --git a/web/app/components/billing/type.ts b/web/app/components/billing/type.ts
index 01d9f2fe7e..28bce37098 100644
--- a/web/app/components/billing/type.ts
+++ b/web/app/components/billing/type.ts
@@ -87,6 +87,10 @@ export type CurrentPlanInfoBackend = {
can_replace_logo: boolean
model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean
+ education: {
+ enabled: boolean
+ activated: boolean
+ }
}
export type SubscriptionItem = {
diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx
index 2d20dd9d50..1a0cc96b98 100644
--- a/web/app/components/header/account-dropdown/index.tsx
+++ b/web/app/components/header/account-dropdown/index.tsx
@@ -3,7 +3,18 @@ import { useTranslation } from 'react-i18next'
import { Fragment, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
-import { RiAccountCircleLine, RiArrowDownSLine, RiArrowRightUpLine, RiBookOpenLine, RiGithubLine, RiInformation2Line, RiLogoutBoxRLine, RiMap2Line, RiSettings3Line, RiStarLine } from '@remixicon/react'
+import {
+ RiAccountCircleLine,
+ RiArrowRightUpLine,
+ RiBookOpenLine,
+ RiGithubLine,
+ RiGraduationCapFill,
+ RiInformation2Line,
+ RiLogoutBoxRLine,
+ RiMap2Line,
+ RiSettings3Line,
+ RiStarLine,
+} from '@remixicon/react'
import Link from 'next/link'
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react'
import Indicator from '../indicator'
@@ -11,21 +22,19 @@ import AccountAbout from '../account-about'
import GithubStar from '../github-star'
import Support from './support'
import Compliance from './compliance'
-import classNames from '@/utils/classnames'
+import PremiumBadge from '@/app/components/base/premium-badge'
import I18n from '@/context/i18n'
import Avatar from '@/app/components/base/avatar'
import { logout } from '@/service/common'
import AppContext, { useAppContext } from '@/context/app-context'
+import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language'
import { LicenseStatus } from '@/types/feature'
import { IS_CLOUD_EDITION } from '@/config'
+import cn from '@/utils/classnames'
-export type IAppSelector = {
- isMobile: boolean
-}
-
-export default function AppSelector({ isMobile }: IAppSelector) {
+export default function AppSelector() {
const itemClassName = `
flex items-center w-full h-9 pl-3 pr-2 text-text-secondary system-md-regular
rounded-lg hover:bg-state-base-hover cursor-pointer gap-1
@@ -37,6 +46,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
+ const { isEducationAccount } = useProviderContext()
const { setShowAccountSettingModal } = useModalContext()
const handleLogout = async () => {
@@ -58,20 +68,8 @@ export default function AppSelector({ isMobile }: IAppSelector) {
{
({ open }) => (
<>
-
-
- {!isMobile && <>
- {userProfile.name}
-
- >}
+
+
-
{userProfile.name}
+
+ {userProfile.name}
+ {isEducationAccount && (
+
+
+ EDU
+
+ )}
+
{userProfile.email}
@@ -101,7 +107,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {