From ff388fe3e6db25c00605bdf81f34934b950e811b Mon Sep 17 00:00:00 2001 From: Jiang <65766008+AlwaysBluer@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:06:35 +0800 Subject: [PATCH 001/290] optimize lindorm vdb add_texts (#17212) Co-authored-by: jiangzhijie --- .../datasource/vdb/lindorm/lindorm_vector.py | 92 ++++++++++++++----- 1 file changed, 68 insertions(+), 24 deletions(-) diff --git a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py index 1ba37dfb3f..b2fbfced0a 100644 --- a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py +++ b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py @@ -1,10 +1,12 @@ import copy import json import logging +import time from typing import Any, Optional from opensearchpy import OpenSearch from pydantic import BaseModel, model_validator +from tenacity import retry, stop_after_attempt, wait_exponential from configs import dify_config from core.rag.datasource.vdb.field import Field @@ -77,31 +79,74 @@ class LindormVectorStore(BaseVector): def refresh(self): self._client.indices.refresh(index=self._collection_name) - def add_texts(self, documents: list[Document], embeddings: list[list[float]], **kwargs): - actions = [] + def add_texts( + self, + documents: list[Document], + embeddings: list[list[float]], + batch_size: int = 64, + timeout: int = 60, + **kwargs, + ): + logger.info(f"Total documents to add: {len(documents)}") uuids = self._get_uuids(documents) - for i in range(len(documents)): - action_header = { - "index": { - "_index": self.collection_name.lower(), - "_id": uuids[i], + + total_docs = len(documents) + num_batches = (total_docs + batch_size - 1) // batch_size + + @retry( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=4, max=10), + ) + def _bulk_with_retry(actions): + try: + response = self._client.bulk(actions, timeout=timeout) + if response["errors"]: + error_items = [item for item in response["items"] if "error" in item["index"]] + error_msg = f"Bulk indexing had {len(error_items)} errors" + logger.exception(error_msg) + raise Exception(error_msg) + return response + except Exception: + logger.exception("Bulk indexing error") + raise + + for batch_num in range(num_batches): + start_idx = batch_num * batch_size + end_idx = min((batch_num + 1) * batch_size, total_docs) + + actions = [] + for i in range(start_idx, end_idx): + action_header = { + "index": { + "_index": self.collection_name.lower(), + "_id": uuids[i], + } } - } - action_values: dict[str, Any] = { - Field.CONTENT_KEY.value: documents[i].page_content, - Field.VECTOR.value: embeddings[i], # Make sure you pass an array here - Field.METADATA_KEY.value: documents[i].metadata, - } - if self._using_ugc: - action_header["index"]["routing"] = self._routing - if self._routing_field is not None: - action_values[self._routing_field] = self._routing - actions.append(action_header) - actions.append(action_values) - response = self._client.bulk(actions) - if response["errors"]: - for item in response["items"]: - print(f"{item['index']['status']}: {item['index']['error']['type']}") + action_values: dict[str, Any] = { + Field.CONTENT_KEY.value: documents[i].page_content, + Field.VECTOR.value: embeddings[i], + Field.METADATA_KEY.value: documents[i].metadata, + } + if self._using_ugc: + action_header["index"]["routing"] = self._routing + if self._routing_field is not None: + action_values[self._routing_field] = self._routing + + actions.append(action_header) + actions.append(action_values) + + logger.info(f"Processing batch {batch_num + 1}/{num_batches} (documents {start_idx + 1} to {end_idx})") + + try: + _bulk_with_retry(actions) + logger.info(f"Successfully processed batch {batch_num + 1}") + # simple latency to avoid too many requests in a short time + if batch_num < num_batches - 1: + time.sleep(1) + + except Exception: + logger.exception(f"Failed to process batch {batch_num + 1}") + raise def get_ids_by_metadata_field(self, key: str, value: str): query: dict[str, Any] = { @@ -130,7 +175,6 @@ class LindormVectorStore(BaseVector): if self._using_ugc: params["routing"] = self._routing self._client.delete(index=self._collection_name, id=id, params=params) - self.refresh() else: logger.warning(f"DELETE BY ID: ID {id} does not exist in the index.") From 931d3390f03a3e10a8d26b6be79a948d4c343dde Mon Sep 17 00:00:00 2001 From: GuanMu Date: Tue, 1 Apr 2025 11:07:47 +0800 Subject: [PATCH 002/290] Fix api document (#17178) --- web/app/(commonLayout)/datasets/template/template.en.mdx | 4 ++-- web/app/(commonLayout)/datasets/template/template.ja.mdx | 4 ++-- web/app/(commonLayout)/datasets/template/template.zh.mdx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/app/(commonLayout)/datasets/template/template.en.mdx b/web/app/(commonLayout)/datasets/template/template.en.mdx index 27c0de16ef..afc924202a 100644 --- a/web/app/(commonLayout)/datasets/template/template.en.mdx +++ b/web/app/(commonLayout)/datasets/template/template.en.mdx @@ -1698,9 +1698,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ```bash {{ title: 'cURL' }} ``` diff --git a/web/app/(commonLayout)/datasets/template/template.ja.mdx b/web/app/(commonLayout)/datasets/template/template.ja.mdx index 4fe13b156f..e2bdd27d80 100644 --- a/web/app/(commonLayout)/datasets/template/template.ja.mdx +++ b/web/app/(commonLayout)/datasets/template/template.ja.mdx @@ -1698,9 +1698,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ```bash {{ title: 'cURL' }} ``` diff --git a/web/app/(commonLayout)/datasets/template/template.zh.mdx b/web/app/(commonLayout)/datasets/template/template.zh.mdx index ef2637461d..2fafa32307 100644 --- a/web/app/(commonLayout)/datasets/template/template.zh.mdx +++ b/web/app/(commonLayout)/datasets/template/template.zh.mdx @@ -1733,9 +1733,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi ```bash {{ title: 'cURL' }} ``` From 6372cb7b41f03f4dce1704bf3575e550f265d544 Mon Sep 17 00:00:00 2001 From: Obada Khalili <54270856+obadakhalili@users.noreply.github.com> Date: Tue, 1 Apr 2025 05:19:36 +0200 Subject: [PATCH 003/290] Support variables in question classifier classes (#17227) --- .../nodes/_base/components/prompt/editor.tsx | 3 ++ .../components/class-item.tsx | 54 +++++++++---------- .../components/class-list.tsx | 13 +++-- .../nodes/question-classifier/node.tsx | 10 +++- .../nodes/question-classifier/panel.tsx | 3 +- 5 files changed, 46 insertions(+), 37 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index aba0683923..cc8799bbed 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -73,6 +73,7 @@ type Props = { titleTooltip?: ReactNode inputClassName?: string editorContainerClassName?: string + placeholder?: string placeholderClassName?: string titleClassName?: string required?: boolean @@ -108,6 +109,7 @@ const Editor: FC = ({ gradientBorder = true, titleTooltip, inputClassName, + placeholder, placeholderClassName, titleClassName, editorContainerClassName, @@ -223,6 +225,7 @@ const Editor: FC = ({
void onRemove: () => void index: number readonly?: boolean + filterVar: (payload: Var, valueSelector: ValueSelector) => boolean } const ClassItem: FC = ({ + nodeId, payload, onChange, onRemove, index, readonly, + filterVar, }) => { const { t } = useTranslation() @@ -31,35 +34,26 @@ const ClassItem: FC = ({ onChange({ ...payload, name: value }) }, [onChange, payload]) + const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + hideChatVar: false, + hideEnv: false, + filterVar, + }) + return ( - -
-
- {`${t(`${i18nPrefix}.class`)} ${index}`} -
-
-
} + -
{payload.name.length}
-
- {!readonly && ( - - )} - - )} - readonly={readonly} - minHeight={64} + showRemove + onRemove={onRemove} + nodesOutputVars={availableVars} + availableNodes={availableNodesWithParent} + readOnly={readonly} // ? + justVar // ? + isSupportFileVar // ? /> ) } diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index e23c42bf97..4dc0ff759e 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -7,21 +7,24 @@ import { useEdgesInteractions } from '../../../hooks' import AddButton from '../../_base/components/add-button' import Item from './class-item' import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types' +import type { ValueSelector, Var } from '@/app/components/workflow/types' const i18nPrefix = 'workflow.nodes.questionClassifiers' type Props = { - id: string + nodeId: string list: Topic[] onChange: (list: Topic[]) => void readonly?: boolean + filterVar: (payload: Var, valueSelector: ValueSelector) => boolean } const ClassList: FC = ({ - id, + nodeId, list, onChange, readonly, + filterVar, }) => { const { t } = useTranslation() const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() @@ -44,13 +47,13 @@ const ClassList: FC = ({ const handleRemoveClass = useCallback((index: number) => { return () => { - handleEdgeDeleteByDeleteBranch(id, list[index].id) + handleEdgeDeleteByDeleteBranch(nodeId, list[index].id) const newList = produce(list, (draft) => { draft.splice(index, 1) }) onChange(newList) } - }, [list, onChange, handleEdgeDeleteByDeleteBranch, id]) + }, [list, onChange, handleEdgeDeleteByDeleteBranch, nodeId]) // Todo Remove; edit topic name return ( @@ -59,12 +62,14 @@ const ClassList: FC = ({ list.map((item, index) => { return ( ) }) diff --git a/web/app/components/workflow/nodes/question-classifier/node.tsx b/web/app/components/workflow/nodes/question-classifier/node.tsx index 8316c3b259..87ec68b021 100644 --- a/web/app/components/workflow/nodes/question-classifier/node.tsx +++ b/web/app/components/workflow/nodes/question-classifier/node.tsx @@ -9,13 +9,14 @@ import { useTextGenerationCurrentProviderAndModelAndModelList, } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' +import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var' const i18nPrefix = 'workflow.nodes.questionClassifiers' const Node: FC> = (props) => { const { t } = useTranslation() - const { data } = props + const { data, id } = props const { provider, name: modelId } = data.model // const tempTopics = data.topics const topics = data.classes @@ -47,7 +48,12 @@ const Node: FC> = (props) => { > + } /> > = ({ title={t(`${i18nPrefix}.class`)} > From d5b48a0aa3341c2e9c65c5886c9924552084aa40 Mon Sep 17 00:00:00 2001 From: liuzhenghua <1090179900@qq.com> Date: Tue, 1 Apr 2025 11:20:32 +0800 Subject: [PATCH 004/290] fix: keywords field not persist in segements api (#17151) --- api/services/dataset_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 8f88cecbe6..6fe301588a 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -1663,6 +1663,7 @@ class SegmentService: content=content, word_count=len(content), tokens=tokens, + keywords=segment_item.get("keywords", []), status="completed", indexing_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), completed_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None), From 4b5ec242e7e289f1952215a293c36b28737e2864 Mon Sep 17 00:00:00 2001 From: HuDenghui Date: Tue, 1 Apr 2025 12:16:49 +0800 Subject: [PATCH 005/290] fix: Fix lodash module not found issue (#16953) --- web/app/components/base/chat/__tests__/utils.spec.ts | 2 +- .../components/workflow/nodes/http/hooks/use-key-value-list.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts index 990e59d5ee..3dc484c930 100644 --- a/web/app/components/base/chat/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/__tests__/utils.spec.ts @@ -1,4 +1,4 @@ -import { get } from 'lodash' +import { get } from 'lodash-es' import { buildChatItemTree, getThreadMessages } from '../utils' import type { ChatItemInTree } from '../types' import branchedTestMessages from './branchedTestMessages.json' diff --git a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts index 8e01055c37..3892461c36 100644 --- a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts +++ b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import { useBoolean } from 'ahooks' -import { uniqueId } from 'lodash' +import { uniqueId } from 'lodash-es' import type { KeyValue } from '../types' const UNIQUE_ID_PREFIX = 'key-value-' From d1801b1f2e11950d75f4027ab87c04b99b94e3d7 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Tue, 1 Apr 2025 13:58:10 +0800 Subject: [PATCH 006/290] =?UTF-8?q?Feat=EF=BC=9Aedu=20frontend=20(#17251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: crazywoola <427733928@qq.com> Co-authored-by: zxhlyh --- .github/workflows/build-push.yml | 1 + web/app/(commonLayout)/apps/Apps.tsx | 4 +- web/app/(commonLayout)/apps/page.tsx | 3 + .../(commonLayout)/education-apply/page.tsx | 29 +++ web/app/account/account-page/index.tsx | 17 +- web/app/account/avatar.tsx | 16 +- .../configuration/dataset-config/index.tsx | 1 - .../date-picker/index.tsx | 1 - .../assets/public/education/triangle.svg | 3 + .../icons/src/public/education/Triangle.json | 27 +++ .../icons/src/public/education/Triangle.tsx | 16 ++ .../base/icons/src/public/education/index.ts | 1 + .../base/portal-to-follow-elem/index.tsx | 9 + web/app/components/billing/plan/index.tsx | 57 +++++- web/app/components/billing/type.ts | 4 + .../header/account-dropdown/index.tsx | 64 +++--- .../workplace-selector/index.tsx | 2 +- web/app/components/header/index.tsx | 4 +- .../components/header/plan-badge/index.tsx | 8 +- web/app/components/swr-initor.tsx | 9 + web/app/education-apply/constants.ts | 2 + .../education-apply/education-apply-page.tsx | 191 ++++++++++++++++++ web/app/education-apply/hooks.ts | 67 ++++++ web/app/education-apply/role-selector.tsx | 53 +++++ web/app/education-apply/search-input.tsx | 121 +++++++++++ web/app/education-apply/types.ts | 11 + web/app/education-apply/user-info.tsx | 61 ++++++ .../education-apply/verify-state-modal.tsx | 122 +++++++++++ web/context/modal-context.tsx | 11 +- web/context/provider-context.tsx | 20 +- web/i18n/en-US/education.ts | 47 +++++ web/i18n/i18next-config.ts | 13 ++ web/i18n/ja-JP/education.ts | 47 +++++ web/i18n/zh-Hans/education.ts | 48 +++++ web/public/education/bg.png | Bin 0 -> 173704 bytes web/service/use-education.ts | 67 ++++++ web/utils/index.ts | 9 + 37 files changed, 1115 insertions(+), 51 deletions(-) create mode 100644 web/app/(commonLayout)/education-apply/page.tsx create mode 100644 web/app/components/base/icons/assets/public/education/triangle.svg create mode 100644 web/app/components/base/icons/src/public/education/Triangle.json create mode 100644 web/app/components/base/icons/src/public/education/Triangle.tsx create mode 100644 web/app/components/base/icons/src/public/education/index.ts create mode 100644 web/app/education-apply/constants.ts create mode 100644 web/app/education-apply/education-apply-page.tsx create mode 100644 web/app/education-apply/hooks.ts create mode 100644 web/app/education-apply/role-selector.tsx create mode 100644 web/app/education-apply/search-input.tsx create mode 100644 web/app/education-apply/types.ts create mode 100644 web/app/education-apply/user-info.tsx create mode 100644 web/app/education-apply/verify-state-modal.tsx create mode 100644 web/i18n/en-US/education.ts create mode 100644 web/i18n/ja-JP/education.ts create mode 100644 web/i18n/zh-Hans/education.ts create mode 100644 web/public/education/bg.png create mode 100644 web/service/use-education.ts 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) {
-
setShowAccountSettingModal({ payload: 'members' })}> @@ -123,7 +129,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
{systemFeatures.license.status === LicenseStatus.NONE && -
setAboutVisible(true)}> @@ -186,7 +192,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
handleLogout()}>
diff --git a/web/app/components/header/account-dropdown/workplace-selector/index.tsx b/web/app/components/header/account-dropdown/workplace-selector/index.tsx index 387e7bfb29..70b6ece5fd 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next' import { Menu, MenuButton, MenuItems, Transition } from '@headlessui/react' import { RiArrowDownSLine } from '@remixicon/react' import cn from '@/utils/classnames' +import PlanBadge from '@/app/components/header/plan-badge' import { switchWorkspace } from '@/service/common' import { useWorkspacesContext } from '@/context/workspace-context' import { ToastContext } from '@/app/components/base/toast' -import PlanBadge from '../../plan-badge' import type { Plan } from '@/app/components/billing/type' const WorkplaceSelector = () => { diff --git a/web/app/components/header/index.tsx b/web/app/components/header/index.tsx index 7e46e000d5..13c587dc19 100644 --- a/web/app/components/header/index.tsx +++ b/web/app/components/header/index.tsx @@ -94,10 +94,10 @@ const Header = () => { }
-
+
- +
{ (isMobile && isShowNavMenu) && ( diff --git a/web/app/components/header/plan-badge/index.tsx b/web/app/components/header/plan-badge/index.tsx index 22f7a0fa05..37cbe2a710 100644 --- a/web/app/components/header/plan-badge/index.tsx +++ b/web/app/components/header/plan-badge/index.tsx @@ -1,6 +1,9 @@ import { useProviderContext } from '@/context/provider-context' import type { FC } from 'react' import { useTranslation } from 'react-i18next' +import { + RiGraduationCapFill, +} from '@remixicon/react' import { SparklesSoft } from '../../base/icons/src/public/common' import PremiumBadge from '../../base/premium-badge' import { Plan } from '../../billing/type' @@ -13,7 +16,7 @@ type PlanBadgeProps = { } const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = false, onClick }) => { - const { isFetchedPlan } = useProviderContext() + const { isFetchedPlan, isEducationWorkspace } = useProviderContext() const { t } = useTranslation() if (!isFetchedPlan) return null @@ -39,7 +42,8 @@ const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = fa if (plan === Plan.professional) { return
- + + {isEducationWorkspace && } pro
diff --git a/web/app/components/swr-initor.tsx b/web/app/components/swr-initor.tsx index 2a119df996..d6a7f23f3a 100644 --- a/web/app/components/swr-initor.tsx +++ b/web/app/components/swr-initor.tsx @@ -5,6 +5,10 @@ import { useCallback, useEffect, useState } from 'react' import type { ReactNode } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { fetchSetupStatus } from '@/service/common' +import { + EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, + EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION, +} from '@/app/education-apply/constants' type SwrInitorProps = { children: ReactNode @@ -41,6 +45,11 @@ const SwrInitor = ({ useEffect(() => { (async () => { + const action = searchParams.get('action') + + if (action === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION) + localStorage.setItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'yes') + try { const isFinished = await isSetupFinished() if (!isFinished) { diff --git a/web/app/education-apply/constants.ts b/web/app/education-apply/constants.ts new file mode 100644 index 0000000000..a5672d19f9 --- /dev/null +++ b/web/app/education-apply/constants.ts @@ -0,0 +1,2 @@ +export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify' +export const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx new file mode 100644 index 0000000000..8d822f2ab0 --- /dev/null +++ b/web/app/education-apply/education-apply-page.tsx @@ -0,0 +1,191 @@ +'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 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 ( +
+
+
+
+
+ dify logo +
+
+
+
{t('education.toVerified')}
+
+ {t('education.toVerifiedTip.front')}  + {t('education.toVerifiedTip.coupon')}  + {t('education.toVerifiedTip.end')} +
+
+
+ +
+
+
+ {t('education.form.schoolName.title')} +
+ +
+
+
+ {t('education.form.schoolRole.title')} +
+ +
+
+
+ {t('education.form.terms.title')} +
+
+ {t('education.form.terms.desc.front')}  + {t('education.form.terms.desc.termsOfService')}  + {t('education.form.terms.desc.and')}  + {t('education.form.terms.desc.privacyPolicy')} + {t('education.form.terms.desc.end')} +
+
+
+ setAgeChecked(!ageChecked)} + /> + {t('education.form.terms.option.age')} +
+
+ setInSchoolChecked(!inSchoolChecked)} + /> + {t('education.form.terms.option.inSchool')} +
+
+
+ +
+ + {t('education.learn')} + + +
+
+ {})} + onCancel={modalShow?.onConfirm || (() => {})} + /> +
+ ) +} + +export default EducationApplyAge diff --git a/web/app/education-apply/hooks.ts b/web/app/education-apply/hooks.ts new file mode 100644 index 0000000000..01fb36c7ff --- /dev/null +++ b/web/app/education-apply/hooks.ts @@ -0,0 +1,67 @@ +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([]) + 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]) +} diff --git a/web/app/education-apply/role-selector.tsx b/web/app/education-apply/role-selector.tsx new file mode 100644 index 0000000000..b8448a0052 --- /dev/null +++ b/web/app/education-apply/role-selector.tsx @@ -0,0 +1,53 @@ +import { useTranslation } from 'react-i18next' +import cn from '@/utils/classnames' + +type RoleSelectorProps = { + onChange: (value: string) => void + value: string +} + +const RoleSelector = ({ + onChange, + value, +}: RoleSelectorProps) => { + const { t } = useTranslation() + const options = [ + { + key: 'Student', + value: t('education.form.schoolRole.option.student'), + }, + { + key: 'Teacher', + value: t('education.form.schoolRole.option.teacher'), + }, + { + key: 'School-Administrator', + value: t('education.form.schoolRole.option.administrator'), + }, + ] + + return ( +
+ { + options.map(option => ( +
onChange(option.key)} + > +
+
+ {option.value} +
+ )) + } +
+ ) +} + +export default RoleSelector diff --git a/web/app/education-apply/search-input.tsx b/web/app/education-apply/search-input.tsx new file mode 100644 index 0000000000..800f49fc06 --- /dev/null +++ b/web/app/education-apply/search-input.tsx @@ -0,0 +1,121 @@ +import { + useCallback, + useRef, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { useEducation } from './hooks' +import Input from '@/app/components/base/input' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' + +type SearchInputProps = { + value?: string + onChange: (value: string) => void +} +const SearchInput = ({ + value, + onChange, +}: SearchInputProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const { + schools, + setSchools, + querySchoolsWithDebounced, + handleUpdateSchools, + hasNext, + } = useEducation() + const pageRef = useRef(0) + const valueRef = useRef(value) + + const handleSearch = useCallback((debounced?: boolean) => { + const keywords = valueRef.current + const page = pageRef.current + if (debounced) { + querySchoolsWithDebounced({ + keywords, + page, + }) + return + } + + handleUpdateSchools({ + keywords, + page, + }) + }, [querySchoolsWithDebounced, handleUpdateSchools]) + + const handleValueChange = useCallback((e: any) => { + setOpen(true) + setSchools([]) + pageRef.current = 0 + const inputValue = e.target.value + valueRef.current = inputValue + onChange(inputValue) + handleSearch(true) + }, [onChange, handleSearch, setSchools]) + + const handleScroll = useCallback((e: Event) => { + const target = e.target as HTMLDivElement + const { + scrollTop, + scrollHeight, + clientHeight, + } = target + if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0 && hasNext) { + pageRef.current += 1 + handleSearch() + } + }, [handleSearch, hasNext]) + + return ( + + + + + + { + !!schools.length && value && ( +
+ { + schools.map((school, index) => ( +
{ + onChange(school) + setOpen(false) + }} + > + {school} +
+ )) + } +
+ ) + } +
+
+ ) +} + +export default SearchInput diff --git a/web/app/education-apply/types.ts b/web/app/education-apply/types.ts new file mode 100644 index 0000000000..ff435c6fc0 --- /dev/null +++ b/web/app/education-apply/types.ts @@ -0,0 +1,11 @@ +export type SearchParams = { + keywords?: string + page?: number + limit?: number +} + +export type EducationAddParams = { + token: string + institution: string + role: string +} diff --git a/web/app/education-apply/user-info.tsx b/web/app/education-apply/user-info.tsx new file mode 100644 index 0000000000..e1d60a5e94 --- /dev/null +++ b/web/app/education-apply/user-info.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from 'react-i18next' +import { useRouter } from 'next/navigation' +import Button from '@/app/components/base/button' +import { useAppContext } from '@/context/app-context' +import { logout } from '@/service/common' +import Avatar from '@/app/components/base/avatar' +import { Triangle } from '@/app/components/base/icons/src/public/education' + +const UserInfo = () => { + const router = useRouter() + const { t } = useTranslation() + const { userProfile } = useAppContext() + + const handleLogout = async () => { + await logout({ + url: '/logout', + params: {}, + }) + + localStorage.removeItem('setup_status') + localStorage.removeItem('console_token') + localStorage.removeItem('refresh_token') + + router.push('/signin') + } + + return ( +
+
+
+ {t('education.currentSigned')} +
+ +
+
+ +
+
+ {userProfile.name} +
+
+ {userProfile.email} +
+
+
+ +
+ ) +} + +export default UserInfo diff --git a/web/app/education-apply/verify-state-modal.tsx b/web/app/education-apply/verify-state-modal.tsx new file mode 100644 index 0000000000..aace6a3bb1 --- /dev/null +++ b/web/app/education-apply/verify-state-modal.tsx @@ -0,0 +1,122 @@ +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(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( +
{ + e.preventDefault() + e.stopPropagation() + }} + > +
+
+
+
{title}
+
{content}
+
+ {email && ( +
+
{t('education.emailLabel')}
+
{email}
+
+ )} +
+
+ {showLink && ( + <> + {t('education.learn')} + + + )} +
+ +
+
+
+
, document.body, + ) +} + +export default React.memo(Confirm) diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 622077ee91..8641dce617 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -17,7 +17,9 @@ import type { ModelLoadBalancingConfigEntry, ModelProvider, } from '@/app/components/header/account-setting/model-provider-page/declarations' - +import { + EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, +} from '@/app/education-apply/constants' import Pricing from '@/app/components/billing/pricing' import type { ModerationConfig, PromptVariable } from '@/models/debug' import type { @@ -33,6 +35,7 @@ import type { OpeningStatement } from '@/app/components/base/features/types' import type { InputVar } from '@/app/components/workflow/types' import type { UpdatePluginPayload } from '@/app/components/plugins/types' import UpdatePlugin from '@/app/components/plugins/update-plugin' +import { removeSpecificQueryParam } from '@/utils' export type ModalState = { payload: T @@ -121,6 +124,12 @@ export const ModalContextProvider = ({ const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false) const handleCancelAccountSettingModal = () => { + const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) + + if (educationVerifying === 'yes') + localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM) + + removeSpecificQueryParam('action') setShowAccountSettingModal(null) if (showAccountSettingModal?.onCancelCallback) showAccountSettingModal?.onCancelCallback() diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index 11340f6acd..67c389167d 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -22,6 +22,9 @@ import { fetchCurrentPlanInfo } from '@/service/billing' import { parseCurrentPlan } from '@/app/components/billing/utils' import { defaultPlan } from '@/app/components/billing/config' import Toast from '@/app/components/base/toast' +import { + useEducationStatus, +} from '@/service/use-education' type ProviderContextState = { modelProviders: ModelProvider[] @@ -40,6 +43,9 @@ type ProviderContextState = { enableReplaceWebAppLogo: boolean modelLoadBalancingEnabled: boolean datasetOperatorEnabled: boolean + enableEducationPlan: boolean + isEducationWorkspace: boolean + isEducationAccount: boolean } const ProviderContext = createContext({ modelProviders: [], @@ -70,6 +76,9 @@ const ProviderContext = createContext({ enableReplaceWebAppLogo: false, modelLoadBalancingEnabled: false, datasetOperatorEnabled: false, + enableEducationPlan: false, + isEducationWorkspace: false, + isEducationAccount: false, }) export const useProviderContext = () => useContext(ProviderContext) @@ -97,13 +106,19 @@ export const ProviderContextProvider = ({ const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false) + const [enableEducationPlan, setEnableEducationPlan] = useState(false) + const [isEducationWorkspace, setIsEducationWorkspace] = useState(false) + const { data: isEducationAccount } = useEducationStatus(!enableEducationPlan) + const fetchPlan = async () => { const data = await fetchCurrentPlanInfo() const enabled = data.billing.enabled setEnableBilling(enabled) + setEnableEducationPlan(data.education.enabled) + setIsEducationWorkspace(data.education.activated) setEnableReplaceWebAppLogo(data.can_replace_logo) if (enabled) { - setPlan(parseCurrentPlan(data)) + setPlan(parseCurrentPlan(data) as any) setIsFetchedPlan(true) } if (data.model_load_balancing_enabled) @@ -155,6 +170,9 @@ export const ProviderContextProvider = ({ enableReplaceWebAppLogo, modelLoadBalancingEnabled, datasetOperatorEnabled, + enableEducationPlan, + isEducationWorkspace, + isEducationAccount: isEducationAccount?.result || false, }}> {children} diff --git a/web/i18n/en-US/education.ts b/web/i18n/en-US/education.ts new file mode 100644 index 0000000000..b3a13612ed --- /dev/null +++ b/web/i18n/en-US/education.ts @@ -0,0 +1,47 @@ +const translation = { + toVerified: 'Get Education Verified', + toVerifiedTip: { + front: 'You are now eligible for Education Verified status. Please enter your education information below to complete the process and receive an', + coupon: 'exclusive 50% coupon', + end: 'for the Dify Professional Plan.', + }, + currentSigned: 'CURRENTLY SIGNED IN AS', + form: { + schoolName: { + title: 'Your School Name', + placeholder: 'Enter the official, unabbreviated name of your school', + }, + schoolRole: { + title: 'Your School Role', + option: { + student: 'Student', + teacher: 'Teacher', + administrator: 'School Administrator', + }, + }, + terms: { + title: 'Terms & Agreements', + desc: { + front: 'Your information and use of Education Verified status are subject to our', + and: 'and', + end: '. By submitting:', + termsOfService: 'Terms of Service', + privacyPolicy: 'Privacy Policy', + }, + option: { + age: 'I confirm I am at least 18 years old', + inSchool: 'I confirm I am enrolled or employed at the institution provided. Dify may request proof of enrollment/employment. If I misrepresent my eligibility, I agree to pay any fees initially waived based on my education status.', + }, + }, + }, + submit: 'Submit', + submitError: 'Form submission failed. Please try again later.', + learn: 'Learn how to get education verified', + successTitle: 'You Have Got Dify Education Verified', + successContent: 'We have issued a 50% discount coupon for the Dify Professional plan to your account. The coupon is valid for one year, please use it within the validity period.', + rejectTitle: 'Your Dify 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 diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index 7215b7818e..eea15ac8be 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -4,6 +4,18 @@ import { initReactI18next } from 'react-i18next' import { LanguagesSupported } from '@/i18n/language' +const requireSilent = (lang: string) => { + let res + try { + res = require(`./${lang}/education`).default + } + catch { + res = require('./en-US/education').default + } + + return res +} + const loadLangResources = (lang: string) => ({ translation: { common: require(`./${lang}/common`).default, @@ -31,6 +43,7 @@ const loadLangResources = (lang: string) => ({ plugin: require(`./${lang}/plugin`).default, pluginTags: require(`./${lang}/plugin-tags`).default, time: require(`./${lang}/time`).default, + education: requireSilent(lang), }, }) diff --git a/web/i18n/ja-JP/education.ts b/web/i18n/ja-JP/education.ts new file mode 100644 index 0000000000..d51bac817d --- /dev/null +++ b/web/i18n/ja-JP/education.ts @@ -0,0 +1,47 @@ +const translation = { + toVerified: '教育認証を取得', + toVerifiedTip: { + front: '現在、教育認証ステータスを取得する資格があります。以下に教育情報を入力し、認証プロセスを完了すると、Difyプロフェッショナルプランの', + coupon: '50%割引クーポン', + end: 'を受け取ることができます。', + }, + currentSigned: '現在ログイン中のアカウントは', + form: { + schoolName: { + title: '学校名', + placeholder: '学校の正式名称(省略不可)を入力してください。', + }, + schoolRole: { + title: '学校での役割', + option: { + student: '学生', + teacher: '教師', + administrator: '学校管理者', + }, + }, + terms: { + title: '利用規約と同意事項', + desc: { + front: 'お客様の情報および 教育認証ステータス の利用は、当社の ', + and: 'および', + end: 'に従うものとします。送信することで以下を確認します:', + termsOfService: '利用規約', + privacyPolicy: 'プライバシーポリシー', + }, + option: { + age: '18歳以上であることを確認します。', + inSchool: '提供した教育機関に在籍または勤務している ことを確認します。Difyは在籍/雇用証明の提出を求める場合があります。不正な情報を申告した場合、教育認証に基づき免除された費用を支払うことに同意します。', + }, + }, + }, + submit: '送信', + submitError: 'フォームの送信に失敗しました。しばらくしてから再度ご提出ください。', + learn: '教育認証の取得方法はこちら', + successTitle: 'Dify教育認証を取得しました!', + successContent: 'お客様のアカウントに Difyプロフェッショナルプランの50%割引クーポン を発行しました。有効期間は 1年間 ですので、期限内にご利用ください。', + rejectTitle: 'Dify教育認証が拒否されました', + rejectContent: '申し訳ございませんが、このメールアドレスでは 教育認証 の資格を取得できず、Difyプロフェッショナルプランの50%割引クーポン を受け取ることはできません。', + emailLabel: '現在のメールアドレス', +} + +export default translation diff --git a/web/i18n/zh-Hans/education.ts b/web/i18n/zh-Hans/education.ts new file mode 100644 index 0000000000..ca4d2cb3cc --- /dev/null +++ b/web/i18n/zh-Hans/education.ts @@ -0,0 +1,48 @@ +const translation = { + toVerified: '获取教育版认证', + toVerifiedTip: { + front: '您现在符合教育版认证的资格。请在下方输入您的教育信息,以完成认证流程,并领取 Dify Professional 版的', + coupon: '50% 独家优惠券', + end: '。', + }, + currentSigned: '您当前登录的账户是', + form: { + schoolName: { + title: '您的学校名称', + placeholder: '请输入您的学校的官方全称(不得缩写)', + }, + schoolRole: { + title: '您在学校的身份', + option: { + student: '学生', + teacher: '教师', + administrator: '学校管理员', + }, + }, + terms: { + title: '条款与协议', + desc: { + front: '您的信息和教育版认证资格的使用需遵守我们的', + and: '和', + end: '。提交即表示:', + termsOfService: '服务条款', + privacyPolicy: '隐私政策', + }, + option: { + age: '我确认我已年满 18 周岁。', + inSchool: '我确认我目前已在提供的学校入学或受雇。Dify 可能会要求提供入学/雇佣证明。如我虚报资格,我同意支付因教育版认证而被减免的费用。', + }, + }, + }, + submit: '提交', + submitError: '提交表单失败,请稍后重新提交问卷。', + learn: '了解如何获取教育版认证', + successTitle: '您已成功获得 Dify 教育版认证!', + successContent: '我们已向您的账户发放 Dify Professional 版 50% 折扣优惠券。该优惠券有效期为一年,请在有效期内使用。', + rejectTitle: '您的 Dify 教育版认证已被拒绝', + rejectContent: '非常遗憾,您无法使用此电子邮件以获得教育版认证资格,也无法领取 Dify Professional 版的 50% 独家优惠券。', + emailLabel: '您当前的邮箱', + +} + +export default translation diff --git a/web/public/education/bg.png b/web/public/education/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..0c2e503f5f741e4e9eb5ebabfa87275c3f7eddc5 GIT binary patch literal 173704 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 z z=YA)Q{g1A=Q3p^AZHTMf2nf>X-fOK?S-ZU`c1NVqWX|uv9FSW~k_Ow#$V+Yq^DA{U_mm0(rQb z&nFY+LD?&Hc%5}}dw07-uK3rvN_y2U;$3cSkJcZb0O20r^s>2OH(QxrOb2&Wnph{VKg0o#M8726 zP%rvN7gLaS89XAo-M1H?6b~*|D;jX#ewhimYlQlkt)vZ&cqb(o2nq=sX~+dSZG<2b z(BKW$g|K6M1v%g+jo}D6@`YAne6ir*1QoF^pk{oiigp`S=)G{;5;BVYMb{BJKnms@ z5JIUB08B6sb5C*^D8cr!%!jJVvCSIJ5!IGcmO=Zu7zt`J9+HjW1bePgJ%eeZl$kc&mB=eN2pQK5+&n39Nw!TcA8t>hK38k7no1#j zVD?wqCKHe2Ts?#K%Ni1I$r(_Kj@C0a!B2F7@3GKYsIgeux;S7`;wyNzDq=H2bY*CO zM}Q(zLoD&=Zb35OGdyN68Eur;++rkfC&CM1~&=s!QqnG zwz;b)aBAipw<}uh(S4n!NV?Rq_C{p1iM)_QTmy2?2KU3NWpXjJK8nc7yhfPCmzZ?> zjr?5WgapdT$<*-bkt0i@-U;*3gDXZ5XuPQez8Cwap3Hr7%2%=16fPKQTrW<{AkVsA`EFLlpqQ11lxoVz!J1I zsVi|&EpC|Dg$qrzsV-`4qBb$Jn7w--o-^Cd4Ip-MQfGgFdkw}Pe7Xj`WstCTn zHIk*)kFWqTw=A=^*Wb$Q$!ce23~DMQ}z2(K`LY0aGO)4?G2=H ze^@JhH2>arf0XaWCY93U@*XNxsyl^5-9SI+s($>E<9;BNH%18;6xytzGo0{Q=qaX(eLw5b8dH){{+yvfjflV^r$f8BTQ zEk0`8(nm-QBn^;BlMj>ETXJw85mv%2^-2DR0JjuujDIJBX6YgzE7m|oOMz~xQOZ~i zNqaQsZGw@cO~p-7@=eLs7)gYXYQ9klEPNB(2RjXq)kG4f(8*>1Y?MBMaSMG!o0$dF zfOvKevcir*TnyU~_D6ZeBj-3!P6d&t8jhrQ4=VT)Bd%cRg}4JkqtMVTFTnwgLP7|p zk|;U|$ZHbtJ@2oHD#;HShWKpmQNoi0YK!ofN6>XpOpQPB3e4W!qsMY}b*tjpZ_i3RQ#n{g~y`ZnsK(OxnGq> zS`*rEW-p&0qZG2Ob1P0GJOCYvx^b}BP`a@3fa_I9BN&h1=Tv|#&2Kc26sorRP{;0I zAQQ?;j0|XdW!tPGm*;d1)ifz#q&}4qp;4SYRF|uvC>iEzs%!zt*ryR*Oj9!}UtGfK z$F(t>n%XVtQ6bIDR9zSE3;Y>f#IzY{{HcBXB=)GQXzDQ4zS{k>c694MjD=Z}I z+Xy20qqF|j+Vg&%Q7jvK`3@7z=yZSe{OHx%tL-&f;n#-%pr!>UhfE!wCWEEP*`$=n z4FMVa$6G zoE0M%v0);a@E+b?A!lw%JJI%@yH9WBJ)215qF1^=*P0z1KR)I^T78~s+)pELkzWMd z(@rL#u##S`v*w#jQa?P+MF8#|z`e!7dA80ZO8RZq@~?k$mQi%c*$-14GP(1wcUP~M zK6!$P+wFIkBRgA&3cP_MSHTv1fu5>3x_0gpn6-kNTnz-as}U`dLb#J#*tj!mAXBg# zHd*NqC)hDpQMYC1Gi`cz*aIwC&F2!SwLTbHFtWfqFQ>;jvWWAIkQxS6X^WvJMyibg zCd(Bf`NbpR7{wpKVYt)Lh!xMm&TU7UGZn~6)CxJc!?6fc2NjcEDwI8P6auG`$t5D& zVT?5(zU6UrWHnnZb}b#IDF?bTw9s;RI6LKy$-fSDk1MGJ&7^8?F7aF4Lo6Pfny`sN z%v$nHb2pLSqkt$k2Dm#EMi9o?!924NeArgE4C|Z?XV=bEOdOABjpsoX2)~7xWMcAQ z9uSPPJZ>QzI1GJnRl-gAr77`+YrF?5P`k8Ay5?;IpLP@s$_cyxJUaKjnu7Tqg7N7592W2OX63X zX!E0$CGg&{0DKN>pYHl-6@wzF+-MPwL4;s+L~Im1A6k6~mv1id-pxxErAkXFgE-=| z3hF8lSY9J-P~D}fA_kg`1~2o{G12(53?@QV8IiTfd#~|osOXlue;D&yzW8+Las|~z zqdpHWjVMb(e5Fe=OxM{gUxXYclZ~o}=Px1r`y-ol@d{oOtS}lDZ5>>M+3^Txdk9@; z+~h=ov>Zilowrk+TrNXNO!M6p*U#^b?Y+6iJ@j=t2(Ht_HCDh=aspHR>%u}R`4F`~ z2IabE4&1T3U@~}fht6^Ju?}`!RAJ3!+2156>*_Yy=78|J+}-46HwX;Zz)7inforeb z8<1`~?BiKX<~j+&T?v?yMeZ)aVE4WCRf1bdNGCJF9{cVkhUw(n_cf12aq@p_+#jT3A(bTWyuHDMq{WMi^iVCT zgUAK=%f<}ly-6;e(@uG*1 ze*}D=?)K42Xqwch;IkiIpL`O{`jf|oN6^c6Jnp*x*bFx(L^ne_H-*~(CpEfFexYvb z3F6p>qYGJub)$R;)5C^HCVPWAN}JUsRsoGiKvgjb;wD;ERzi?eu~iK_i(QdLL$s1NcfUMDu3hV6Fo=Ncqy{{fD)bJo7ZwFN91(3lh3Bc1JZ+ z@F zSN+fd0GqWCzhJ$Z;7N=BW|>L!I(nnlG@9xtA$6+E0$`U##woWo!*s7Wp^yQ5#_2sW zH3YK@giGzPqVkSA-EK?y=UChUS=4FxaFm4aXPbyE79F6D#S+}a&1@-3uvLx%;bA1| zPcftcti_dWTd-&6TqRVD-Zm!|R*{&u_wlzNl%ct zJ0*Ti>yvAWX43`ZmV(U}KjuIG$2JG<+q=lnOVi*@uJLGco_{@+UICQj=>FB=;ni@E zAl~#>vsp2bBFZ~iM0&oDA(!;w?0gsN3+~TBwyI2BJJMBm>5{yjRnNBkKLCgDs za=RM$|4+H+xOjZfD`NknY~btNBx6OaD+^Z%LlhF}y)@-VAGzEebnIDhV?DEhH3oAD zI=6o>!wOf^6v?sZPBf zf_d@#KmKF45Ivc@D&05u{?yOkJ-fZWLy8YZif@je=5OowJBa_#{PWcODB!;8Z`|du zsQXHFU&&9scq4V%n*+CBB}sr9ls?UGp?AZ#$>+|W()NC;)WF~TW%HAp6dO;3g#aT0 z30eh|kxkTFHa^%WbP~k&pr|anwMZA^b@#C+U<$#x4NXN(Z1y3G3Gj>WuMnV=CKd|G zCfWl~h8UwN2>v_(H+#{}Rr)mWYJttXQg|NkBt~rjXMi(L1HN$yqO^!tt3yX~;KSn1 zs1^Y&5K;LwY-OQHRVQ2)P7l7ipv*zw9K;rbgdP?17KSQ0T(SpCiUq4K$Z#(4Uct5~ zBUu9^<}i7RUFJ0rH5fo&HBpSz3XitVqpMZik-}`g4Ww|~BRI=FaXtc}l<-+9e^In{F;vRyBeshf7Ne%S)M#ofB9-G2*aqPDJdcJ6jHQs8SVi z#X-4Jv07CIB&hF@$^mdS3VUo-An}0%g{G#1+I-l6wG|jIg{#6nW*pbB45F};tL<6l zan^EyxyQ{iWS^2@lQqPq7YIu@nIMEl(LO^PKGWQZU7}Mx1}B%;%6B5p=+w4N(;{(GmK^&4Yfe`8Yc#r zOAuH%!cU*Se1U9TCXRfp46ZnO|9QAcsS|y=g|8dWOOYI_aX@r?bcuY;)S+k<=Q<}e zbT}!b6=b%nbbVfS88Q4a6vc-^UnI2LqA;t0#-qc1i7|~!vqr99_wv)_aifB{rR3j0 zCShiHW{VvkOw^aQ*Z%seC8r&usZALtxibR*C*fex(B{7^?@dPHfi`gLXO{ z1Gtj|yf%j=IaXqai3lQ0aFW=;+<8~L8C>*o={-7sc=&X1FzWuIa5C4vpFI&e&_({3 z=)vNHm-lZ@Tm#|1kXv{EuL&3nREKe=Zi-3H>2k9)h8kZ#pYgpLq~NEE61dKvQKam5 z=)YlZ8LpDm-~8q^GWTy7K6y=X^5m1aMAVB2RBr-vt}5$7s;qVZxE#p9pj7M=jL~60 zTEe{T&Bt?hw`d$@q=?Ez#Ztr+Z3Pu>LF!c;S}+{O;2nNaoQCB$(d+lGOa+;LQy z6zU^rL-u)s28rQK0gL7N)Aaz!qKNJ8(HP1abTtr!q81*`WqcR|>v)fb!Q&EOMcr-qLuHHWQu9hH?bPv}#okuazN5h`Pj^nI9?lV;T z)ZAn~qh=KHu0DRODBR5=5+JgU;w;sVSzG1lcm6s0tA` z_+UqX8^<<|H=AXgBH328&}11?_6cGUufi<-E@Fk;CJe7LyCJLE#BC#CnPq`Rph_zw zLwi>~#k2($FQxY3K0)yYq4`^?<`RVLaJguvP(v^s`z-y%I$70&h-b z|0M-a!da~moA5xq+KuON_RcTX;7xIQi2g$ zy#(a+p`J@zPh+w4O)eHmTqGxLU#=b5N#yXD;Hn4?pxZmzay9hrUzTl3Y2(1p%G}`I#8RbfmolNlM?ZxLV25Iz?Sa6VxXe?zAn@y6BGgU_c zk6a{*!HJ*hCu;ljHgZ*aTU$OB`(7yA%HvkGAnColxH>@}dt9X6Ve-d2rQU~ifO~6p zQ{TFoWl~^vDL?h?<~uNMA$LkCuB#my+>c(f{jl%HRl;Qolv`)7AI?^PbG^g_oD&&K zz(rV{i>w4gX;6ZMY_1wYae<2E2}?GyxCsB`aZx1z3hkk_n?Rf)dE8a7)i1LoG-97d z*3(2KHbk>wv=XuV6(9Z6WTUUc+A1*CPqV)QAO*LJ+`% zv%DBI8X*PjRzyiue3{n|G-0q(oK5WKBHace3U`E?RQ;fIUn!Blvrh zG61jTkpdp4Z(Q+;N~#J2-1O1MHAN9Pt}b^+*>D_p9HtW{Hp zW)m|dc_b?aj?7O!fC+zYscoJE5vhi??w>nKy#iFtGBDYCxgwu?+|88Pmu%j!mbxT9 zAx@7i#w@fNp>P-q8)%|B;02+a$~?+y6mTTDP+7Ivf;u(nYd1rTs$nz)3j;TY!&UX5 zVpV6%7$*mK3MoK-3Js`*RD_(Op^=u&PqJ&NpjJ#A-61p`JdRvPF1M9KMz9~rEG^2V z@{@z}*vxrGt4ziTSuL3UR03G;wyJi8k3PPP&yoX;jzd@7FalAvF(#88?`4CcBqjyl zEVbA3(b$1bLg8!A7j%I;oo-V#>P%$?^fQfivcC0Za}L@r<6Pjk*yqRRm@R<2IqkP| zq*2#P7vm>=>s*X7r|!+w?VvqVNXNzF{75gd$4xJo&cUU35Jj%*r33=Q6i?y6J4}G) z=~-`>ocM2OdcVeJ7rdi9sf5)0(W`A#kkzC7^X+x9@4e?l1!Abf5Xc?t&r!UUH+6=! zd)|Np2=?mJj<0?ln@p+4CN3!MJ^Xl~Bs|dt<_S{&gK>W-CY)R)3C%5{=nSV1#p#r# zLWN1MOS%2K?TkMItyaxpOVNM?H+HreT1K)I!aVEH9yHwV_hrcZ0+Q!bd&o1K;?^lm zZe{~&xaU{iWNsr_gmKqP#U0d=3s#57FqjUuQ|Wpn=pE#eiQZ0<$aVk5ozH*2TfhC$ zRW)$(Nw;kIyE$+-cA2B#<-F$2J6FlGMbi}DAMcdn>j~rT_fu)UeC-~RTV{CLKYKlU z?3{dkhL_~D&uC!TOPHg;5S-uq@CeO>**Ir#?&IIc0L zADYEuG0{Ey$)zScz;o;ldm|ocsk{=7wse+21Cdj)2@EG^)z(`uD+b+*``A;O0ZV?2 zmLhM*fH!7ET;~Q7VLf70VNH! z(g34;DBMKmi9`5eID}Z@P)bGWCW_Uem>b|&3W39wEP5LK`J>9R^=WoU!y^UH73M;- zD>QQhk{un>AA~8!869A~;nwr6I&I2mCw7&=kq*_$taSVr2jo#vXEa`tL>R*8V(gO| z&LVW)f>#6H)iGXe%#BS#wvcARjg~`^q)=Nq?-G<~5;4FT9N|5eXb}ugV4@+D44JFD zo$*opB2UB}q>#%DQMUW1rAfUvq`JhA?q8oQ+GZBG{m8IBg4hd^_PfG#`#OyxkWBW* zkIj^9$0R;BuHFOztsBZ{lpqJeNOC8KIU?D^X#o?aww56G${VXEFOt19HvWJ2L zJx@;h!&540rDSYU#|{W|!$UKGc7Jtsi*Aoh=XE~QCl3RjjT!Lh)!y?}V4V^0`8-i| zVEbb_VA0FWn`!T1P`sW(`Gdv8P49{W{Z9kc-a+&K#kdz21l+7h)M&30MOr>%)9KE9 z$~*9#>y&WQ?{;=KVB1^%Ts!U0r4YFH_WZkhI5pGg-n$tB)5*!2wIkBy6{p1n9B(?2 zI6Gy^d*kqc*>EwYGrYlX)AnENOxrvrc_Dz0alSK%6%$BRr5T<+_Amx#)~K zXsBrieOs#4VUBa0`{KZN`^n$^=S0^Jdon-#VowK0oW$IveVswOLuC5ohwb#~BXIA& zK3Y3HC48IecmDb7-@JZ(hQZ{A?i@23C~2nr^X4;LeF0#2K2Q<_j)J&~fGWg2TmT}u zPlE2UYw(uY!%g3YFdHW)Op=s~P&lsIp5ai6l+hbR2cx+O7O%7qMvIBsvPrNIVb`v> zAXS{$Z}D`&wyfPzUBE3Z6qj{Siw1Y7bJLA8aiG~eLz27!V8YkP@J93&8CM@?Rg*0@ zq4E5Js4Iv?an~jix@BlB!3r+#H5zz1BxY~oBT;&{Y>%?1Am-aV5AqEhuY~lvSu}L~ z-)fsIgUSXNjVgi2(4^g)PjG3BkyXQSJj%}oQkYRHYQ35?58STS#w zb*1Xacq#jf`GQ74T6#DfK)z{&?B*wFJtFLztXP%~4CEC=^7s-R;0P`r>(jPkYpYcN zo9CgVEa7W(wJ674GB0b*Kr6uWy5Yds(Xb%cYSh3-?zSei5R9S4`y;5jmM;$}BobWg zimklvc=P6LkI z)_s@26Er0j9X!}1$Kr9s7alU**$xz6sWXD(#GAm=LlzsN2%~c>mPdoXiN>~>Evtt5 zJ*t86JoJE?Kr~v5j%!gS#-Fo8Bp;K(|1HCADCy-midK(3=VeK3TUfAxbr{@E`MBoW zxFH>o2Asshq&^&|NH9?Hc2Owf4-H+AGnT#JI_69&zTvFw`BEb zHFxpoDmk4Fr}ae4JDsvVOsCUg@9Ovwn%*1cAl=|&~dLcxD}X|sXgJb>&5W4!^j z!|4!nqdSC57`)xt>!fomom&d!_NPh=ch_y1)Gk|!b{yvL)A61M-zP!99v!4Oncz-I z)a$QT*L$NO)u-rNrdm)=7bx?u5+b**@RF^FB<4>jH>k?zG3^&qh`U;wNM(#nth`DY$4qdI#N)7v9Z%UM%Gc#+|;Q3eihn zPwV;ogTM4p@~0oA690DRw4<>J9bdYWr*ZPMzfJsWAAOs^5;+6^ULcQKSy2cku5Rs- zD^Nx}z?Lie3H-|L$8UWD++62D;@y%-BtdXRQm+~>;i$^+)4hUV#X_Npi%`S6g@KmE zjZ~)hxnqS&`oA{Gqc(w=PNC);i%h z@}=uV>`%8JHO zH7t$1jLS0;*4YucTUkiF##Kg{_E_LkF_;jxOjZ`bDXaywMAbN-5W7ZA_Argi#5!U~ z!Js9EW}SG{8PiZ2r_Hh)if=NzkrMHk*eWFVTGuE8u8m31 z{8@`ubdq4<^($rkfmgsT78Op z%t3XMWeF)HS-gv51Q+VQ*UDC#NF)3W%5OC-$6)y$b3m9(I3=L}rqHC7v)VnJ_%=8( zM={MQu>@U}+l@ZP+Lps}G3Y=>h3?L59WPPQKynU~fB> z7~a0Qx@uK_p+Kd#k-O{RHEB(4M%Q>%nUv_Ux7UGEAFZvyuKTi(P^9cL?S1d`Nd1y{ z0gu))lq~blkIvRgkAoEI(wDA^l>SFPI@wZaSQ{IYJ*4h!zn^d^iV##F7&^(c zIkmiNE0WgICJ}4e?ySvPI*E}h=j6Dc%0}bOn}cDI)0Vp#4t17_o%@|$68)oy_axWv zB=7Hv2`{~u$%|Oe-}dg@Z1wIoadrzC>a$$(Q_nCS08L-vM`DGs`54uV2#d@Zn;-r| z4-Gd))j-{Fh4;9Yivqbt(Z9ND1oxtn0Qrq0h&Cq zjmvSN)!Bq>b@OX!XhVua2fuuKq-zHofx~)kxR4tM5B~8i0I6|};n}|69ts^GSY8o7) z-o1%7T?R$f`B}D!r#fs|s9PjXbtyVJV)&FCTTH8msDss z6mb|E8bS*JX95n;T|()dbUNvFeKPf}}74idox#lhhO`QWaH zLcu#d0JydFb|Kf8w0(SMQZqDeAK@{jbjIz0PbmRRL>%g!>8UvBZr)g`o*X2H{a(VK zQ(CUJd6Oh(G?b{XK04^p5kZ^CFL;5dL4<;W{Kyc9SmZl0ow_+RU>Nb zVsOvm?eisqHwW{y!T-1|B%^;V(Yi3d{^-bW^=^vkL@zhNg_$nlV9&pv+&lvJy5a%P z^3*8%r>pstloJ3P*goqJ&D~D}+@AoKC42zLQ~_PPHr;&ua(T0;Gnc^|Jz_3|i)M>% zo@lN+cqYKk&J}UgXUW%Ud4z4x_p+%QWDpS{mz^29EgV*=2@f!YteZdo_BTHN)vx~S zOJDucS3m#xuYdllU;5IQzV?%EZHB=DcruS>Gt5m@F=47ewt!ozJ3xnv!x0wK@u*rQ z9v0}8G6yN%0m5u(NZBhjn^&At8VbjMb3y1_6-ti|3aS$7VyR0oNLr2tu(h%TeJgb;-z731oDTH1x zLq?Rzxd(d!Q#EiMiq1@URteZQMbI%$K@=q028f}eI%fggb=(0*sdr&mHo8G{Ov&yX zBZt7WP_^O^O1WeZtvMgT2yIdbu=qr(7+=VovPN>q*-f02wQ8Ao4i*<1)k2eMZ2a9- zG4N&p5qLy`cnIrAcO1|tOMu-XzShg=)iwOhk>wmaqrk43tCA&ICJ8@AQbqnvfK9$}*x%wLn_SryGGN zZ^uCW8Fw2s>X0lD=M6f9y0lV`N8|Gb(;2X*)U5NgKLzIoyZHgP%WQ9_XzfV38aH9d z{r054k?uc4;7XHeX190s6u6(F_y>0_$hKlej+G6CmVwXkCU}P*EmBNk93g>D?SrD# z_z#QR9ZU=&JHhM7{ap^l`y!!i!gM=_b^Gw`pWb$xC+u#~KEmj-zCSrA!RG|DMU-m! z>#zd?$kZkKnGXH#dCix6ew3NP!nZoJ4CqZ`CBz%trgE9S5CY!qBuliSC$Wh$`=_4V ziPTf|H@w9A6Ce0;(fa>p+>1*~SJ2&@EPIJyE}7gJ0GIC6Uvcgv44eP>Q&6DI`F0a& z=3%0C@9lB0oC@W(_f&$&k+njjt75$6M?t)fXo7kb(cNwM+SF@Y7`R^#f| zgZo~}Tl7U@^~}!wK_s0z@FuYnws(R#&2$J3BEW(>VEqCEdNl#(M^exX+``* z?M6Y#pqTuv@a$M%j8yGPBy;t`CB`DvLU5t7L}}sfoQShg&qRip-vpiIVQ%c6&=BV9 zj+lWbYZZ`k%v~pB8{636@*=8@q7sP4M2~G0UF%;jpZgzJkVKfsG zT$T+_@2b#Lv{pGFr3pXEIKC_?-9Xi0I!n`bZRZ}qc_}+OuVD7$%pIV#WkwNT!Rc!? zJS33hDiu@GWf30awxB7!WxUr6$UibLek{-Joz=wxN82m9iERTgDkn0#L0CD076o5Km~0m*Zm- z7^Rtrr0kI#j3b+)!OlYa4EqM_=_qn2O|ZOxuOG|yR=?8XXF+`zU&wmDfPMt1*_ z+ASk`NItQn`${OmN0I?3Xm;OCVGqXHy*nQkoJg(X2iVPA_uCzYm^}D0i30)m0;1>W z^d5*G&mXZsJIap;uidNLnD@XNOU4qtJGN;Zi&|t5MA`tj64!0uyF{}H(|(8a65c5& zganS>4Nkvg?Dj`*sVs|Pzk`njrAI@(C^EZ@mhYaPE*XN5>O5@jX!|+7zct?)v&+uF zXxEwX{D`i~we4T-;#<$>SC7`#&!%9Y(g8~k56pHSMB1nIIzEl$U@|;h)ZgA488FA~ zj{*17e#w7HB};F#GWBANV!o@V`E^|v{K`~vfWDLQmpX(F%zC@KvvitiL=+{(w12rp zMoEe!FccHwb+_;grh@a+1e#>L8BUH8!_rN!_3SBbFCrEB!xYGQKWrV`#$pGNo#O4q zY&R0bAQ*$N$5>9icfE_R(CCB+jvpmBh*7a^Y_i)ouHEd$H(H#g}@1LjR+t>R&CMMO{8qh>4 zA8{J~%R=A=zXFPq3!(ybw}FRyrIQOtRo4IcPrf0R{namh<7+?p$#>Yk_Kk1Ay+8Ar z&wSzAK(`}}TY3nr8Q!mDA#A9zhoYt$fYiLBnxm8roQ&WfxQniB!|y?LsWPlHY9P`z zYCvFg#m}2>W0Z0|+Qq4mpZ$TntYi{9!pFd9jL~)1cO6J?2-j(roN^IcLfM` zT_zhhVAAXWVIqKLkGI3d!N_SfCm^(Ens8Oeq(W&@{j;IvxUyT|drN~QLnBho5ivq5 zt?J5o*50Q&a~TeEtk_o|y<*W0JDoC;A`AvJhfRs~z(xXbKFO-H6g8Ct0scFUwSC@iHN zHvOVP;c+~w2+7M`wbK|Y=OG;DjIg(|G}niel6g{U~dpmMh3R_FXi^F}1@Ibah zS~sEIsPXcRpW`CIUxv7y*+}^^+g#%+XUp>`?@Dt^qm?A452vMcCpD={DtUzMr!&4E ze=jaR0tOd1n~j63H@893DRu@)Z+(zVBgxW;y>kas6kaEAHz2WHxl zZdS?j&K?0KN6!g)?fAIm=oP&D=ye^!E0w#)?8du0r1Cc5dQfgW9|wyD?ga!d!abe> zHy1t(27EHy`1m#nAoh|8bbDq72X`Wo3wPC!SW+aYWP3v$k~Ds9lx8V1N~l5b_rau@ z!PecUg%Z^yx0@m@P(+;26G@#GV!VkXiRkojf<7?mk`H2*|6RMQo{sO2Ca)0SF-~Ohd*$jfZP50OY z;?lLXI0Q6PT`D`sCLCrp2>_hBLoPHhnqvr993OZr9?T&kZo{6{6)DmTdk;&b2P6zV zk<#f>r5skkl_3qiunE-4frv~p3dwdT`#2{kQ5n;PzR1IyH3QvSZx^ewfd$2xk7>*D z5mp!`z}|T@uMTUcL!b2lpH7$rT72a-Qyu>W?Q~yDW*P8I?7J z(w^eD9i|t6!DVa_GMO-r%|QGhY!BK_xChEdRlz4LD~zK@6TvG{pv8UPLam@_oDYKu z5*`eQDDZ_UoS4oS(?(OLD+m)|3%JE%BIh(D8ps8L%Lbx&1^DFPdEKlb_canp!_;s~ zmJQ5)MzzbGG+F|wwjFvV)1Ol-#L>VJl`YWR4UDol%NPNzm<5^J!@Izv&D}gV5Mq)I z5_X8y2xRA00tjIFbk2mjbZn}XeZ}UbLx<;_3Yh5r259cta_4%I03jp2wn77`K+f8!8zy@R*l7GaylfGy&N=!5B|hBb z6W-h2*olz!%*Zhq_s$%+U+o>OZmp9cifdvo=hci@RkKdBHOTx}O zzV1*;rVmhJN_Yc^XR!We0mDxR7Yn8rVD;|Ohg!+gnVCxmLE2hML(V#aCAclZP9lNE z&4$K3!9qg6J9GDG@tjd8#sWCm<`#M;Ujf~;>=VPwbWja;e$=>&LuwGrWEk{?Js_2~wf-4tlwydYpn0q<$RickS2(&7HAC@~+)Zc<=pt z+liYAy--8?pAPrC&BEOE{eDL&zZS#mT;SgCd%Y84x6{)}n~azYO;KBqE$=tmuk)Qe zrfp>IvsGEQnK*d_|Hje{-981l3(6`H&DMrU@+gIPBu&plk4;x9aqj^@ z`7wXG2Voe10h5L905fnm&pMHp8=!^Ynuw(!EocaQ3;ZOLP>~dBAM4K>_(N2VB8rti z`GTPPGvE0+&sGnOjt(@HR}h?(Hlq2%63SiWT*D5)O!)vwK(@aXC4~sGqfVBWRWG?~ zM$hsrr5-NX0TorzzO{~+z)`3l8!wmP@}?~dOF*`c=op>CkO0l718gaZLNygyZE=&O=27A5z`ca0 zXbt#WR?s))PD_0_P3Q6epX4r&dnA`wOlL@gRvvG7_DU**`M`VOht=kRF*elKo+ zf|z1avX$oqn|tN*W!Q#Pzf{4MlteOf>B%8^8}N3}w^~M2)S}*nIGG2LK3miV7h$ANfcrY%$qXWy5`MFx%-F*2dLBSNgIwpSMCSK=uso*W0OVK! z&lrHyLHTBQFu_Lov-Rfyw<|j&>vyQmgC4E!ISds$#a#X1eQO^Vp7u??1MVlx`zhmI z9R2ZOFu|mId1#6kHQgyQghF5-DQhq?i8wqUWEr)0tW6@OpB4T|cw~$IqU9Sh;+k}vycdO-u&I^D(`+t#(2KPSIO^?G9oBW5%ZR8CF2K!laRWJgR>0O+o%8hUs4l6d zaF{hlW*cmWMmMoIhp|!948zTQN*L6rusR_)uScO&B%UCoD4^RxxY4v?DxGL8RU^xo zmkLZC%U(7N7FCF+vQ5%liSI5;HC_&5`oKbm>6~g`#e{krXfqm%8a0eL6&zt7U6kagG0YfF>t|_Pd-x$o*K3 zt6mv$;$5Bwmv0GoyGZfGR;`R$%;z)=-m7ggDUFEcRxugW0BqJYjdlo>+o>%?RNC2rRZhM&3CEY}q$y5f z)nreoML#p4Qa$_I4oB8^yr`L7DqxZXog^;>A%=N+O0bAmwdK8SKtkD%oY zViCM^L8j_lJ$02l8G$p$XnkpWa^XLY(wqbLjMx>JZ4ZNWGS{x>k`d8uA$J0J2BP<7 zfHqdZ&erbs_AdO}kG}+;U@q17Zyb;LIXke^5}5pw^21aU?U zcTIrOxY+^%YLFOlM4F3mG;6r+@KWRj=;~}|2!zCMoGhZux;HG*1L`cQY=Wa|jI=i? zast(2)JiEqybf>T+g9q-d{kSrfDY@PfFLOIP+Z&~H1a!gwNegQuWb6~UFpAT^LDdY zr%e0{L3bwHR7YpwDUlvQMmN5XI4F)(BhdF$h)`|T)XN?o3+Eu(V`!}*M+-P!tx^Yo zeKrG4)DTvlK=dSBGx2|zG)gWZpleD!YIv6_BdG+8YOhKjF>IQ=!^Y2WjLH(HS=Ru6%c0teG^C;XaZ=VtpIs@} zmRVyq3uW9djs?}0woWu*cLWDlAA6h@o~}b`3CU0|Eon8iPmNl%#lN&iU^%80O+6kl z7soP*`>yD-qgx1lrO}F;fH``4v~c+8R@F@sAKBf@GK(oI&=Sp(w_`MwC}Pz)=EA6_ z^@l>dN(rWx_~k27pOHw?ncZC`WHHs1b8(k(gS4}B>a>%|WjYgB-gRbUE%n3>$o5nm z`J}Xw#+%8YwBef}HZwwoCiFY+K;WEYq7EZ{uZIKqi6_ZYCg#QRCj-l@gT-Vxy!!rC z7iDC@;1*4nAK&1aEbJK%81B{;w~G_zQ02Yb^6a4-1wVs^{|ta;eS8TaB%9A71J(t=c; z?5C67>EBM7*8T|GJqikwOs?eLqiqtfPHDpNS|-iny0z`k_#e)IY(l^k;h55}U?Df7 zB&IuLZpZ4AS^Momo>@fneQ*$5c&chiJwf_@dv)lg^@%+Oy~)oN+gRuCy%D0%+sVU8 zW_B@T^h2;zto!%K1;3-_*hXmE+KAz(|HB`?DH{L5s0Q}m?d=ZU)CY;Jq0+|ht7iA=p@kX z-?{D?0rxNdRM20Z4ZqG0P`bbHg|B_@>M|~J?Zl;+puiwUn+_RKy0EM<@s@b(l8Sgj zLLI&1jx(OKd|q9*N$+7Ff^q~8UFKla&TiXKR(gWm{BT{w+QCy@6AWHQS3%EePJ)6_ z;(D|2@;IpL?iI-$9$kr2=%YWB`HMUPDLAY!Z@L-7BF@llRFHtGwu;kY7A1}7;Y~CW zb%+LlYd)`vwQM`2V^xWBrH9*61QtpSu4J=mtvF~CMgUMoC0UX!5J}r)W-wBJGb)$5 z%U%t2DB;A`#ARmP;B_p^?}t=Cvbu%Gh3UK1nu%0UHTg+6b{7(Q4&{y7ROfI;OIHuk zH3gpGUx5Ty3ouynKpZ%Fh^W6V4jc8Evg_Is{f^*z7NUwZM|NwcDX4ZNU4z~hl2MJ9 z1U#!ar)BGQc$s}0({*5UIkFV$_#)ngPBTH%T(S>dkco}ensJww60A z#!t&=jLb$&oxY@}5LQJrEoU72E&Q0H_UE8)1}VgZ+yqRuuP0ku zY0MoRNT6@8f4gX~aJNMbaZAD}gk~AU5={k8q zcpvl3)5V}a2ZR+Pw{PBdIqUwomi<95%K}R|70=9y>He(qI+$49*io!2aOOMO+XJsT zEnnY)Td#u7FuAQ)oxN3}``6ZC-lTD>7iX{Y>dXz9QtwVX*{iD+y;rISd!xT&OO4||g~YZyg8D0h+Blf&7aynt@3>sAMd_5;z>iPI^@-4IxU z*6qV-zmf3uG97}xa{e5RmE5S|@DO73XpJ7et_Ksd-rL1XJwfe{4ToEZPjkBn|8O}K zyS*ZUAaX|~f~fFG$;&!eZ!2I0`-xgUQ+|9%!AxrJ#^0XZ+y`+5l_tYC4ar;D%@f`G zcdedpLSs`W$T4PU4B-DXWvjcxf?xOBrGB5kLuU0<;ZhTNZ#u*4qu=}n_Kor_;1RpHGF#o$GVO}nd_P*IxFogY3t#%$?++VIfC5sjkXA>H901}b zZa&4R%!Lj}wA-Jflvq)2Sy?cmuy#n{c1w^1TREnNXtEAj0d=?VBs?UpDG{EH+|7R# zBy@R0gxIigZc%orii;xpXwHCkT#Hgv-ju960|KhYS`vVOR}tC)Y}A&ykup#*vWibZ znc-97GCN|)kxF2@O{9jnx6qQAFBD>$al%V852N5e#b z=^CVya8M}a4WwriHcYl`nDx!BK&7oRZ=V)XCOwdHf^(2##p<^b`qYT%K3)@J$qT1S zfEF}46k^e#tJ0z;QjO~@TMW^mx{2My3aLSURA^ciE@;kAx7-@j$YfHjtYN-1miIpw zs$psr)|;1f%Uf7r%z}X@9L651jM1Sso7gWM>Xfarr%;8K+pYRGJe$*Te?~daKK@Z< zGMEB`u^7;s=ud*l>$DHtPUV6(soBQs-#!BOztc6iTD*H0VWVX{*P{U@hDtijDNV#1 z%#jdHUtS5pV5+_SqAQ!c(pEzr#3}{xHeUCLIFr z2Ax&1IkaKy+nQ-a*|rZ*c)oSEx{mj6eYM|LOm}CMfjAvXlytxMHNEVp?hXs4URal9 zjl98RDP2sG8SAUx&JpOt5jgs1`@~birNxE!81BLZ(@5ruY(ZEz>8|Z`aVMR-c;wtY zX2*5<1Q5(%+oj!&-pn^)_6A0 zk=5|u-ps!{2XUU9EFB<=u#iO}sT_UvL4=uU#Cvy<#5g{?nYE{MmhSo!#lhXhAd%bf z-_3S!hLgb+aVN`jrRyDV*U}k(znYv=)n5Fa>v?7Vb?|6D>2Q)UA^-F1K~2BpMreGPg0+ndjR_x7&{+w+`*Z=Sfo zJ@5M$a;rr7)3=8W*CQ>D9#%WSEL2x}GUOfM8WOW2SzQr^UvOi&1m|x~rAtA#jMXND zUH$T)v!hr;9Xh*_f@@oPx><9;WBDAprqPmAF%7!jkrw9lRt->7k0iU%0ikvX!_hv< zRXzonHVO(QPtk22PMggpw4W6xAg1j=F+p|H;gKd~F%6mE3&M>4Ymk^HWDhmRFIbGK z(c=PQ2lU?onGLmTYDy0JtOn!OqI$)shAJS{Q-VbQt;mw?nDFlXcouyDADlu`HDIU` zLX@W29W%MjcYtz%$ziYXk~K)UWou?Ze2kg-WvhV(Q*~a1w1zNDnDlM5bxr)IMV;aE zq)JuG;<6~j)c8U;u+@Rjazw!1z9nZkZyT+KcNMBh33s6HfM)@F_=^foMR$HZ=?5=a ziJ;%KNv=TUv2h3Va*Z)F8I(BNRQgGLTw9^|>5z@`l3Bn8+}*TZ)EFKy4v?tLI0*xs z{+UNqjyVs??H3i_@OesWcosv%#j|5K)hv&@5oCWuG zi9%pL1M`VD^#9N=Q>o4@rJ_w!MR##pm4+dk_f zai1~W{ed-hwWE;=>1$%9fbRa}3|Gk~Jnr}!0WjO5-(4_0zaZZ=51GTa^p1BuI=WXI zDCUxG_Zfoffa0B0%yUyk=5TI=*@Ye>(HsSx?dL!F5`6ocS%CX5zks2*{YG#9z=!pS z+pf+nmHVsTqo>W0!~xc->?j38KrCowpINRrhQ=m{io2q_Zs0BvZF=wpvhpapzH3|L zoSGf5E{g~rZwVywYs=-x`8C(MO>ACJ$z{l*NW2`9a-nF`r@?C={HWY&@D2WQY$Ow! z60j9y1uv#tD5r^b_^AX6`1+};6`R7SPWyHsOWTKC1AZm zw6CHdTpje5@mg^U;IkDWCnB3vLL}Q|_tvHl;)dr7xNxCda{z5(ohb4Zgx+&%&JT6)kFZhM=m9A5AE ziSqZ$Pk`G6Ajez`I3V{MUBEA4F*-+>T~}HwqD{t!yQA@_oaR`YG<5zRMXUT0^6^MK z2tIPgB%lBEbDl6l9>+4+F|W5{zEvK+p(E0AR-1LwG!G8=s zlqTV79s9_7e)jw**{+RtYrA!BG2ZOh zQwZ7_QLV*qtqZqA2OCjJ#R9i*8iX7c+zGq^LIJ;MXHyR9?rq7m-@|8eF>$J24DuVl z*PJz#tGW2qzx@Gpf9d02d9?eX59SDsIft%ml79rS>Qa!+BLD9vL~RR(O|J)#6G6fO zT%2Qa0%d3h&{iG_B1ZV*K|YQGd?YYbmx>_Tv(lrme1?Avf3l{^R#fR0m~*5CV!5)O zCgR?WR3;LEGLbrkOhImdBuS2AN`MW70c8@^$VehhvLk{>ohFh)cBpYr3Zrv(*-Av& zIpo@)Q7Mt?lS5fC1g^tnt9mU;I4K2Fs1dQYfU_rE2MA_+8txh(nwJxM0B?Fpx#4!R z6GoVVkgrIF31nIV-4h9FR=+uv>}yNEC_6K{S;|p|=PS zL+q4PZ&6MQX%ZVjOd@L!q3b57xJkAq&<+GcYZCxVyhu1tsVe}P1V^$Zu{Ne&t|>K8 zTuq(c{f)CM%2_5IjQzr6mzmDkuhMAhf)mNl$`#;jvI;Lrz&xjQ?`x z66I+br^KK>t-MZ^@8wxhq^7Kqvv8C0Ugl0pDJgi0JYY!4Ebe}AwJ3XIB@TH?tsitp zDNYieW1>nWF~!`j(R!YC7!StFICziqfX92bYea~+We%gpT zI(KKA2TQm-*=}fXQotQ%sB0R-vMF+H00+Pz2}crML|0Oqo|dgRTf}mM-BOj%l+4l$ z1ywL5S}#Nk)nK6&ZLjoU*#2{hOzGcXz|Dq}7ViU#%wH+DRWG*XMKT^NRLQ{!!9wtl z7IH1bqvhE+W)^T(sv%R8mIrG&*#j5CMB7>)J#Z~fx#}8&<)v{+f2T#@?Jt)fJ?;8v zW{%4IseA3CHXXpYL)DvRS5GY;93AZ)HTg7CiO0*7jQ{Y+YyO2%R3~{H5W?PaIMWVI zit+AZK2Zmk4|(cPlVld=q+56GwTx*J13P zWO%V0O`;%~TU51lL?)!*jNfU}nX11P4R)YA(zdQIhZ(L=}O;41gox{>;Z;di_&F7&$Q}*~>E{ z`2OmtK5#Qq`S*7oU%Ucxf94OjF5LRy#0EFPi-}UT2=7%b(4_p+hJ@*G*xb1kCV>FE zt6&!E+=1*PxRyI>&*V1O0->~~{7&{Jpilc|sGlM@3nWmOB)EG+&QFjHDgGT2Mn6+j z{VykdqX6bg)zrcX_e4(*P9e{!Upb?}}et$?*8p)DEF_V|r~znuRmt zjm+HKdD7s=*QOV1+b|_(rX@Dn8K0^W^9t_$)jT|RjfYF2S_>YA>Qa>qGTZ%|c0%p} zE#S$Bdyr*@ufRJhy{2|zXH4RSkJp@CuxVxhOq-spp&?Tb_@+U&EsT6pC zd(j2Kz1XM%+lb#{$U1u-+rF?HIor#%l|; z!f5Rvv$NmY-8(RiafpK$CUu!4GM}}^G?<+nZ_feb4V))|?sxVR?-J;lmlccjVrpQ> zqO?%f;JE>481vgV(48| zaWL!%4FrGu)r(GTv5iNMXWn&}{K+d{xpnmN(QaVKrml0FU;)sr=v_AfHj7gh6e12+ zJZze-Wl$Ovw#o0Hu(H^N)~KK9!=sU-G$<*yLS~AE?&|ue7Kr?lx$5dQKdH1Pe0ty@a2e%1Rl@Fwx`Yj1oN+pohLqrVbdeFKP zxRSxRI>z6kydP!j6mLbKM8Ml3`*}o)?2b)JX*P5HgPy zaSkm)mYmxBpeltesZL8e4K0qc59#-NR(6-R_-_Mh#?4QQ2;YS&e6Iqt*lh03BjZ~k zjo%#+M{?^H!cGG&+ z;McH@jGGS>Jt7MClw6OA<;!QzWWpKZJ8Gb22zXpw$kjPH;1F?FkK2W26jQ)p5IqA9 z$LJe6-w2SC(7NAB& zf}jUBf?|QHMlkDQ28}|g0zIHi(2Wx(qpFm0?BHtQwR+O|p?~-5`NNR^ zXs;FZr+;mFjOGVN(7^I*54?GiIXs<@%3Uhx7S4u7V;>0ydCu?y0Rf{cUgxifE#!kA z4pk+FsR#EFtw+bl3!%BSSO~ieHRzy4gq-22Fykxd*I>4HaOMvy+P+gG1^(W>8dQle zR)D&}L}+W1AcfE>nGJo6Y}@9(3(VCFoSO(;tglWWP-lSym;kQYQ5{0LoIDtzj@bl^ zt4rYM`{ZYCf8g~uXDCzP6eO1{{g>7e$Vp_n`1(s9c;F=oUYsiP9J~ct|fuuRAe60$2vn1^9%C{5jg2`2-6@ z@Iq`+=7uTZY#vLVi%J3peMX@!*|#APMn}*q8K~%>K%5o2|#E6+4N6jMl zHct~w5U?Bzd~iuwm#8qZd{8q`HN%m4xTX|wI<5>j7weG)kIT(K#3%->fQ)V*%C?-I zTcQ?H1iHlHsd>IA#M?L_gHk-TN-{#=vC8=34B>T-q9aAF#W^>SuW&$m2vS7__4F7@ zp|q1&n?PDm4N=4sQbW=sn@65nX$*=JKq>;0qxTD#Q#tuh2GE$0iw>mk;y295AZpDL z4iOmhc<`A+WU?9{zo!)8gWyT92^&ANv`n;o8qbOMEd}O1aOMF0L9O72d5XwFa<~IrDXqwyWc+|?0Ei$Vx<&?{jzGc7u0EGaM9#s7 z&|m!Exz&J2L+uc!~2y^3l2((_`=|Tt6F3wizlrtViPns@@UR%g!3eb~5YE z9fZzy+y2(U%j>63pod)_{53!LX4jK^2^@E$9i2l1eBA0(6*=oQOMWDZKZG*V7}DaF zu%p~+5+#-JP7UtH`JFZ5JS5(7Rd4~^;#ej_P0FMAad;OI-7bfs!AE^Lpc6f7;t&r5 zQW9qB;Pp(Rge4MiLTEH_)6}Hg(hrccJ*uQmtrEO^88C(#;p3;)F5iJ`6u0-qcAWBG z?dU8kd|M)I_){&>}(ory_@W#`zk+|_8yGv9Zsg4!-U;R zxy+`wR$hDU0rkB{rEobqoW%_@2Kip-%1epFPS}!0t$lXyoPVLJabCWpLNr=PHaayD zS7)t`n{t$mpoSVUVZMtwXp~y=oqO91Opfo!zbv_POv*dRBs{jzOB1wOzjKwR2G0OD z5UmI5CYWZecZ7r};!-T0tICvJ(n~{wY{(hPS-v)w4^Qw0+CFvTr7N#)%-Dc?5V)OB z()?F&m*L6V-?;Vig_m!=&zxb%;F*<^_&`=epom15JB~mSl$Bsag<%y;EZ=$VJG#&{+mr62h;x{Y-191jql zll)bDyD9}T&jWjS36&rP5#evvJu{H^2r#i{Ah{A!2)#Mf7D^bbkKH*SnzTuZmF%7- zKU9o*3J zE?v^+p}cvx5uhIrN=PJ1VsUh2cZF0RDt_ayiFv{nAU?6aWYp^E5d1ct;pE{hpj8Nm z@y(*nYaBTst_Iy`p;l|xNL6mML*W=z>Kcm&WEZx)ahOZli1Rw}J^|dKN?N0;BOS!O(M!s@ z=y^D9ssm1|0ynYt;uL{3z+FpHmZU*c3(JiY4lMWhI6avusY+~x)JY)bIGC@JHd%*! zCY5~oM9RCCG&L--)11mBT0SX6v2EsD{veN)rkB>Fx>lzijNvP0+TLM3z8Hh7%8Ygu zZleaoeiLyZf$U;CO6W~JnW(hy>KV0zlN43Sm&<$=U+lDJLyzoro)6ECMg4P4GTEm0 zx~v}bK=GyrBqvgQt3jGE)Uo{G2VTPc_FTAjUOxo7!MZ%?AifX6$ymJJ!TWhX)(N(^ zrxqKrsJC6~c4MT39@GWg&@7mi)pn9$293uhNJY-g_`?q0m3}6j0k{=$Cj_+0U$4^#0k}N9j7hIHf>slj@Aj!T!9VdI7MHECo6VfZ@+QW-#vr;@eVhdl-+(E( zqJUf9@>3$F?|yZI)&9q2KF2q&K$d*|+($2b=;aG3&Sh8+#nnnySE_nKr=0=hJgiB5 zNQf->RN*T&o029p-Jfzkm7ZflZ8J0(fW^t#awZ%k{|8||%CF&biG>k~< zJ@*#zNbr+RE3cMPDP&H{s+sJ25=lv)C*Dn+p7*3wI?6p0fp1~BRhYi|J~ug!#Wdh8 zLjQxL_rPECLj&)eO?`w6ej1AhPekRfBD)C4tw(+&MIo9Bcsz_25#I<-5g6M|CnQA- zY*Fa|K_i@#?5GfLFS)sm%H~SCgspw&2MEh3gfUm6Fksl&8`j`1y^049wdeO$EO0 zve_=8!D%Oe=6O`fphV(PXjYX-r37iU@`o04l^pa$l~ZIyFm`;NT&ZpHSQ{8C z)M`8X<=Qqr!r|WFq%gXlEt$2tZ5Hih!eawygU^PN0CzQx);mN~q;T4!uQ*XB=xGbU znh{L^l?5vZXO+#;0^az*>u9zI1v>8nxYgO$h??B@jn_$_`~+Odhb|oLawc%p(&eH& za!?lc2}Ls(wW{@olt1BOR5Sjv@H6^hGhQ_m7pJ66oPPoA1GOE#se#K^N_fLs=Ne2RWi z^;m?8)@WzK+#T^a*Hq)_1TUTPX$XSh0;zIRfP0Q|hMa)Hf2tvXfCjH#+1S8#^W2it z@OqA;`G}BAL!ZMm1kjnJpl;TSM6_u1RF+m>4d`CU0?ttJ_PV4Ir*Zqn4PD1z>2X-aFa)< zEg{Kf@$?5H4DPPZ`&8XLwO$;WK!fcYO3nMo(t-3)oy5S!tSL5&UAz>8V6!iRb3C*U z2cpwvjJSv%(q+@-e^Nq_Ed)OdIx_ps_L!nz>rL5!0Kr@ z?&-DZTCJgJ_{^E4JSPR*e!sU+MpnWoYcsDuakTbZ`xofnU}lTGp_5^&d9iR(0%L)@ zPQ>l+;2q|t4!|T{Kh=7#IN)A2ITzPpvCI-&8CPbuAQS#ka5{<*kTBat%@UwJ#yD~# zMuIA}1@Swhe0i(YDhI0>Gn9H_DDuCe^)m?*9l2aT^5@UC5Q(RC2wek5J}vs@H50(S z^|jZ?uv`pxUA5WSTF^V2nXch+InqXaS?7lWhQ_f>x4u)G-I=o-cNmQivTmpmq<{_C zmaW|zpBf)`ZSOE3R_G_j?vy0Ml-*^OWU$}Lv zNY*A0IRjqEEE0?Zk`0FIB`{^Pv4Ce{o2qPUYS**{5Y~NJR!B69CgyD@jy}S(Ou)?k z3ee4d?jG=p@-|2475n=A!>I%vdQ$7nmJ(3cv0pyU#*{#TWX##zV z+apIY7`({(SgJBa_Tm=hoS-WJSpKFo^2JSc5JgKNPw7Bo2a3{XVsn#4OvJWUmw@n` z&J*31JkADc@*btVQ+?N=NW7WG#7sW48#bL;&_%(`Z=>miwpaZS5VirFls4C0`iXK*dw@^%s9=3A`?D&{BVuTPCQF7Sg<=Q3u$ljrd4ia*w#eV~4R zJGpT%-ZmU}^`O>j$3k9ohEd(P;?UXY>8YKasj2otEs2O5;I2(0EfJ~RIDdW(&RfHl z>@zMCoamm-z`=ddA8G|d(KaD>H#kxVTi`xnfMai~H$92nv}feJNC1CvMH!RTauXdO z@-dEANbWigaSKw`1Aen}3t<+cu7fy9;(~G+%W0qz4msM5Em8<~#Fhty(D66@ z!9tl@CAx|;>g(Lk3j;Ts?Xj!%qdZ;>xDzorIl!^>QOn8P`E@_za$*&GrKTSdd^_JA z?baG~a5$?dc@e!PAURoZlIrJ>E$I z<3!xxuu!*8beokYIXDw`#ssiaCZq*r%NtmdTN&n0geU~B0(L>Y1({@uk?;r4G{7H}V}GDymaj6U2{CK1envz zWQP}hWy&xmu^GFZzrkfXkoQJfpKh)9oNm=b#r?2RHY;Q?f4MD^&mJ{7sZIip3-UQDF% z=-F~hIGz=2FB`QXu`KRlTD*0d;}Xgso!`VwK4(V_Dxk5)Slf>kBSWA!_L6|BPo=wI zKe9vA=6FwnHnF}nQQ=X*=wbqz!u=lpwk-xMC5M6D;S@*$O{FXZi83#e*5o}|DEBQa z#8Q~1XO~WwN)Rdm6e*`ukizS#Qai5<8o9O;!B-5ordm&KWThs1k)(QLjcKBH5TBb$ zN!!D~vC+l^5Gt;w#|V}^!p>Dlp5l9!Ha%ppx&ivJ-S;C72kXJf9yzJRrHuuVr_~>lq z@Qc`jw84C0YdP>9x!fm8%^v{V5UDCtl5s^*mdr+Lb7Aj@sN!J9znHAY8@Pe*AKz&Q z+pcK2dN@M4ep0aa{CLiMu{E-?va(z;Ly*tVT0be+-N&wTbtHt8wm7J-_hHRKpZ=W; zv}q@MpOHC9CD<}$1}W=WHSPkUO2(&RYvWeMoy76|yv8VZPyKVR-p+O+Dgdk?xch8=tcISDwLooFXDflixQIRLopP);KxIgyf- zM>#Hz+W_~dOKbz69gBCv64}kVV%=KORVQUq^6uCuY7+(<#DI@Zj#xRMzYWJ7s`^oA zmS1`Lm%$)JYyCyQoBb?skM3O<2~Fa|x&G^?aay}Lz&#)~?BREoa#iQf`K$5K;6f&N zwou)h49@1|Mn4iKN0Kk1);;Kzb%oDPqC$ny%1l$nH4{rp6F9!T7;BM?a|Yn1n5rUJ zi_e-`;O^CAQVkO!u`wHyNkT6Nhj}kIo|ZKgsKkwcZU*TURWngzB(q0*o6uWOXDDF- z_p85t{5PJ3PN`ZmIGNpG{=~CQORp+>@;4VQ5Od$U6*1yG6E{mfEhLtTTOqV=)NC<2 zK_mOnnhBHvXmv5yOx>D!DuxZ$o;FodjRj!Y8IAO@E10at#jN5{vA9KR0%In^e8Yw- z5mT|<5HFp|U^o871f_gtoQAj#I4Rh!z(Gua z>}d%KrR}*$`E-ve!dy&miLxP%-ncjn-c}M~tuH1b5l*V1ONKWC*`b_!gP})i_rl6l z0z+B8vJ$|Z%_8%V47-%#`3!SSNX~ag0*t=L)M^UKMgtpU?J!hWNld8JwL5|OPx?L% zNsVF5U;vw#d1;;wh!j-;=h@)d9SfxA3EMZ{kJ@9&ow5b9Je-P$j(#7I*GXqF&CSB9 zD|5%B_M|}TrL;y@hf)X-H&3U?naBAJ0VH@Ifv_Q~W{hVIwmb7g{T>wpms==QdW7V9 z1{WFajF1R`%alyp7bIqvLF$EwZvVV8N}Ic^edNXi>eW_HNG!Rj3ovrJ=WZH~IGwW;mt#u>Q=PXz_sw5BQpHp=x@P<686 z(fXlW90h~_S*g$z27E5^sh!N8WJzUR4`4~)rx6?G{rOd<3>ElMP z$kgmkBeQc{BUy5%Hdoswt8)CF2oso-?Iz3P_fVAxZFn0wbU*Fglp>J>Lc(St0wBU+ z0r!x)Sv0Us2jCu1Fx@N3-H*_F!;@~4vPjc-B>lZ2CbF4Coc-#%H$L~JjVFJfa69qW zcmDLHg#D6PH{2k=O{J-yAH6KxzOZENYz(U%;Cd#rM?x3Naac_6QzSk!1Egsa+L10a zgPPJUfDX`U_crD^w#{f=m@P9)og94R^kU2Hae{OjX{AC$tDuDGU;)-xXHr&^Di`X< zxvbiBdK5Uys?>Inn;}fFQlE#I|K4<1RrH3eMZL^|S)T}K5R{{pePoy{64^c31OQ($ zKVkrWXygjR6QCh!k+9xiE|oxunR%f_G*-o1)V|Zv3__dS`K%AO5AJgqa3;{sqWI=1 zgU?x(2f+kvM{0h{*Z|=kCTnLBSzdqr6=jt0YvFTtY_fFimxHK7;l050I6RcLNeoY>M0t8ejp#Ei;(? zrpwg{k4D?a`yIfWy!UD-kNo(xS01gG_&(%B>r{es<_bav4c}>I_8^*{tOJUN`7MVk zv61wNkbB1B2y?Az%6l;4?m4VP=VDEU?mDe#A=sA#x{_xgvq9*_&YiUez&(eaL~`!r z`6)yt=cQq&u?E?Ve%Kb7VsH}AfY9-NCU4n*7o$jy|u(;WSs^9+0X zIH(fFCRGc@9|RN8mefNdec;Z^r%d`D{D=h3E)^)rWL$Xh$E%~WbkE9mvWhuBqn4BQ zp1n{zm}62hH|PpIx;RLw|L60B_y1~r2&Niq$2%!Gpy9YJbpNMPb0Fk)2%`SeJds|0 z<=o_PINtS_Ywgi^oEqchSTa^CBjgrvkFQb1buKwhK2)vT#={I|d*@yw|wAT1#jveC1ESIL4_(k%fs2oI9Ym^E9yTStflXYP{@!(gU8 zp53*ZB4PpeT?QUM+5fNF@N)0^)jRL}>T$nVji$f<(kCr&|K{lELqhIdkF~RL>vmCz zgR){qdV`)O+BUH^6E<>bNk)W~aKJRdcE2eWXA>LPS&gaQ4j~Rj9MBv=h&_a}6ZLU~ z)pui}j7mBSGy^q8&LXzNZJ2G~oAUrhVkC&e6(v(!5L`CK3n}5~u-@4TeR6;@Xw3u2 z5LMvz2AY2Ul{{iGwkmjCkY|~#DAQ7yPeS6w4LU)JtV*KDrC;1MmIykgM37CC3!0l- zL{#BPaF!Cl&37XnB+VTd|4>g!2%i|D9^K=Teq?QkFexoFyvKgZfZ=QqA3QC5;+@~O z#9$>}pZqW&kzJo~nzG$vdXA!+0mWgRB}Y-UT=~xXRE}>#>P@%8?DUd!kI?FAo+#pS z$Q-SB5TX-#^RbW#Bbj7`a2hnG2X>!;o4wM=8xiIM_bee_M9hMo9Nl`Lf(OqQG90ns zSfg=2V=UxAZ4GC>Z?lLf9a9J)JFof3JY`LARr4&%LjpH>JUWyOYqB~MrEDsgS)?2{ zC^44-uo=lC`esxtuQn1oPKFER?Ha}qv+OqC%k8)R~&ho;-8}^OZM`0T;2h0hwd4`?dOo(YqSjKhW&K&;zMSy#?J6eT0_vc&deTxK#)E%si_`E7I zwynB=>S~DGp?VZWDXNiLW5H(NbbpW>$KYZI>I%??-ag)*TDx~|F}$`z_`QP(&N09( z%%(9J?!)gBDK<=rCc-XI*MW2axA0qC+TEwQy8C)SmcdX{(^Vlc2WdBZdZWI6)dIId z6Gy>x>PvtB<72uU0Bk^$zqbEfUw!9Gec*;F`4GUJ5;7Z^xb93=!abb{OZ+6zO8{#@ zT8%lA2ye5Zb*J0z!GX3zJ?226y6qqoqn9PS%p}69bm{gO1%W>G=`7-0bS!VT$7qN%H9tj9)J8EF*L}dTxpmOBIzMIo8njGOVn|(_~{gDBT^wZ*D zJV3gXjF8XByVLTZhwdhV*J+s!L&SKBqGV)H4}Hbo$|+RO+kD&*CQvZkq^cB7nDNa7 zku_iG*ayLBNmh2@$a9!&rZgBZPfWh9#F`<_16?AqIQErpYP3l32#3P;c><_vHY)}o$a33DMt5_h z&<+s}JHYM4&W;pXxBOgj-AR&L!0nF*^Io!;sD*<0%^ZE@=Q=fE*J+`O*D=eTH!D2^hv*MuER;RB9DZ-7vJT0XoBIVzVxTxqcc(0lR6Eb z{Qdf=S8spd%I)8L^h2}@7qF@|iHa1v6UG5thR_DW`@ypCkBM;6;xmwCQPcJ?*GtW8 zGQIs>?LLCiK1SQkz?!D^01tD+p=+b)aJvf^Wx>9B=Vf=@h*{GDQ(8pi5s0s|8HhLc z%vDK$5$uv5!h}mmam}eBC@=*7M3g=W|2(>mPsRnn;SeROVU)Q5aeM?XOW+V?TnXWw zv>1^zZ~A;P;3+ZQCq?&GBvnE45#d^a5<+LU&My~AiS@xvDmp%(Qq@bwgxy7Q z!naB!#1sCKx1ObjA)z>JTu$7*SyFXD zj==(6UsY*IY5Q_>pnz6IoD7idlcNG}H`zEvV))@oL})MZVG-7aE%Pk$R8e{k>h7X3 z#!#r9mP8T$o4O_uvg&cMhfaYUf)(P7vc+)+AV_d0LhAzeO&(}*^ZlC>>zk{qJ~W7( zuxn~=VGed1GBuhxPrADc#spOftzM=EbgY2PjO34U8Eh=SmVIJQ^Au*E2)YMGBrVu# z72qBvkSzx(oLc(zf1|jaaY-glpU~)89C!5ixP4sShd-_7L2gkcF%;TV{sXC}0D&ZJ zeCN(I%WH&qu-*{cNq_;|2FS<5mMWnH?Ib{T90v!>q3Zr*K3;G*?t%7fy~}e3CwcW5 zgj0_Y>yVa7!Pq?YxcwtMD-3G$+AAxVrmoalSP{2tL9Y98WF`zB^FXC6U_43&{J|(g z$&6ZhM}Tg*yidM{`A>)^?LkPuZSV91cjJ&aFQh6twcd?)n$5kJkIokD-=@j)e-S+o zw*l_<)U48tlC^SoZi;mNsDPW08y$CMZadc4#&wbh={>5!BQi-MykA`0*`C{>cF8*? z;!arLzG#AGOQedh)~dy5tN?SKR_q)dmTeR9sk>yPt0B99}`+{&A;2xVvNFhh8CoTh>h;Y-4I&yYn zB9DOLP18O_(@ae>UbemZDzCvtrzZA*SDESn+hexhy?d9n{(*14smWa$3%s<^?xL6G zwrMweJuc2cY|&{=Z+{zG>{WTQOMmf`W~wdPL2sQ~mwD2@Vy5b_8@|m6?OAnRt@W<; z2Xk&Su?zY)*y#N>_hS25a6YZ44&6wPDFG8zIFdtT+QUnm=2;~vAVdW@={cVvNtEbL zZ6{=d>
J_O1;fqR4EIQlTq1WzyLruKde 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

fU_3Agjp{Dq6KL4BFyn@;kPOy?U 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

?$@<`jbLX(tf*^Jk!9ei^=T<267uS?G_K{y z#OeW9TsAdpK0SmEHiZv6-r?r#;O$KQ!37tpFfwxENb>82tP^USp&yj&N#Hia?Aoex zFJ?H>WEDExblHD4<+7iD7bxKx%Gy<;0lG6<)NKo0^LU{(VZux~G|+O2jUIE89D2ZZ zY{NhvV*v&+g*#l2!Sc?yovoX;5U=;-q+1w=56xQ6{rN_JbCrbsRO0-X%t_6&^fzno zsEJQ?nq)0@?MQ&r!{2r^t5fEWLheQXGd}%9xwU<7KO_!x)hx4=!QFC|82)`yq-)6{ zbJ2>`kEB*_e^YA2H(!(NeK?nBZ%m;(qc@+=${e837qv->s?v<+5bq(KEEjWfq(i zlX&?{QJB5e03#z-_DOqCwT9F0Y28Trv#9$YY*Mo0m$Vnf#NL1UOm<;%#*EL{0 z@us(r?~P$>cz}zeyR78g;q#9>IroBTbgfv`ZeOIKeC%0?Ce7T~-U)hn9ZntGJ^$)U z4`Vl!2(K3gKEiY|8+;jHr^iV6D}GCxJ3CS(jSH;@&V)pDGa)_Q=8Q23Zj6HXEI2?s zpkw!tj*|F@D@<%{bUJnE?xW?iD)R_?xg}a;+J{s=GzWTMLs^K#Kw|OcjHYoKY2>rMj_v^k50O95ySJMPZ)-0zJRtYA^+~sqiIo$dv&Xe%~^hum_oY$ zOn6|Q5}yg7Q#|XHj5_HHAGvYn{sx$qU`SKANS6lTQIM|7})s zdi!B~qXL}WKjijNJ3mlJN?LZQULZ%%A>THAf0(e!yPi{*?^kCyX5g{@krhYnHt6&%RT?sz@nWV z*#rCb1HDcDoD|grJIAN6QcTI~ndTA0w{>>f?=$)iP7cPlL-+QX9v+n8pc&rM{ydtP zw}cHk;IHOMhB9p0w}D3bqh7?WK6tPnk_8-_Y$dhb5C&Uyr$wId+R%b?nsw6F;+$Ed z9XdhU5H6OP*B2JTqoOj_HdfZ+rQc5$XXw-ST3XABGXIIaw~4 zc%VT*(TX$;GUx#s#a8QeNvNsxbz#El2Dao1N`u_y^8QK*X|&YM-Q-DUASE+BLg?

`e(68P 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 z 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

-2U2Y+)*pJY15`M zA#nF4M|Y&$OI>b@+`dj_ooFM`(ee||dPtW1g47i)mwhJta?8VTyCOJ-qnJBJi{X@< zS-&!QBTmBWka*p#k0QJL>Rkn6&%kPsTL@+-jV%Fxx$*X2#Ab%}&KO`GaB$6Ao86Sd z32y&}9>G6R>+XQb32cyU2e{vo9z~G578E~=uo}R#7@kt@ef_Ps{s{~-WPJ^&bZ!6u N002ovPDHLkV1gIVN|*ou literal 0 HcmV?d00001 diff --git a/web/service/use-education.ts b/web/service/use-education.ts new file mode 100644 index 0000000000..4405db7478 --- /dev/null +++ b/web/service/use-education.ts @@ -0,0 +1,67 @@ +import { get, post } from './base' +import { + useMutation, + useQuery, +} from '@tanstack/react-query' +import { useInvalid } from './use-base' +import type { EducationAddParams } from '@/app/education-apply/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']) +} diff --git a/web/utils/index.ts b/web/utils/index.ts index 4a0adce113..263d415479 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -90,3 +90,12 @@ export const canFindTool = (providerId: string, oldToolId?: string) => { || providerId === `langgenius/${oldToolId}/${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()) +} From 9c4be5d098d3608de4c7e3a2bdd5942aa4fd1cea Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Tue, 1 Apr 2025 02:45:34 -0400 Subject: [PATCH 007/290] Feat/education api (#17168) --- api/configs/feature/__init__.py | 5 ++ api/controllers/console/error.py | 12 +++ api/controllers/console/workspace/account.py | 84 +++++++++++++++++++- api/controllers/console/wraps.py | 11 +++ api/services/billing_service.py | 44 +++++++++- api/services/feature_service.py | 8 ++ 6 files changed, 162 insertions(+), 2 deletions(-) diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 46ded0244f..fa8e8c2bf6 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -848,6 +848,11 @@ class AccountConfig(BaseSettings): default=5, ) + EDUCATION_ENABLED: bool = Field( + description="whether to enable education identity", + default=False, + ) + class FeatureConfig( # place the configs in alphabet order diff --git a/api/controllers/console/error.py b/api/controllers/console/error.py index bd4ae9dc7f..b8fd1f0358 100644 --- a/api/controllers/console/error.py +++ b/api/controllers/console/error.py @@ -103,6 +103,18 @@ class AccountInFreezeError(BaseHTTPException): ) +class EducationVerifyLimitError(BaseHTTPException): + error_code = "education_verify_limit" + description = "Rate limit exceeded" + code = 429 + + +class EducationActivateLimitError(BaseHTTPException): + error_code = "education_activate_limit" + description = "Rate limit exceeded" + code = 429 + + class CompilanceRateLimitError(BaseHTTPException): error_code = "compilance_rate_limit" description = "Rate limit exceeded for downloading compliance report." diff --git a/api/controllers/console/workspace/account.py b/api/controllers/console/workspace/account.py index f1ec0f3d29..d2cc140489 100644 --- a/api/controllers/console/workspace/account.py +++ b/api/controllers/console/workspace/account.py @@ -15,7 +15,13 @@ from controllers.console.workspace.error import ( InvalidInvitationCodeError, RepeatPasswordNotMatchError, ) -from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required +from controllers.console.wraps import ( + account_initialization_required, + cloud_edition_billing_enabled, + enterprise_license_required, + only_edition_cloud, + setup_required, +) from extensions.ext_database import db from fields.member_fields import account_fields from libs.helper import TimestampField, timezone @@ -292,6 +298,79 @@ class AccountDeleteUpdateFeedbackApi(Resource): return {"result": "success"} +class EducationVerifyApi(Resource): + verify_fields = { + "token": fields.String, + } + + @setup_required + @login_required + @account_initialization_required + @only_edition_cloud + @cloud_edition_billing_enabled + @marshal_with(verify_fields) + def get(self): + account = current_user + + return BillingService.EducationIdentity.verify(account.id, account.email) + + +class EducationApi(Resource): + status_fields = { + "result": fields.Boolean, + } + + @setup_required + @login_required + @account_initialization_required + @only_edition_cloud + @cloud_edition_billing_enabled + def post(self): + account = current_user + + parser = reqparse.RequestParser() + parser.add_argument("token", type=str, required=True, location="json") + parser.add_argument("institution", type=str, required=True, location="json") + parser.add_argument("role", type=str, required=True, location="json") + args = parser.parse_args() + + return BillingService.EducationIdentity.activate(account, args["token"], args["institution"], args["role"]) + + @setup_required + @login_required + @account_initialization_required + @only_edition_cloud + @cloud_edition_billing_enabled + @marshal_with(status_fields) + def get(self): + account = current_user + + return BillingService.EducationIdentity.is_active(account.id) + + +class EducationAutoCompleteApi(Resource): + data_fields = { + "data": fields.List(fields.String), + "curr_page": fields.Integer, + "has_next": fields.Boolean, + } + + @setup_required + @login_required + @account_initialization_required + @only_edition_cloud + @cloud_edition_billing_enabled + @marshal_with(data_fields) + def get(self): + parser = reqparse.RequestParser() + parser.add_argument("keywords", type=str, required=True, location="args") + parser.add_argument("page", type=int, required=False, location="args", default=0) + parser.add_argument("limit", type=int, required=False, location="args", default=20) + args = parser.parse_args() + + return BillingService.EducationIdentity.autocomplete(args["keywords"], args["page"], args["limit"]) + + # Register API resources api.add_resource(AccountInitApi, "/account/init") api.add_resource(AccountProfileApi, "/account/profile") @@ -305,5 +384,8 @@ api.add_resource(AccountIntegrateApi, "/account/integrates") api.add_resource(AccountDeleteVerifyApi, "/account/delete/verify") api.add_resource(AccountDeleteApi, "/account/delete") api.add_resource(AccountDeleteUpdateFeedbackApi, "/account/delete/feedback") +api.add_resource(EducationVerifyApi, "/account/education/verify") +api.add_resource(EducationApi, "/account/education") +api.add_resource(EducationAutoCompleteApi, "/account/education/autocomplete") # api.add_resource(AccountEmailApi, '/account/email') # api.add_resource(AccountEmailVerifyApi, '/account/email-verify') diff --git a/api/controllers/console/wraps.py b/api/controllers/console/wraps.py index ed6e16b035..6caaae87f4 100644 --- a/api/controllers/console/wraps.py +++ b/api/controllers/console/wraps.py @@ -54,6 +54,17 @@ def only_edition_self_hosted(view): return decorated +def cloud_edition_billing_enabled(view): + @wraps(view) + def decorated(*args, **kwargs): + features = FeatureService.get_features(current_user.current_tenant_id) + if not features.billing.enabled: + abort(403, "Billing feature is not enabled.") + return view(*args, **kwargs) + + return decorated + + def cloud_edition_billing_resource_check(resource: str): def interceptor(view): @wraps(view) diff --git a/api/services/billing_service.py b/api/services/billing_service.py index ab68aad45a..d44483ad89 100644 --- a/api/services/billing_service.py +++ b/api/services/billing_service.py @@ -6,7 +6,7 @@ from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fix from extensions.ext_database import db from libs.helper import RateLimiter -from models.account import TenantAccountJoin, TenantAccountRole +from models.account import Account, TenantAccountJoin, TenantAccountRole class BillingService: @@ -106,6 +106,48 @@ class BillingService: json = {"email": email, "feedback": feedback} return cls._send_request("POST", "/account/delete-feedback", json=json) + class EducationIdentity: + verification_rate_limit = RateLimiter(prefix="edu_verification_rate_limit", max_attempts=10, time_window=60) + activation_rate_limit = RateLimiter(prefix="edu_activation_rate_limit", max_attempts=10, time_window=60) + + @classmethod + def verify(cls, account_id: str, account_email: str): + if cls.verification_rate_limit.is_rate_limited(account_email): + from controllers.console.error import EducationVerifyLimitError + + raise EducationVerifyLimitError() + + cls.verification_rate_limit.increment_rate_limit(account_email) + + params = {"account_id": account_id} + return BillingService._send_request("GET", "/education/verify", params=params) + + @classmethod + def is_active(cls, account_id: str): + params = {"account_id": account_id} + return BillingService._send_request("GET", "/education/status", params=params) + + @classmethod + def activate(cls, account: Account, token: str, institution: str, role: str): + if cls.activation_rate_limit.is_rate_limited(account.email): + from controllers.console.error import EducationActivateLimitError + + raise EducationActivateLimitError() + + cls.activation_rate_limit.increment_rate_limit(account.email) + params = {"account_id": account.id, "curr_tenant_id": account.current_tenant_id} + json = { + "institution": institution, + "token": token, + "role": role, + } + return BillingService._send_request("POST", "/education/", json=json, params=params) + + @classmethod + def autocomplete(cls, keywords: str, page: int = 0, limit: int = 20): + params = {"keywords": keywords, "page": page, "limit": limit} + return BillingService._send_request("GET", "/education/autocomplete", params=params) + @classmethod def get_compliance_download_link( cls, diff --git a/api/services/feature_service.py b/api/services/feature_service.py index add34d9f43..c2226c319f 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -17,6 +17,11 @@ class BillingModel(BaseModel): subscription: SubscriptionModel = SubscriptionModel() +class EducationModel(BaseModel): + enabled: bool = False + activated: bool = False + + class LimitationModel(BaseModel): size: int = 0 limit: int = 0 @@ -38,6 +43,7 @@ class LicenseModel(BaseModel): class FeatureModel(BaseModel): billing: BillingModel = BillingModel() + education: EducationModel = EducationModel() members: LimitationModel = LimitationModel(size=0, limit=1) apps: LimitationModel = LimitationModel(size=0, limit=10) vector_space: LimitationModel = LimitationModel(size=0, limit=5) @@ -128,6 +134,7 @@ class FeatureService: features.can_replace_logo = dify_config.CAN_REPLACE_LOGO features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED + features.education.enabled = dify_config.EDUCATION_ENABLED @classmethod def _fulfill_params_from_billing_api(cls, features: FeatureModel, tenant_id: str): @@ -136,6 +143,7 @@ class FeatureService: features.billing.enabled = billing_info["enabled"] features.billing.subscription.plan = billing_info["subscription"]["plan"] features.billing.subscription.interval = billing_info["subscription"]["interval"] + features.education.activated = billing_info["subscription"].get("education", False) if "members" in billing_info: features.members.size = billing_info["members"]["size"] From e58703877bbbd51ae21256a89d33d4911c0b8ac0 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 1 Apr 2025 16:02:52 +0800 Subject: [PATCH 008/290] chore: slice workflow store (#17254) --- web/app/components/workflow/store.ts | 323 ------------------ web/app/components/workflow/store/index.ts | 1 + .../store/workflow/chat-variable-slice.ts | 34 ++ .../store/workflow/env-variable-slice.ts | 20 ++ .../workflow/store/workflow/form-slice.ts | 18 + .../store/workflow/help-line-slice.ts | 19 ++ .../workflow/store/workflow/history-slice.ts | 25 ++ .../workflow/store/workflow/index.ts | 69 ++++ .../workflow/store/workflow/node-slice.ts | 80 +++++ .../workflow/store/workflow/panel-slice.ts | 32 ++ .../workflow/store/workflow/tool-slice.ts | 26 ++ .../workflow/store/workflow/version-slice.ts | 26 ++ .../store/workflow/workflow-draft-slice.ts | 36 ++ .../workflow/store/workflow/workflow-slice.ts | 65 ++++ 14 files changed, 451 insertions(+), 323 deletions(-) delete mode 100644 web/app/components/workflow/store.ts create mode 100644 web/app/components/workflow/store/index.ts create mode 100644 web/app/components/workflow/store/workflow/chat-variable-slice.ts create mode 100644 web/app/components/workflow/store/workflow/env-variable-slice.ts create mode 100644 web/app/components/workflow/store/workflow/form-slice.ts create mode 100644 web/app/components/workflow/store/workflow/help-line-slice.ts create mode 100644 web/app/components/workflow/store/workflow/history-slice.ts create mode 100644 web/app/components/workflow/store/workflow/index.ts create mode 100644 web/app/components/workflow/store/workflow/node-slice.ts create mode 100644 web/app/components/workflow/store/workflow/panel-slice.ts create mode 100644 web/app/components/workflow/store/workflow/tool-slice.ts create mode 100644 web/app/components/workflow/store/workflow/version-slice.ts create mode 100644 web/app/components/workflow/store/workflow/workflow-draft-slice.ts create mode 100644 web/app/components/workflow/store/workflow/workflow-slice.ts diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts deleted file mode 100644 index a71454ffff..0000000000 --- a/web/app/components/workflow/store.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { useContext } from 'react' -import { - useStore as useZustandStore, -} from 'zustand' -import { createStore } from 'zustand/vanilla' -import { debounce } from 'lodash-es' -import type { Viewport } from 'reactflow' -import type { - HelpLineHorizontalPosition, - HelpLineVerticalPosition, -} from './help-line/types' -import type { VariableAssignerNodeType } from './nodes/variable-assigner/types' -import type { - ConversationVariable, - Edge, - EnvironmentVariable, - HistoryWorkflowData, - Node, - RunFile, - ToolWithProvider, - WorkflowRunningData, -} from './types' -import { WorkflowContext } from './context' -import type { NodeTracing, VersionHistory } from '@/types/workflow' - -// #TODO chatVar# -// const MOCK_DATA = [ -// { -// id: 'fjlaksdjflkjg-dfjlajfl0dnfkafjk-djfdkafj-djfak', -// name: 'chat_history', -// value_type: 'array[message]', -// value: [], -// description: 'The chat history of the conversation', -// }, -// { -// id: 'fljdaklfjl-dfjlafj0-dklajglje-eknglh', -// name: 'order_id', -// value: '123456', -// value_type: 'string', -// description: '', -// }, -// ] - -type PreviewRunningData = WorkflowRunningData & { - resultTabActive?: boolean - resultText?: string -} - -type Shape = { - appId: string - panelWidth: number - showSingleRunPanel: boolean - setShowSingleRunPanel: (showSingleRunPanel: boolean) => void - workflowRunningData?: PreviewRunningData - setWorkflowRunningData: (workflowData: PreviewRunningData) => void - historyWorkflowData?: HistoryWorkflowData - setHistoryWorkflowData: (historyWorkflowData?: HistoryWorkflowData) => void - showRunHistory: boolean - setShowRunHistory: (showRunHistory: boolean) => void - showFeaturesPanel: boolean - setShowFeaturesPanel: (showFeaturesPanel: boolean) => void - helpLineHorizontal?: HelpLineHorizontalPosition - setHelpLineHorizontal: (helpLineHorizontal?: HelpLineHorizontalPosition) => void - helpLineVertical?: HelpLineVerticalPosition - setHelpLineVertical: (helpLineVertical?: HelpLineVerticalPosition) => void - draftUpdatedAt: number - setDraftUpdatedAt: (draftUpdatedAt: number) => void - publishedAt: number - setPublishedAt: (publishedAt: number) => void - currentVersion: VersionHistory | null - setCurrentVersion: (currentVersion: VersionHistory) => void - showWorkflowVersionHistoryPanel: boolean - setShowWorkflowVersionHistoryPanel: (showWorkflowVersionHistoryPanel: boolean) => void - showInputsPanel: boolean - setShowInputsPanel: (showInputsPanel: boolean) => void - inputs: Record - setInputs: (inputs: Record) => void - toolPublished: boolean - setToolPublished: (toolPublished: boolean) => void - files: RunFile[] - setFiles: (files: RunFile[]) => void - backupDraft?: { - nodes: Node[] - edges: Edge[] - viewport: Viewport - features: Record - environmentVariables: EnvironmentVariable[] - } - setBackupDraft: (backupDraft?: Shape['backupDraft']) => void - notInitialWorkflow: boolean - setNotInitialWorkflow: (notInitialWorkflow: boolean) => void - nodesDefaultConfigs: Record - setNodesDefaultConfigs: (nodesDefaultConfigs: Record) => void - nodeAnimation: boolean - setNodeAnimation: (nodeAnimation: boolean) => void - isRestoring: boolean - setIsRestoring: (isRestoring: boolean) => void - debouncedSyncWorkflowDraft: (fn: () => void) => void - buildInTools: ToolWithProvider[] - setBuildInTools: (tools: ToolWithProvider[]) => void - customTools: ToolWithProvider[] - setCustomTools: (tools: ToolWithProvider[]) => void - workflowTools: ToolWithProvider[] - setWorkflowTools: (tools: ToolWithProvider[]) => void - clipboardElements: Node[] - setClipboardElements: (clipboardElements: Node[]) => void - showDebugAndPreviewPanel: boolean - setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void - showEnvPanel: boolean - setShowEnvPanel: (showEnvPanel: boolean) => void - environmentVariables: EnvironmentVariable[] - setEnvironmentVariables: (environmentVariables: EnvironmentVariable[]) => void - envSecrets: Record - setEnvSecrets: (envSecrets: Record) => void - showChatVariablePanel: boolean - setShowChatVariablePanel: (showChatVariablePanel: boolean) => void - showGlobalVariablePanel: boolean - setShowGlobalVariablePanel: (showGlobalVariablePanel: boolean) => void - conversationVariables: ConversationVariable[] - setConversationVariables: (conversationVariables: ConversationVariable[]) => void - selection: null | { x1: number; y1: number; x2: number; y2: number } - setSelection: (selection: Shape['selection']) => void - bundleNodeSize: { width: number; height: number } | null - setBundleNodeSize: (bundleNodeSize: Shape['bundleNodeSize']) => void - controlMode: 'pointer' | 'hand' - setControlMode: (controlMode: Shape['controlMode']) => void - candidateNode?: Node - setCandidateNode: (candidateNode?: Node) => void - panelMenu?: { - top: number - left: number - } - setPanelMenu: (panelMenu: Shape['panelMenu']) => void - nodeMenu?: { - top: number - left: number - nodeId: string - } - setNodeMenu: (nodeMenu: Shape['nodeMenu']) => void - mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number } - setMousePosition: (mousePosition: Shape['mousePosition']) => void - syncWorkflowDraftHash: string - setSyncWorkflowDraftHash: (hash: string) => void - showConfirm?: { title: string; desc?: string; onConfirm: () => void } - setShowConfirm: (showConfirm: Shape['showConfirm']) => void - showAssignVariablePopup?: { - nodeId: string - nodeData: Node['data'] - variableAssignerNodeId: string - variableAssignerNodeData: VariableAssignerNodeType - variableAssignerNodeHandleId: string - parentNode?: Node - x: number - y: number - } - setShowAssignVariablePopup: (showAssignVariablePopup: Shape['showAssignVariablePopup']) => void - hoveringAssignVariableGroupId?: string - setHoveringAssignVariableGroupId: (hoveringAssignVariableGroupId?: string) => void - connectingNodePayload?: { nodeId: string; nodeType: string; handleType: string; handleId: string | null } - setConnectingNodePayload: (startConnectingPayload?: Shape['connectingNodePayload']) => void - enteringNodePayload?: { - nodeId: string - nodeData: VariableAssignerNodeType - } - setEnteringNodePayload: (enteringNodePayload?: Shape['enteringNodePayload']) => void - isSyncingWorkflowDraft: boolean - setIsSyncingWorkflowDraft: (isSyncingWorkflowDraft: boolean) => void - controlPromptEditorRerenderKey: number - setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void - showImportDSLModal: boolean - setShowImportDSLModal: (showImportDSLModal: boolean) => void - showTips: string - setShowTips: (showTips: string) => void - iterTimes: number - setIterTimes: (iterTimes: number) => void - loopTimes: number - setLoopTimes: (loopTimes: number) => void - iterParallelLogMap: Map> - setIterParallelLogMap: (iterParallelLogMap: Map>) => void - versionHistory: VersionHistory[] - setVersionHistory: (versionHistory: VersionHistory[]) => void -} - -export const createWorkflowStore = () => { - const hideAllPanel = { - showDebugAndPreviewPanel: false, - showEnvPanel: false, - showChatVariablePanel: false, - showGlobalVariablePanel: false, - } - return createStore(set => ({ - appId: '', - panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420, - showSingleRunPanel: false, - setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), - workflowRunningData: undefined, - setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), - historyWorkflowData: undefined, - setHistoryWorkflowData: historyWorkflowData => set(() => ({ historyWorkflowData })), - showRunHistory: false, - setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })), - showFeaturesPanel: false, - setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })), - helpLineHorizontal: undefined, - setHelpLineHorizontal: helpLineHorizontal => set(() => ({ helpLineHorizontal })), - helpLineVertical: undefined, - setHelpLineVertical: helpLineVertical => set(() => ({ helpLineVertical })), - draftUpdatedAt: 0, - setDraftUpdatedAt: draftUpdatedAt => set(() => ({ draftUpdatedAt: draftUpdatedAt ? draftUpdatedAt * 1000 : 0 })), - publishedAt: 0, - setPublishedAt: publishedAt => set(() => ({ publishedAt: publishedAt ? publishedAt * 1000 : 0 })), - currentVersion: null, - setCurrentVersion: currentVersion => set(() => ({ currentVersion })), - showWorkflowVersionHistoryPanel: false, - setShowWorkflowVersionHistoryPanel: showWorkflowVersionHistoryPanel => set(() => ({ showWorkflowVersionHistoryPanel })), - showInputsPanel: false, - setShowInputsPanel: showInputsPanel => set(() => ({ showInputsPanel })), - inputs: {}, - setInputs: inputs => set(() => ({ inputs })), - toolPublished: false, - setToolPublished: toolPublished => set(() => ({ toolPublished })), - files: [], - setFiles: files => set(() => ({ files })), - backupDraft: undefined, - setBackupDraft: backupDraft => set(() => ({ backupDraft })), - notInitialWorkflow: false, - setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), - nodesDefaultConfigs: {}, - setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), - nodeAnimation: false, - setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })), - isRestoring: false, - setIsRestoring: isRestoring => set(() => ({ isRestoring })), - debouncedSyncWorkflowDraft: debounce((syncWorkflowDraft) => { - syncWorkflowDraft() - }, 5000), - buildInTools: [], - setBuildInTools: buildInTools => set(() => ({ buildInTools })), - customTools: [], - setCustomTools: customTools => set(() => ({ customTools })), - workflowTools: [], - setWorkflowTools: workflowTools => set(() => ({ workflowTools })), - clipboardElements: [], - setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), - showDebugAndPreviewPanel: false, - setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })), - showEnvPanel: false, - setShowEnvPanel: showEnvPanel => set(() => ({ showEnvPanel })), - environmentVariables: [], - setEnvironmentVariables: environmentVariables => set(() => ({ environmentVariables })), - envSecrets: {}, - setEnvSecrets: envSecrets => set(() => ({ envSecrets })), - showChatVariablePanel: false, - setShowChatVariablePanel: showChatVariablePanel => set(() => ({ showChatVariablePanel })), - showGlobalVariablePanel: false, - setShowGlobalVariablePanel: showGlobalVariablePanel => set(() => { - if (showGlobalVariablePanel) - return { ...hideAllPanel, showGlobalVariablePanel: true } - else - return { showGlobalVariablePanel: false } - }), - conversationVariables: [], - setConversationVariables: conversationVariables => set(() => ({ conversationVariables })), - selection: null, - setSelection: selection => set(() => ({ selection })), - bundleNodeSize: null, - setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })), - controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand', - setControlMode: (controlMode) => { - set(() => ({ controlMode })) - localStorage.setItem('workflow-operation-mode', controlMode) - }, - candidateNode: undefined, - setCandidateNode: candidateNode => set(() => ({ candidateNode })), - panelMenu: undefined, - setPanelMenu: panelMenu => set(() => ({ panelMenu })), - nodeMenu: undefined, - setNodeMenu: nodeMenu => set(() => ({ nodeMenu })), - mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, - setMousePosition: mousePosition => set(() => ({ mousePosition })), - syncWorkflowDraftHash: '', - setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })), - showConfirm: undefined, - setShowConfirm: showConfirm => set(() => ({ showConfirm })), - showAssignVariablePopup: undefined, - setShowAssignVariablePopup: showAssignVariablePopup => set(() => ({ showAssignVariablePopup })), - hoveringAssignVariableGroupId: undefined, - setHoveringAssignVariableGroupId: hoveringAssignVariableGroupId => set(() => ({ hoveringAssignVariableGroupId })), - connectingNodePayload: undefined, - setConnectingNodePayload: connectingNodePayload => set(() => ({ connectingNodePayload })), - enteringNodePayload: undefined, - setEnteringNodePayload: enteringNodePayload => set(() => ({ enteringNodePayload })), - isSyncingWorkflowDraft: false, - setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), - controlPromptEditorRerenderKey: 0, - setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })), - showImportDSLModal: false, - setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), - showTips: '', - setShowTips: showTips => set(() => ({ showTips })), - iterTimes: 1, - setIterTimes: iterTimes => set(() => ({ iterTimes })), - loopTimes: 1, - setLoopTimes: loopTimes => set(() => ({ loopTimes })), - iterParallelLogMap: new Map>(), - setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })), - - versionHistory: [], - setVersionHistory: versionHistory => set(() => ({ versionHistory })), - })) -} - -export function useStore(selector: (state: Shape) => T): T { - const store = useContext(WorkflowContext) - if (!store) - throw new Error('Missing WorkflowContext.Provider in the tree') - - return useZustandStore(store, selector) -} - -export const useWorkflowStore = () => { - return useContext(WorkflowContext)! -} diff --git a/web/app/components/workflow/store/index.ts b/web/app/components/workflow/store/index.ts new file mode 100644 index 0000000000..61cd5773ce --- /dev/null +++ b/web/app/components/workflow/store/index.ts @@ -0,0 +1 @@ +export * from './workflow' diff --git a/web/app/components/workflow/store/workflow/chat-variable-slice.ts b/web/app/components/workflow/store/workflow/chat-variable-slice.ts new file mode 100644 index 0000000000..0d81446005 --- /dev/null +++ b/web/app/components/workflow/store/workflow/chat-variable-slice.ts @@ -0,0 +1,34 @@ +import type { StateCreator } from 'zustand' +import type { ConversationVariable } from '@/app/components/workflow/types' + +export type ChatVariableSliceShape = { + showChatVariablePanel: boolean + setShowChatVariablePanel: (showChatVariablePanel: boolean) => void + showGlobalVariablePanel: boolean + setShowGlobalVariablePanel: (showGlobalVariablePanel: boolean) => void + conversationVariables: ConversationVariable[] + setConversationVariables: (conversationVariables: ConversationVariable[]) => void +} + +export const createChatVariableSlice: StateCreator = (set) => { + const hideAllPanel = { + showDebugAndPreviewPanel: false, + showEnvPanel: false, + showChatVariablePanel: false, + showGlobalVariablePanel: false, + } + + return ({ + showChatVariablePanel: false, + setShowChatVariablePanel: showChatVariablePanel => set(() => ({ showChatVariablePanel })), + showGlobalVariablePanel: false, + setShowGlobalVariablePanel: showGlobalVariablePanel => set(() => { + if (showGlobalVariablePanel) + return { ...hideAllPanel, showGlobalVariablePanel: true } + else + return { showGlobalVariablePanel: false } + }), + conversationVariables: [], + setConversationVariables: conversationVariables => set(() => ({ conversationVariables })), + }) +} diff --git a/web/app/components/workflow/store/workflow/env-variable-slice.ts b/web/app/components/workflow/store/workflow/env-variable-slice.ts new file mode 100644 index 0000000000..de60e7dd5f --- /dev/null +++ b/web/app/components/workflow/store/workflow/env-variable-slice.ts @@ -0,0 +1,20 @@ +import type { StateCreator } from 'zustand' +import type { EnvironmentVariable } from '@/app/components/workflow/types' + +export type EnvVariableSliceShape = { + showEnvPanel: boolean + setShowEnvPanel: (showEnvPanel: boolean) => void + environmentVariables: EnvironmentVariable[] + setEnvironmentVariables: (environmentVariables: EnvironmentVariable[]) => void + envSecrets: Record + setEnvSecrets: (envSecrets: Record) => void +} + +export const createEnvVariableSlice: StateCreator = set => ({ + showEnvPanel: false, + setShowEnvPanel: showEnvPanel => set(() => ({ showEnvPanel })), + environmentVariables: [], + setEnvironmentVariables: environmentVariables => set(() => ({ environmentVariables })), + envSecrets: {}, + setEnvSecrets: envSecrets => set(() => ({ envSecrets })), +}) diff --git a/web/app/components/workflow/store/workflow/form-slice.ts b/web/app/components/workflow/store/workflow/form-slice.ts new file mode 100644 index 0000000000..a6c607d2af --- /dev/null +++ b/web/app/components/workflow/store/workflow/form-slice.ts @@ -0,0 +1,18 @@ +import type { StateCreator } from 'zustand' +import type { + RunFile, +} from '@/app/components/workflow/types' + +export type FormSliceShape = { + inputs: Record + setInputs: (inputs: Record) => void + files: RunFile[] + setFiles: (files: RunFile[]) => void +} + +export const createFormSlice: StateCreator = set => ({ + inputs: {}, + setInputs: inputs => set(() => ({ inputs })), + files: [], + setFiles: files => set(() => ({ files })), +}) diff --git a/web/app/components/workflow/store/workflow/help-line-slice.ts b/web/app/components/workflow/store/workflow/help-line-slice.ts new file mode 100644 index 0000000000..7d9aeac14a --- /dev/null +++ b/web/app/components/workflow/store/workflow/help-line-slice.ts @@ -0,0 +1,19 @@ +import type { StateCreator } from 'zustand' +import type { + HelpLineHorizontalPosition, + HelpLineVerticalPosition, +} from '@/app/components/workflow/help-line/types' + +export type HelpLineSliceShape = { + helpLineHorizontal?: HelpLineHorizontalPosition + setHelpLineHorizontal: (helpLineHorizontal?: HelpLineHorizontalPosition) => void + helpLineVertical?: HelpLineVerticalPosition + setHelpLineVertical: (helpLineVertical?: HelpLineVerticalPosition) => void +} + +export const createHelpLineSlice: StateCreator = set => ({ + helpLineHorizontal: undefined, + setHelpLineHorizontal: helpLineHorizontal => set(() => ({ helpLineHorizontal })), + helpLineVertical: undefined, + setHelpLineVertical: helpLineVertical => set(() => ({ helpLineVertical })), +}) diff --git a/web/app/components/workflow/store/workflow/history-slice.ts b/web/app/components/workflow/store/workflow/history-slice.ts new file mode 100644 index 0000000000..47d1b8ad42 --- /dev/null +++ b/web/app/components/workflow/store/workflow/history-slice.ts @@ -0,0 +1,25 @@ +import type { StateCreator } from 'zustand' +import type { + HistoryWorkflowData, +} from '@/app/components/workflow/types' +import type { + VersionHistory, +} from '@/types/workflow' + +export type HistorySliceShape = { + historyWorkflowData?: HistoryWorkflowData + setHistoryWorkflowData: (historyWorkflowData?: HistoryWorkflowData) => void + showRunHistory: boolean + setShowRunHistory: (showRunHistory: boolean) => void + versionHistory: VersionHistory[] + setVersionHistory: (versionHistory: VersionHistory[]) => void +} + +export const createHistorySlice: StateCreator = set => ({ + historyWorkflowData: undefined, + setHistoryWorkflowData: historyWorkflowData => set(() => ({ historyWorkflowData })), + showRunHistory: false, + setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })), + versionHistory: [], + setVersionHistory: versionHistory => set(() => ({ versionHistory })), +}) diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts new file mode 100644 index 0000000000..769b986606 --- /dev/null +++ b/web/app/components/workflow/store/workflow/index.ts @@ -0,0 +1,69 @@ +import { useContext } from 'react' +import { + useStore as useZustandStore, +} from 'zustand' +import { createStore } from 'zustand/vanilla' +import type { ChatVariableSliceShape } from './chat-variable-slice' +import { createChatVariableSlice } from './chat-variable-slice' +import type { EnvVariableSliceShape } from './env-variable-slice' +import { createEnvVariableSlice } from './env-variable-slice' +import type { FormSliceShape } from './form-slice' +import { createFormSlice } from './form-slice' +import type { HelpLineSliceShape } from './help-line-slice' +import { createHelpLineSlice } from './help-line-slice' +import type { HistorySliceShape } from './history-slice' +import { createHistorySlice } from './history-slice' +import type { NodeSliceShape } from './node-slice' +import { createNodeSlice } from './node-slice' +import type { PanelSliceShape } from './panel-slice' +import { createPanelSlice } from './panel-slice' +import type { ToolSliceShape } from './tool-slice' +import { createToolSlice } from './tool-slice' +import type { VersionSliceShape } from './version-slice' +import { createVersionSlice } from './version-slice' +import type { WorkflowDraftSliceShape } from './workflow-draft-slice' +import { createWorkflowDraftSlice } from './workflow-draft-slice' +import type { WorkflowSliceShape } from './workflow-slice' +import { createWorkflowSlice } from './workflow-slice' +import { WorkflowContext } from '@/app/components/workflow/context' + +export type Shape = + ChatVariableSliceShape & + EnvVariableSliceShape & + FormSliceShape & + HelpLineSliceShape & + HistorySliceShape & + NodeSliceShape & + PanelSliceShape & + ToolSliceShape & + VersionSliceShape & + WorkflowDraftSliceShape & + WorkflowSliceShape + +export const createWorkflowStore = () => { + return createStore((...args) => ({ + ...createChatVariableSlice(...args), + ...createEnvVariableSlice(...args), + ...createFormSlice(...args), + ...createHelpLineSlice(...args), + ...createHistorySlice(...args), + ...createNodeSlice(...args), + ...createPanelSlice(...args), + ...createToolSlice(...args), + ...createVersionSlice(...args), + ...createWorkflowDraftSlice(...args), + ...createWorkflowSlice(...args), + })) +} + +export function useStore(selector: (state: Shape) => T): T { + const store = useContext(WorkflowContext) + if (!store) + throw new Error('Missing WorkflowContext.Provider in the tree') + + return useZustandStore(store, selector) +} + +export const useWorkflowStore = () => { + return useContext(WorkflowContext)! +} diff --git a/web/app/components/workflow/store/workflow/node-slice.ts b/web/app/components/workflow/store/workflow/node-slice.ts new file mode 100644 index 0000000000..d937dc2099 --- /dev/null +++ b/web/app/components/workflow/store/workflow/node-slice.ts @@ -0,0 +1,80 @@ +import type { StateCreator } from 'zustand' +import type { + Node, +} from '@/app/components/workflow/types' +import type { + VariableAssignerNodeType, +} from '@/app/components/workflow/nodes/variable-assigner/types' +import type { + NodeTracing, +} from '@/types/workflow' + +export type NodeSliceShape = { + showSingleRunPanel: boolean + setShowSingleRunPanel: (showSingleRunPanel: boolean) => void + nodesDefaultConfigs: Record + setNodesDefaultConfigs: (nodesDefaultConfigs: Record) => void + nodeAnimation: boolean + setNodeAnimation: (nodeAnimation: boolean) => void + candidateNode?: Node + setCandidateNode: (candidateNode?: Node) => void + nodeMenu?: { + top: number + left: number + nodeId: string + } + setNodeMenu: (nodeMenu: NodeSliceShape['nodeMenu']) => void + showAssignVariablePopup?: { + nodeId: string + nodeData: Node['data'] + variableAssignerNodeId: string + variableAssignerNodeData: VariableAssignerNodeType + variableAssignerNodeHandleId: string + parentNode?: Node + x: number + y: number + } + setShowAssignVariablePopup: (showAssignVariablePopup: NodeSliceShape['showAssignVariablePopup']) => void + hoveringAssignVariableGroupId?: string + setHoveringAssignVariableGroupId: (hoveringAssignVariableGroupId?: string) => void + connectingNodePayload?: { nodeId: string; nodeType: string; handleType: string; handleId: string | null } + setConnectingNodePayload: (startConnectingPayload?: NodeSliceShape['connectingNodePayload']) => void + enteringNodePayload?: { + nodeId: string + nodeData: VariableAssignerNodeType + } + setEnteringNodePayload: (enteringNodePayload?: NodeSliceShape['enteringNodePayload']) => void + iterTimes: number + setIterTimes: (iterTimes: number) => void + loopTimes: number + setLoopTimes: (loopTimes: number) => void + iterParallelLogMap: Map> + setIterParallelLogMap: (iterParallelLogMap: Map>) => void +} + +export const createNodeSlice: StateCreator = set => ({ + showSingleRunPanel: false, + setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), + nodesDefaultConfigs: {}, + setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), + nodeAnimation: false, + setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })), + candidateNode: undefined, + setCandidateNode: candidateNode => set(() => ({ candidateNode })), + nodeMenu: undefined, + setNodeMenu: nodeMenu => set(() => ({ nodeMenu })), + showAssignVariablePopup: undefined, + setShowAssignVariablePopup: showAssignVariablePopup => set(() => ({ showAssignVariablePopup })), + hoveringAssignVariableGroupId: undefined, + setHoveringAssignVariableGroupId: hoveringAssignVariableGroupId => set(() => ({ hoveringAssignVariableGroupId })), + connectingNodePayload: undefined, + setConnectingNodePayload: connectingNodePayload => set(() => ({ connectingNodePayload })), + enteringNodePayload: undefined, + setEnteringNodePayload: enteringNodePayload => set(() => ({ enteringNodePayload })), + iterTimes: 1, + setIterTimes: iterTimes => set(() => ({ iterTimes })), + loopTimes: 1, + setLoopTimes: loopTimes => set(() => ({ loopTimes })), + iterParallelLogMap: new Map>(), + setIterParallelLogMap: iterParallelLogMap => set(() => ({ iterParallelLogMap })), +}) diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts new file mode 100644 index 0000000000..06a5f45e11 --- /dev/null +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -0,0 +1,32 @@ +import type { StateCreator } from 'zustand' + +export type PanelSliceShape = { + panelWidth: number + showFeaturesPanel: boolean + setShowFeaturesPanel: (showFeaturesPanel: boolean) => void + showWorkflowVersionHistoryPanel: boolean + setShowWorkflowVersionHistoryPanel: (showWorkflowVersionHistoryPanel: boolean) => void + showInputsPanel: boolean + setShowInputsPanel: (showInputsPanel: boolean) => void + showDebugAndPreviewPanel: boolean + setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void + panelMenu?: { + top: number + left: number + } + setPanelMenu: (panelMenu: PanelSliceShape['panelMenu']) => void +} + +export const createPanelSlice: StateCreator = set => ({ + panelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420, + showFeaturesPanel: false, + setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })), + showWorkflowVersionHistoryPanel: false, + setShowWorkflowVersionHistoryPanel: showWorkflowVersionHistoryPanel => set(() => ({ showWorkflowVersionHistoryPanel })), + showInputsPanel: false, + setShowInputsPanel: showInputsPanel => set(() => ({ showInputsPanel })), + showDebugAndPreviewPanel: false, + setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })), + panelMenu: undefined, + setPanelMenu: panelMenu => set(() => ({ panelMenu })), +}) diff --git a/web/app/components/workflow/store/workflow/tool-slice.ts b/web/app/components/workflow/store/workflow/tool-slice.ts new file mode 100644 index 0000000000..2d54bbd925 --- /dev/null +++ b/web/app/components/workflow/store/workflow/tool-slice.ts @@ -0,0 +1,26 @@ +import type { StateCreator } from 'zustand' +import type { + ToolWithProvider, +} from '@/app/components/workflow/types' + +export type ToolSliceShape = { + buildInTools: ToolWithProvider[] + setBuildInTools: (tools: ToolWithProvider[]) => void + customTools: ToolWithProvider[] + setCustomTools: (tools: ToolWithProvider[]) => void + workflowTools: ToolWithProvider[] + setWorkflowTools: (tools: ToolWithProvider[]) => void + toolPublished: boolean + setToolPublished: (toolPublished: boolean) => void +} + +export const createToolSlice: StateCreator = set => ({ + buildInTools: [], + setBuildInTools: buildInTools => set(() => ({ buildInTools })), + customTools: [], + setCustomTools: customTools => set(() => ({ customTools })), + workflowTools: [], + setWorkflowTools: workflowTools => set(() => ({ workflowTools })), + toolPublished: false, + setToolPublished: toolPublished => set(() => ({ toolPublished })), +}) diff --git a/web/app/components/workflow/store/workflow/version-slice.ts b/web/app/components/workflow/store/workflow/version-slice.ts new file mode 100644 index 0000000000..df19218786 --- /dev/null +++ b/web/app/components/workflow/store/workflow/version-slice.ts @@ -0,0 +1,26 @@ +import type { StateCreator } from 'zustand' +import type { + VersionHistory, +} from '@/types/workflow' + +export type VersionSliceShape = { + draftUpdatedAt: number + setDraftUpdatedAt: (draftUpdatedAt: number) => void + publishedAt: number + setPublishedAt: (publishedAt: number) => void + currentVersion: VersionHistory | null + setCurrentVersion: (currentVersion: VersionHistory) => void + isRestoring: boolean + setIsRestoring: (isRestoring: boolean) => void +} + +export const createVersionSlice: StateCreator = set => ({ + draftUpdatedAt: 0, + setDraftUpdatedAt: draftUpdatedAt => set(() => ({ draftUpdatedAt: draftUpdatedAt ? draftUpdatedAt * 1000 : 0 })), + publishedAt: 0, + setPublishedAt: publishedAt => set(() => ({ publishedAt: publishedAt ? publishedAt * 1000 : 0 })), + currentVersion: null, + setCurrentVersion: currentVersion => set(() => ({ currentVersion })), + isRestoring: false, + setIsRestoring: isRestoring => set(() => ({ isRestoring })), +}) diff --git a/web/app/components/workflow/store/workflow/workflow-draft-slice.ts b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts new file mode 100644 index 0000000000..ec28debee2 --- /dev/null +++ b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts @@ -0,0 +1,36 @@ +import type { StateCreator } from 'zustand' +import { debounce } from 'lodash-es' +import type { Viewport } from 'reactflow' +import type { + Edge, + EnvironmentVariable, + Node, +} from '@/app/components/workflow/types' + +export type WorkflowDraftSliceShape = { + backupDraft?: { + nodes: Node[] + edges: Edge[] + viewport: Viewport + features: Record + environmentVariables: EnvironmentVariable[] + } + setBackupDraft: (backupDraft?: WorkflowDraftSliceShape['backupDraft']) => void + debouncedSyncWorkflowDraft: (fn: () => void) => void + syncWorkflowDraftHash: string + setSyncWorkflowDraftHash: (hash: string) => void + isSyncingWorkflowDraft: boolean + setIsSyncingWorkflowDraft: (isSyncingWorkflowDraft: boolean) => void +} + +export const createWorkflowDraftSlice: StateCreator = set => ({ + backupDraft: undefined, + setBackupDraft: backupDraft => set(() => ({ backupDraft })), + debouncedSyncWorkflowDraft: debounce((syncWorkflowDraft) => { + syncWorkflowDraft() + }, 5000), + syncWorkflowDraftHash: '', + setSyncWorkflowDraftHash: syncWorkflowDraftHash => set(() => ({ syncWorkflowDraftHash })), + isSyncingWorkflowDraft: false, + setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), +}) diff --git a/web/app/components/workflow/store/workflow/workflow-slice.ts b/web/app/components/workflow/store/workflow/workflow-slice.ts new file mode 100644 index 0000000000..19248161d2 --- /dev/null +++ b/web/app/components/workflow/store/workflow/workflow-slice.ts @@ -0,0 +1,65 @@ +import type { StateCreator } from 'zustand' +import type { + Node, + WorkflowRunningData, +} from '@/app/components/workflow/types' + +type PreviewRunningData = WorkflowRunningData & { + resultTabActive?: boolean + resultText?: string +} + +export type WorkflowSliceShape = { + appId: string + workflowRunningData?: PreviewRunningData + setWorkflowRunningData: (workflowData: PreviewRunningData) => void + notInitialWorkflow: boolean + setNotInitialWorkflow: (notInitialWorkflow: boolean) => void + clipboardElements: Node[] + setClipboardElements: (clipboardElements: Node[]) => void + selection: null | { x1: number; y1: number; x2: number; y2: number } + setSelection: (selection: WorkflowSliceShape['selection']) => void + bundleNodeSize: { width: number; height: number } | null + setBundleNodeSize: (bundleNodeSize: WorkflowSliceShape['bundleNodeSize']) => void + controlMode: 'pointer' | 'hand' + setControlMode: (controlMode: WorkflowSliceShape['controlMode']) => void + mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number } + setMousePosition: (mousePosition: WorkflowSliceShape['mousePosition']) => void + showConfirm?: { title: string; desc?: string; onConfirm: () => void } + setShowConfirm: (showConfirm: WorkflowSliceShape['showConfirm']) => void + controlPromptEditorRerenderKey: number + setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void + showImportDSLModal: boolean + setShowImportDSLModal: (showImportDSLModal: boolean) => void + showTips: string + setShowTips: (showTips: string) => void +} + +export const createWorkflowSlice: StateCreator = set => ({ + appId: '', + workflowRunningData: undefined, + setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), + notInitialWorkflow: false, + setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), + clipboardElements: [], + setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), + selection: null, + setSelection: selection => set(() => ({ selection })), + bundleNodeSize: null, + setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })), + controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand', + setControlMode: (controlMode) => { + set(() => ({ controlMode })) + localStorage.setItem('workflow-operation-mode', controlMode) + }, + mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, + setMousePosition: mousePosition => set(() => ({ mousePosition })), + showConfirm: undefined, + setShowConfirm: showConfirm => set(() => ({ showConfirm })), + controlPromptEditorRerenderKey: 0, + setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })), + showImportDSLModal: false, + setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), + showTips: '', + setShowTips: showTips => set(() => ({ showTips })), +}) From 2ae7a70be918476957737db19ec7da13c050d19d Mon Sep 17 00:00:00 2001 From: NFish Date: Tue, 1 Apr 2025 16:30:41 +0800 Subject: [PATCH 009/290] =?UTF-8?q?fix:=20web=20app=20form=20comp?= =?UTF-8?q?onent=20is=20changing=20an=20uncontrolled=20inpu=E2=80=A6=20(#1?= =?UTF-8?q?7269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../share/text-generation/run-once/index.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/app/components/share/text-generation/run-once/index.tsx b/web/app/components/share/text-generation/run-once/index.tsx index dbaf8e026e..f31c5d5e85 100644 --- a/web/app/components/share/text-generation/run-once/index.tsx +++ b/web/app/components/share/text-generation/run-once/index.tsx @@ -1,4 +1,5 @@ import type { FC, FormEvent } from 'react' +import { useEffect } from 'react' import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { @@ -59,6 +60,17 @@ const RunOnce: FC = ({ inputsRef.current = newInputs }, [onInputsChange, inputsRef]) + useEffect(() => { + const newInputs: Record = {} + promptConfig.prompt_variables.forEach((item) => { + newInputs[item.key] = '' + }) + onInputsChange(newInputs) + }, [promptConfig.prompt_variables]) + + if (inputs === null || inputs === undefined || Object.keys(inputs).length === 0) + return null + return (

From 627a9e2ce1852eaf939e1eef0711e4f25c367b86 Mon Sep 17 00:00:00 2001 From: Jyong <76649700+JohnJyong@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:45:31 +0800 Subject: [PATCH 010/290] SaaS: batch upload limit check for sandbox plan (#17264) --- api/services/dataset_service.py | 5 +++++ api/tasks/document_indexing_task.py | 2 ++ api/tasks/duplicate_document_indexing_task.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index 6fe301588a..61dc86a028 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -880,6 +880,9 @@ class DocumentService: website_info = knowledge_config.data_source.info_list.website_info_list count = len(website_info.urls) # type: ignore batch_upload_limit = int(dify_config.BATCH_UPLOAD_LIMIT) + + if features.billing.subscription.plan == "sandbox" and count > 1: + raise ValueError("Your current plan does not support batch upload, please upgrade your plan.") if count > batch_upload_limit: raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.") @@ -1328,6 +1331,8 @@ class DocumentService: website_info = knowledge_config.data_source.info_list.website_info_list # type: ignore if website_info: count = len(website_info.urls) + if features.billing.subscription.plan == "sandbox" and count > 1: + raise ValueError("Your current plan does not support batch upload, please upgrade your plan.") batch_upload_limit = int(dify_config.BATCH_UPLOAD_LIMIT) if count > batch_upload_limit: raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.") diff --git a/api/tasks/document_indexing_task.py b/api/tasks/document_indexing_task.py index a8e3a69f19..50761a2f34 100644 --- a/api/tasks/document_indexing_task.py +++ b/api/tasks/document_indexing_task.py @@ -35,6 +35,8 @@ def document_indexing_task(dataset_id: str, document_ids: list): vector_space = features.vector_space count = len(document_ids) batch_upload_limit = int(dify_config.BATCH_UPLOAD_LIMIT) + if features.billing.subscription.plan == "sandbox" and count > 1: + raise ValueError("Your current plan does not support batch upload, please upgrade your plan.") if count > batch_upload_limit: raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.") if 0 < vector_space.limit <= vector_space.size: diff --git a/api/tasks/duplicate_document_indexing_task.py b/api/tasks/duplicate_document_indexing_task.py index b0cd486476..fbb33df109 100644 --- a/api/tasks/duplicate_document_indexing_task.py +++ b/api/tasks/duplicate_document_indexing_task.py @@ -35,6 +35,8 @@ def duplicate_document_indexing_task(dataset_id: str, document_ids: list): if features.billing.enabled: vector_space = features.vector_space count = len(document_ids) + if features.billing.subscription.plan == "sandbox" and count > 1: + raise ValueError("Your current plan does not support batch upload, please upgrade your plan.") batch_upload_limit = int(dify_config.BATCH_UPLOAD_LIMIT) if count > batch_upload_limit: raise ValueError(f"You have reached the batch upload limit of {batch_upload_limit}.") From 713902dc47973e1ae831cd1a257e254c482d9da0 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 1 Apr 2025 16:52:07 +0800 Subject: [PATCH 011/290] Feat/loop break node (#17268) --- web/app/account/account-page/index.tsx | 2 +- web/app/account/avatar.tsx | 2 +- .../icons/assets/vender/workflow/loop-end.svg | 5 + .../icons/src/public/education/Triangle.tsx | 14 +- .../icons/src/vender/workflow/LoopEnd.json | 38 +++++ .../icons/src/vender/workflow/LoopEnd.tsx | 20 +++ .../base/icons/src/vender/workflow/index.ts | 1 + web/app/components/base/select/pure.tsx | 157 ++++++++++++++++++ .../header/account-dropdown/index.tsx | 2 +- .../components/header/plan-badge/index.tsx | 4 +- web/app/components/workflow/block-icon.tsx | 3 + .../workflow/block-selector/blocks.tsx | 11 +- .../workflow/block-selector/constants.tsx | 6 + web/app/components/workflow/constants.ts | 16 ++ .../workflow/hooks/use-nodes-data.ts | 7 + .../workflow/hooks/use-nodes-interactions.ts | 31 ++-- .../use-workflow-node-iteration-finished.ts | 14 ++ .../use-workflow-node-loop-finished.ts | 17 +- .../use-workflow-node-loop-next.ts | 20 +-- .../use-workflow-node-loop-started.ts | 3 - web/app/components/workflow/index.tsx | 3 + .../nodes/_base/components/help-link.tsx | 3 + .../panel-operator/panel-operator-popup.tsx | 2 +- .../nodes/_base/components/variable-tag.tsx | 3 +- .../nodes/_base/components/variable/utils.ts | 155 +++++++++++++++-- .../variable/var-reference-picker.tsx | 2 +- .../variable/var-reference-vars.tsx | 9 +- .../_base/hooks/use-available-var-list.ts | 6 +- .../nodes/_base/hooks/use-node-help-link.ts | 13 +- .../components/workflow/nodes/_base/node.tsx | 34 +++- .../assigner/components/var-list/index.tsx | 5 +- .../workflow/nodes/assigner/node.tsx | 1 - .../workflow/nodes/assigner/use-config.ts | 14 +- .../nodes/iteration/use-interactions.ts | 6 +- .../components/workflow/nodes/llm/types.ts | 47 ++++++ .../workflow/nodes/loop-end/default.ts | 23 +++ .../nodes/loop/components/condition-wrap.tsx | 3 - .../loop/components/loop-variables/empty.tsx | 13 ++ .../components/loop-variables/form-item.tsx | 144 ++++++++++++++++ .../loop/components/loop-variables/index.tsx | 28 ++++ .../loop-variables/input-mode-selec.tsx | 37 +++++ .../loop/components/loop-variables/item.tsx | 78 +++++++++ .../loop-variables/variable-type-select.tsx | 51 ++++++ .../components/workflow/nodes/loop/default.ts | 5 + .../components/workflow/nodes/loop/panel.tsx | 29 +++- .../components/workflow/nodes/loop/types.ts | 18 ++ .../workflow/nodes/loop/use-config.ts | 56 ++++++- .../workflow/nodes/loop/use-interactions.ts | 7 +- .../components/node-variable-item.tsx | 91 +++++++--- .../workflow/operator/add-block.tsx | 2 + web/app/components/workflow/run/hooks.ts | 7 +- .../run/loop-log/loop-log-trigger.tsx | 9 +- .../run/loop-log/loop-result-panel.tsx | 20 ++- web/app/components/workflow/run/node.tsx | 25 ++- .../workflow/run/special-result-panel.tsx | 4 + .../components/workflow/run/tracing-panel.tsx | 2 + .../workflow/simple-node/constants.ts | 1 + .../components/workflow/simple-node/index.tsx | 148 +++++++++++++++++ .../components/workflow/simple-node/types.ts | 3 + web/app/components/workflow/types.ts | 14 +- web/app/components/workflow/utils.ts | 16 +- web/i18n/en-US/workflow.ts | 12 ++ web/i18n/zh-Hans/workflow.ts | 12 ++ web/types/workflow.ts | 2 + 64 files changed, 1397 insertions(+), 139 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/workflow/loop-end.svg create mode 100644 web/app/components/base/icons/src/vender/workflow/LoopEnd.json create mode 100644 web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx create mode 100644 web/app/components/base/select/pure.tsx create mode 100644 web/app/components/workflow/nodes/loop-end/default.ts create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/index.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/input-mode-selec.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx create mode 100644 web/app/components/workflow/nodes/loop/components/loop-variables/variable-type-select.tsx create mode 100644 web/app/components/workflow/simple-node/constants.ts create mode 100644 web/app/components/workflow/simple-node/index.tsx create mode 100644 web/app/components/workflow/simple-node/types.ts diff --git a/web/app/account/account-page/index.tsx b/web/app/account/account-page/index.tsx index d09a8c2cfe..72d2648c23 100644 --- a/web/app/account/account-page/index.tsx +++ b/web/app/account/account-page/index.tsx @@ -144,7 +144,7 @@ export default function AccountPage() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/account/avatar.tsx b/web/app/account/avatar.tsx index 63db0f37dc..ea897e639f 100644 --- a/web/app/account/avatar.tsx +++ b/web/app/account/avatar.tsx @@ -78,7 +78,7 @@ export default function AppSelector() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/components/base/icons/assets/vender/workflow/loop-end.svg b/web/app/components/base/icons/assets/vender/workflow/loop-end.svg new file mode 100644 index 0000000000..cedacb9c5d --- /dev/null +++ b/web/app/components/base/icons/assets/vender/workflow/loop-end.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/src/public/education/Triangle.tsx b/web/app/components/base/icons/src/public/education/Triangle.tsx index 34f2a50666..85aa518ad2 100644 --- a/web/app/components/base/icons/src/public/education/Triangle.tsx +++ b/web/app/components/base/icons/src/public/education/Triangle.tsx @@ -4,12 +4,16 @@ 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' +import type { IconData } from '@/app/components/base/icons/IconBase' -const Icon = React.forwardRef, Omit>(( - props, - ref, -) => ) +const Icon = ( + { + ref, + ...props + }: React.SVGProps & { + ref?: React.RefObject>; + }, +) => Icon.displayName = 'Triangle' diff --git a/web/app/components/base/icons/src/vender/workflow/LoopEnd.json b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json new file mode 100644 index 0000000000..1427dfdcc5 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/LoopEnd.json @@ -0,0 +1,38 @@ +{ + "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": "g", + "attributes": { + "id": "ongoing" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Vector", + "fill-rule": "evenodd", + "clip-rule": "evenodd", + "d": "M8 2.75C5.10051 2.75 2.75 5.10051 2.75 8C2.75 10.8995 5.1005 13.25 8 13.25C8.41421 13.25 8.75 13.5858 8.75 14C8.75 14.4142 8.41421 14.75 8 14.75C4.27208 14.75 1.25 11.7279 1.25 8C1.25 4.27208 4.27208 1.25 8 1.25C8.41421 1.25 8.75 1.58579 8.75 2C8.75 2.41421 8.41421 2.75 8 2.75ZM10.3508 2.42715C10.5582 2.06861 11.017 1.94608 11.3755 2.15349C11.9971 2.51301 12.5556 2.96859 13.0311 3.49984C13.3073 3.8085 13.281 4.28264 12.9724 4.55887C12.6637 4.8351 12.1896 4.80882 11.9133 4.50016C11.5429 4.08625 11.1079 3.73153 10.6245 3.4519C10.2659 3.2445 10.1434 2.7857 10.3508 2.42715ZM8.13634 5.46967C8.42923 5.17678 8.9041 5.17678 9.197 5.46967L11.197 7.46967C11.4899 7.76256 11.4899 8.23744 11.197 8.53033L9.197 10.5303C8.9041 10.8232 8.42923 10.8232 8.13634 10.5303C7.84344 10.2374 7.84344 9.76256 8.13634 9.46967L8.85601 8.75H5.33333C4.91912 8.75 4.58333 8.41421 4.58333 8C4.58333 7.58579 4.91912 7.25 5.33333 7.25H8.85601L8.13634 6.53033C7.84344 6.23744 7.84344 5.76256 8.13634 5.46967ZM13.7414 6.09691C14.1478 6.01676 14.5422 6.28123 14.6224 6.68762C14.7062 7.1128 14.75 7.55166 14.75 8C14.75 8.44834 14.7062 8.88721 14.6224 9.31234C14.5422 9.71872 14.1478 9.98318 13.7414 9.90302C13.335 9.82287 13.0706 9.42845 13.1507 9.02206C13.2158 8.69213 13.25 8.35046 13.25 8C13.25 7.64954 13.2158 7.30787 13.1507 6.97785C13.0706 6.57146 13.335 6.17705 13.7414 6.09691ZM12.9723 11.4411C13.281 11.7173 13.3073 12.1915 13.0311 12.5002C12.5556 13.0314 11.9971 13.487 11.3756 13.8465C11.017 14.0539 10.5582 13.9314 10.3508 13.5729C10.1434 13.2143 10.2659 12.7556 10.6244 12.5481C11.1079 12.2685 11.5429 11.9138 11.9133 11.4999C12.1895 11.1912 12.6637 11.1649 12.9723 11.4411Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + }, + "name": "LoopEnd" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx b/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx new file mode 100644 index 0000000000..0b8f71d2d8 --- /dev/null +++ b/web/app/components/base/icons/src/vender/workflow/LoopEnd.tsx @@ -0,0 +1,20 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LoopEnd.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 = 'LoopEnd' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/workflow/index.ts b/web/app/components/base/icons/src/vender/workflow/index.ts index 284be92712..7167b71b44 100644 --- a/web/app/components/base/icons/src/vender/workflow/index.ts +++ b/web/app/components/base/icons/src/vender/workflow/index.ts @@ -13,6 +13,7 @@ export { default as Jinja } from './Jinja' export { default as KnowledgeRetrieval } from './KnowledgeRetrieval' export { default as ListFilter } from './ListFilter' export { default as Llm } from './Llm' +export { default as LoopEnd } from './LoopEnd' export { default as Loop } from './Loop' export { default as ParameterExtractor } from './ParameterExtractor' export { default as QuestionClassifier } from './QuestionClassifier' diff --git a/web/app/components/base/select/pure.tsx b/web/app/components/base/select/pure.tsx new file mode 100644 index 0000000000..81cc2fbadf --- /dev/null +++ b/web/app/components/base/select/pure.tsx @@ -0,0 +1,157 @@ +import { + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowDownSLine, + RiCheckLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import type { + PortalToFollowElemOptions, +} from '@/app/components/base/portal-to-follow-elem' +import cn from '@/utils/classnames' + +type Option = { + label: string + value: string +} + +type PureSelectProps = { + options: Option[] + value?: string + onChange?: (value: string) => void + containerProps?: PortalToFollowElemOptions & { + open?: boolean + onOpenChange?: (open: boolean) => void + } + triggerProps?: { + className?: string + }, + popupProps?: { + wrapperClassName?: string + className?: string + itemClassName?: string + title?: string + }, +} +const PureSelect = ({ + options, + value, + onChange, + containerProps, + triggerProps, + popupProps, +}: PureSelectProps) => { + const { t } = useTranslation() + const { + open, + onOpenChange, + placement, + offset, + } = containerProps || {} + const { + className: triggerClassName, + } = triggerProps || {} + const { + wrapperClassName: popupWrapperClassName, + className: popupClassName, + itemClassName: popupItemClassName, + title: popupTitle, + } = popupProps || {} + + const [localOpen, setLocalOpen] = useState(false) + const mergedOpen = open ?? localOpen + + const handleOpenChange = useCallback((openValue: boolean) => { + onOpenChange?.(openValue) + setLocalOpen(openValue) + }, [onOpenChange]) + + const selectedOption = options.find(option => option.value === value) + const triggerText = selectedOption?.label || t('common.placeholder.select') + + return ( + + handleOpenChange(!mergedOpen)} + asChild + > +
+
+ {triggerText} +
+ +
+
+ +
+ { + popupTitle && ( +
+ {popupTitle} +
+ ) + } + { + options.map(option => ( +
{ + onChange?.(option.value) + handleOpenChange(false) + }} + > +
+ {option.label} +
+ { + value === option.value && + } +
+ )) + } +
+
+
+ ) +} + +export default PureSelect diff --git a/web/app/components/header/account-dropdown/index.tsx b/web/app/components/header/account-dropdown/index.tsx index 1a0cc96b98..66b61d7ec1 100644 --- a/web/app/components/header/account-dropdown/index.tsx +++ b/web/app/components/header/account-dropdown/index.tsx @@ -94,7 +94,7 @@ export default function AppSelector() { {userProfile.name} {isEducationAccount && ( - + EDU )} diff --git a/web/app/components/header/plan-badge/index.tsx b/web/app/components/header/plan-badge/index.tsx index 37cbe2a710..292025caeb 100644 --- a/web/app/components/header/plan-badge/index.tsx +++ b/web/app/components/header/plan-badge/index.tsx @@ -42,8 +42,8 @@ const PlanBadge: FC = ({ plan, allowHover, sandboxAsUpgrade = fa if (plan === Plan.professional) { return
- - {isEducationWorkspace && } + + {isEducationWorkspace && } pro
diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index fa8b56546a..1e76efc2aa 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -16,6 +16,7 @@ import { ListFilter, Llm, Loop, + LoopEnd, ParameterExtractor, QuestionClassifier, TemplatingTransform, @@ -54,6 +55,7 @@ const getIcon = (type: BlockEnum, className: string) => { [BlockEnum.Iteration]: , [BlockEnum.LoopStart]: , [BlockEnum.Loop]: , + [BlockEnum.LoopEnd]: , [BlockEnum.ParameterExtractor]: , [BlockEnum.DocExtractor]: , [BlockEnum.ListFilter]: , @@ -68,6 +70,7 @@ const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.IfElse]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.Iteration]: 'bg-util-colors-cyan-cyan-500', [BlockEnum.Loop]: 'bg-util-colors-cyan-cyan-500', + [BlockEnum.LoopEnd]: 'bg-util-colors-warning-warning-500', [BlockEnum.HttpRequest]: 'bg-util-colors-violet-violet-500', [BlockEnum.Answer]: 'bg-util-colors-warning-warning-500', [BlockEnum.KnowledgeRetrieval]: 'bg-util-colors-green-green-500', diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index f97c1a261a..4182530a91 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -15,6 +15,7 @@ import { BLOCK_CLASSIFICATIONS } from './constants' import { useBlocks } from './hooks' import type { ToolDefaultValue } from './types' import Tooltip from '@/app/components/base/tooltip' +import Badge from '@/app/components/base/badge' type BlocksProps = { searchText: string @@ -90,7 +91,15 @@ const Blocks = ({ className='mr-2 shrink-0' type={block.type} /> -
{block.title}
+
{block.title}
+ { + block.type === BlockEnum.LoopEnd && ( + + ) + }
)) diff --git a/web/app/components/workflow/block-selector/constants.tsx b/web/app/components/workflow/block-selector/constants.tsx index 7e8a7f7a3e..680cbf45b9 100644 --- a/web/app/components/workflow/block-selector/constants.tsx +++ b/web/app/components/workflow/block-selector/constants.tsx @@ -39,6 +39,12 @@ export const BLOCKS: Block[] = [ type: BlockEnum.IfElse, title: 'IF/ELSE', }, + { + classification: BlockClassificationEnum.Logic, + type: BlockEnum.LoopEnd, + title: 'Exit Loop', + description: '', + }, { classification: BlockClassificationEnum.Logic, type: BlockEnum.Iteration, diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index fce79cb1df..cdfd963cfa 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -21,6 +21,7 @@ import ListFilterDefault from './nodes/list-operator/default' import IterationStartDefault from './nodes/iteration-start/default' import AgentDefault from './nodes/agent/default' import LoopStartDefault from './nodes/loop-start/default' +import LoopEndDefault from './nodes/loop-end/default' type NodesExtraData = { author: string @@ -122,6 +123,15 @@ export const NODES_EXTRA_DATA: Record = { getAvailableNextNodes: LoopStartDefault.getAvailableNextNodes, checkValid: LoopStartDefault.checkValid, }, + [BlockEnum.LoopEnd]: { + author: 'Dify', + about: '', + availablePrevNodes: [], + availableNextNodes: [], + getAvailablePrevNodes: LoopEndDefault.getAvailablePrevNodes, + getAvailableNextNodes: LoopEndDefault.getAvailableNextNodes, + checkValid: LoopEndDefault.checkValid, + }, [BlockEnum.Code]: { author: 'Dify', about: '', @@ -297,6 +307,12 @@ export const NODES_INITIAL_DATA = { desc: '', ...LoopStartDefault.defaultValue, }, + [BlockEnum.LoopEnd]: { + type: BlockEnum.LoopEnd, + title: '', + desc: '', + ...LoopEndDefault.defaultValue, + }, [BlockEnum.Code]: { type: BlockEnum.Code, title: '', diff --git a/web/app/components/workflow/hooks/use-nodes-data.ts b/web/app/components/workflow/hooks/use-nodes-data.ts index c68ce92e34..aeb45ddb93 100644 --- a/web/app/components/workflow/hooks/use-nodes-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-data.ts @@ -42,6 +42,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean const availableNextBlocks = useMemo(() => { if (!nodeType) return [] + return nodesExtraData[nodeType].availableNextNodes || [] }, [nodeType, nodesExtraData]) @@ -54,6 +55,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) return false + if (!isInLoop && nType === BlockEnum.LoopEnd) + return false + return true }), availableNextBlocks: availableNextBlocks.filter((nType) => { @@ -63,6 +67,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, isInIteration?: boolean if (isInLoop && (nType === BlockEnum.Iteration || nType === BlockEnum.Loop || nType === BlockEnum.End)) return false + if (!isInLoop && nType === BlockEnum.LoopEnd) + return false + return true }), } diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 6dc7f7e948..90231cfcc8 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -39,6 +39,7 @@ import { import { genNewNodeTitleFromOld, generateNewNode, + getNodeCustomTypeByNodeDataType, getNodesConnectedSourceOrTargetHandleIdsMap, getTopLeftNodePosition, } from '../utils' @@ -638,7 +639,7 @@ export const useNodesInteractions = () => { } if (node.id === currentNode.parentId) - node.data._children = node.data._children?.filter(child => child !== nodeId) + node.data._children = node.data._children?.filter(child => child.nodeId !== nodeId) }) draft.splice(currentNodeIndex, 1) }) @@ -686,6 +687,7 @@ export const useNodesInteractions = () => { newIterationStartNode, newLoopStartNode, } = generateNewNode({ + type: getNodeCustomTypeByNodeDataType(nodeType), data: { ...NODES_INITIAL_DATA[nodeType], title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`), @@ -775,10 +777,10 @@ export const useNodesInteractions = () => { } if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) }) draft.push(newNode) @@ -853,7 +855,7 @@ export const useNodesInteractions = () => { let newEdge - if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) { + if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier) && (nodeType !== BlockEnum.LoopEnd)) { newEdge = { id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, type: CUSTOM_EDGE, @@ -901,7 +903,7 @@ export const useNodesInteractions = () => { } if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) { node.data.start_node_id = newNode.id @@ -909,7 +911,7 @@ export const useNodesInteractions = () => { } if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) { node.data.start_node_id = newNode.id @@ -1004,7 +1006,7 @@ export const useNodesInteractions = () => { const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop - if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) { + if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.LoopEnd) { newNextEdge = { id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`, type: CUSTOM_EDGE, @@ -1049,9 +1051,9 @@ export const useNodesInteractions = () => { node.position.x += NODE_WIDTH_X_OFFSET if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id) - node.data._children?.push(newNode.id) + node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type }) }) draft.push(newNode) if (newIterationStartNode) @@ -1117,6 +1119,7 @@ export const useNodesInteractions = () => { newIterationStartNode, newLoopStartNode, } = generateNewNode({ + type: getNodeCustomTypeByNodeDataType(nodeType), data: { ...NODES_INITIAL_DATA[nodeType], title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`), @@ -1240,7 +1243,7 @@ export const useNodesInteractions = () => { if (nodeId) { // If nodeId is provided, copy that specific node const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start - && node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE) + && node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd) if (nodeToCopy) setClipboardElements([nodeToCopy]) } @@ -1254,7 +1257,7 @@ export const useNodesInteractions = () => { return } - const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start) + const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.LoopEnd) if (selectedNode) setClipboardElements([selectedNode]) @@ -1328,7 +1331,7 @@ export const useNodesInteractions = () => { newChildren = copyChildren idMapping = newIdMapping newChildren.forEach((child) => { - newNode.data._children?.push(child.id) + newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type }) }) newChildren.push(newIterationStartNode!) } @@ -1339,7 +1342,7 @@ export const useNodesInteractions = () => { newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id) newChildren.forEach((child) => { - newNode.data._children?.push(child.id) + newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type }) }) newChildren.push(newLoopStartNode!) } @@ -1424,7 +1427,7 @@ export const useNodesInteractions = () => { const nodes = getNodes() const currentNode = nodes.find(n => n.id === nodeId)! - const childrenNodes = nodes.filter(n => currentNode.data._children?.includes(n.id)) + const childrenNodes = nodes.filter(n => currentNode.data._children?.find((c: any) => c.nodeId === n.id)) let rightNode: Node let bottomNode: Node diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts index fdf9e28587..491628cf0b 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-iteration-finished.ts @@ -19,6 +19,8 @@ export const useWorkflowNodeIterationFinished = () => { const { getNodes, setNodes, + edges, + setEdges, } = store.getState() const nodes = getNodes() setWorkflowRunningData(produce(workflowRunningData!, (draft) => { @@ -38,6 +40,18 @@ export const useWorkflowNodeIterationFinished = () => { currentNode.data._runningStatus = data.status }) setNodes(newNodes) + const newEdges = produce(edges, (draft) => { + const incomeEdges = draft.filter((edge) => { + return edge.target === data.node_id + }) + incomeEdges.forEach((edge) => { + edge.data = { + ...edge.data, + _targetRunningStatus: data.status as any, + } + }) + }) + setEdges(newEdges) }, [workflowStore, store]) return { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts index 38064e3658..0efe530807 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-finished.ts @@ -3,7 +3,6 @@ import { useStoreApi } from 'reactflow' import produce from 'immer' import type { LoopFinishedResponse } from '@/types/workflow' import { useWorkflowStore } from '@/app/components/workflow/store' -import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants' export const useWorkflowNodeLoopFinished = () => { const store = useStoreApi() @@ -14,11 +13,12 @@ export const useWorkflowNodeLoopFinished = () => { const { workflowRunningData, setWorkflowRunningData, - setLoopTimes, } = workflowStore.getState() const { getNodes, setNodes, + edges, + setEdges, } = store.getState() const nodes = getNodes() setWorkflowRunningData(produce(workflowRunningData!, (draft) => { @@ -31,13 +31,24 @@ export const useWorkflowNodeLoopFinished = () => { } } })) - setLoopTimes(DEFAULT_LOOP_TIMES) const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! currentNode.data._runningStatus = data.status }) setNodes(newNodes) + const newEdges = produce(edges, (draft) => { + const incomeEdges = draft.filter((edge) => { + return edge.target === data.node_id + }) + incomeEdges.forEach((edge) => { + edge.data = { + ...edge.data, + _targetRunningStatus: data.status as any, + } + }) + }) + setEdges(newEdges) }, [workflowStore, store]) return { diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts index d3c5164dcb..8525e2838b 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-next.ts @@ -2,18 +2,12 @@ import { useCallback } from 'react' import { useStoreApi } from 'reactflow' import produce from 'immer' import type { LoopNextResponse } from '@/types/workflow' -import { useWorkflowStore } from '@/app/components/workflow/store' +import { NodeRunningStatus } from '@/app/components/workflow/types' export const useWorkflowNodeLoopNext = () => { const store = useStoreApi() - const workflowStore = useWorkflowStore() const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => { - const { - loopTimes, - setLoopTimes, - } = workflowStore.getState() - const { data } = params const { getNodes, @@ -23,11 +17,17 @@ export const useWorkflowNodeLoopNext = () => { const nodes = getNodes() const newNodes = produce(nodes, (draft) => { const currentNode = draft.find(node => node.id === data.node_id)! - currentNode.data._loopIndex = loopTimes - setLoopTimes(loopTimes + 1) + currentNode.data._loopIndex = data.index + + draft.forEach((node) => { + if (node.parentId === data.node_id) { + node.data._waitingRun = true + node.data._runningStatus = NodeRunningStatus.Waiting + } + }) }) setNodes(newNodes) - }, [workflowStore, store]) + }, [store]) return { handleWorkflowNodeLoopNext, diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts index 533154bc33..1745f43b60 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-node-loop-started.ts @@ -7,7 +7,6 @@ import produce from 'immer' import { useWorkflowStore } from '@/app/components/workflow/store' import type { LoopStartedResponse } from '@/types/workflow' import { NodeRunningStatus } from '@/app/components/workflow/types' -import { DEFAULT_LOOP_TIMES } from '@/app/components/workflow/constants' export const useWorkflowNodeLoopStarted = () => { const store = useStoreApi() @@ -25,7 +24,6 @@ export const useWorkflowNodeLoopStarted = () => { const { workflowRunningData, setWorkflowRunningData, - setLoopTimes, } = workflowStore.getState() const { getNodes, @@ -41,7 +39,6 @@ export const useWorkflowNodeLoopStarted = () => { status: NodeRunningStatus.Running, }) })) - setLoopTimes(DEFAULT_LOOP_TIMES) const { setViewport, diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 0c4d5aa671..4c48afb56c 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -61,6 +61,8 @@ import CustomIterationStartNode from './nodes/iteration-start' import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants' import CustomLoopStartNode from './nodes/loop-start' import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' +import CustomSimpleNode from './simple-node' +import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' import Operator from './operator' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' @@ -104,6 +106,7 @@ import DatasetsDetailProvider from './datasets-detail-store/provider' const nodeTypes = { [CUSTOM_NODE]: CustomNode, [CUSTOM_NOTE_NODE]: CustomNoteNode, + [CUSTOM_SIMPLE_NODE]: CustomSimpleNode, [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode, [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode, } diff --git a/web/app/components/workflow/nodes/_base/components/help-link.tsx b/web/app/components/workflow/nodes/_base/components/help-link.tsx index 34520f633c..2e7552001b 100644 --- a/web/app/components/workflow/nodes/_base/components/help-link.tsx +++ b/web/app/components/workflow/nodes/_base/components/help-link.tsx @@ -14,6 +14,9 @@ const HelpLink = ({ const { t } = useTranslation() const link = useNodeHelpLink(nodeType) + if (!link) + return null + return (
{ } as any)[type] || VarType.string } +const structTypeToVarType = (type: Type): VarType => { + return ({ + [Type.string]: VarType.string, + [Type.number]: VarType.number, + [Type.boolean]: VarType.boolean, + [Type.object]: VarType.object, + [Type.array]: VarType.array, + } as any)[type] || VarType.string +} + +export const varTypeToStructType = (type: VarType): Type => { + return ({ + [VarType.string]: Type.string, + [VarType.number]: Type.number, + [VarType.boolean]: Type.boolean, + [VarType.object]: Type.object, + [VarType.array]: Type.array, + } as any)[type] || Type.string +} + +const findExceptVarInStructuredProperties = (properties: Record, filterVar: (payload: Var, selector: ValueSelector) => boolean): Record => { + const res = produce(properties, (draft) => { + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(item.type), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + +const findExceptVarInStructuredOutput = (structuredOutput: StructuredOutput, filterVar: (payload: Var, selector: ValueSelector) => boolean): StructuredOutput => { + const res = produce(structuredOutput, (draft) => { + const properties = draft.schema.properties + Object.keys(properties).forEach((key) => { + const item = properties[key] + const isObj = item.type === Type.object + if (!isObj && !filterVar({ + variable: key, + type: structTypeToVarType(item.type), + }, [key])) { + delete properties[key] + return + } + if (item.type === Type.object && item.properties) + item.properties = findExceptVarInStructuredProperties(item.properties, filterVar) + }) + return draft + }) + return res +} + const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: ValueSelector) => boolean, value_selector: ValueSelector, isFile?: boolean): Var => { const { children } = obj + const isStructuredOutput = !!(children as StructuredOutput)?.schema?.properties + const res: Var = { variable: obj.variable, type: isFile ? VarType.file : VarType.object, - children: children.filter((item: Var) => { + children: isStructuredOutput ? findExceptVarInStructuredOutput(children, filterVar) : children.filter((item: Var) => { const { children } = item const currSelector = [...value_selector, item.variable] if (!children) return filterVar(item, currSelector) - const obj = findExceptVarInObject(item, filterVar, currSelector, false) // File doesn't contains file children - return obj.children && obj.children?.length > 0 + return obj.children && (obj.children as Var[])?.length > 0 }), } return res @@ -139,10 +203,17 @@ const formatItem = ( } case BlockEnum.LLM: { - res.vars = LLM_OUTPUT_STRUCT + res.vars = [...LLM_OUTPUT_STRUCT] + if (data.structured_output_enabled && data.structured_output?.schema?.properties && Object.keys(data.structured_output.schema.properties).length > 0) { + res.vars.push({ + variable: 'structured_output', + type: VarType.object, + children: data.structured_output, + }) + } + break } - case BlockEnum.KnowledgeRetrieval: { res.vars = KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT break @@ -286,6 +357,21 @@ const formatItem = ( break } + case BlockEnum.Loop: { + const { loop_variables } = data as LoopNodeType + res.isLoop = true + res.vars = loop_variables?.map((v) => { + return { + variable: v.label, + type: v.var_type, + isLoopVariable: true, + nodeId: res.nodeId, + } + }) || [] + + break + } + case BlockEnum.DocExtractor: { res.vars = [ { @@ -405,7 +491,7 @@ const formatItem = ( return false const obj = findExceptVarInObject(isFile ? { ...v, children } : v, filterVar, selector, isFile) - return obj?.children && obj?.children.length > 0 + return obj?.children && ((obj?.children as Var[]).length > 0 || Object.keys((obj?.children as StructuredOutput)?.schema?.properties || {}).length > 0) }).map((v) => { const isFile = v.type === VarType.file @@ -457,7 +543,7 @@ export const toNodeOutputVars = ( }, } const res = [ - ...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)), + ...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node?.data?.type)), ...(environmentVariables.length > 0 ? [ENV_NODE] : []), ...((isChatMode && conversationVariables.length > 0) ? [CHAT_VAR_NODE] : []), ].map((node) => { @@ -579,8 +665,7 @@ export const getVarType = ({ isConstant, environmentVariables = [], conversationVariables = [], -}: -{ +}: { valueSelector: ValueSelector parentNode?: Node | null isIterationItem?: boolean @@ -644,7 +729,7 @@ export const getVarType = ({ const isEnv = isENV(valueSelector) const isChatVar = isConversationVar(valueSelector) const startNode = availableNodes.find((node: any) => { - return node.data.type === BlockEnum.Start + return node?.data.type === BlockEnum.Start }) const targetVarNodeId = isSystem ? startNode?.id : valueSelector[0] @@ -655,10 +740,30 @@ export const getVarType = ({ let type: VarType = VarType.string let curr: any = targetVar.vars + if (isSystem || isEnv || isChatVar) { return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type } else { + const targetVar = curr.find((v: any) => v.variable === valueSelector[1]) + if (!targetVar) + return VarType.string + + const isStructuredOutputVar = !!targetVar.children?.schema?.properties + if (isStructuredOutputVar) { + let currProperties = targetVar.children.schema; + (valueSelector as ValueSelector).slice(2).forEach((key, i) => { + const isLast = i === valueSelector.length - 3 + if (!currProperties) + return + + currProperties = currProperties.properties[key] + if (isLast) + type = structTypeToVarType(currProperties?.type) + }) + return type + } + (valueSelector as ValueSelector).slice(1).forEach((key, i) => { const isLast = i === valueSelector.length - 2 if (Array.isArray(curr)) @@ -741,6 +846,9 @@ export const toNodeAvailableVars = ({ }, ], } + const iterationIndex = beforeNodesOutputVars.findIndex(v => v.nodeId === iterationNode?.id) + if (iterationIndex > -1) + beforeNodesOutputVars.splice(iterationIndex, 1) beforeNodesOutputVars.unshift(iterationVar) } return beforeNodesOutputVars @@ -1181,17 +1289,27 @@ export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, new }) return newNode } + const varToValueSelectorList = (v: Var, parentValueSelector: ValueSelector, res: ValueSelector[]) => { if (!v.variable) return res.push([...parentValueSelector, v.variable]) + const isStructuredOutput = !!(v.children as StructuredOutput)?.schema?.properties - if (v.children && v.children.length > 0) { - v.children.forEach((child) => { + if ((v.children as Var[])?.length > 0) { + (v.children as Var[]).forEach((child) => { varToValueSelectorList(child, [...parentValueSelector, v.variable], res) }) } + if (isStructuredOutput) { + Object.keys((v.children as StructuredOutput)?.schema?.properties || {}).forEach((key) => { + varToValueSelectorList({ + variable: key, + type: structTypeToVarType((v.children as StructuredOutput)?.schema?.properties[key].type), + }, [...parentValueSelector, v.variable], res) + }) + } } const varsToValueSelectorList = (vars: Var | Var[], parentValueSelector: ValueSelector, res: ValueSelector[]) => { @@ -1225,7 +1343,16 @@ export const getNodeOutputVars = (node: Node, isChatMode: boolean): ValueSelecto } case BlockEnum.LLM: { - varsToValueSelectorList(LLM_OUTPUT_STRUCT, [id], res) + const vars = [...LLM_OUTPUT_STRUCT] + const llmNodeData = data as LLMNodeType + if (llmNodeData.structured_output_enabled && llmNodeData.structured_output?.schema?.properties && Object.keys(llmNodeData.structured_output.schema.properties).length > 0) { + vars.push({ + variable: 'structured_output', + type: VarType.object, + children: llmNodeData.structured_output, + }) + } + varsToValueSelectorList(vars, [id], res) break } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 0b8e0bbd57..1647642950 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -101,7 +101,7 @@ const VarReferencePicker: FC = ({ const isChatMode = useIsChatMode() const { getCurrentVariableType } = useWorkflowVariables() - const { availableNodes, availableVars } = useAvailableVarList(nodeId, { + const { availableVars, availableNodesWithParent: availableNodes } = useAvailableVarList(nodeId, { onlyLeafNodeVar, passedInAvailableNodes, filterVar, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 6971cf2208..dfd4d19c00 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -16,6 +16,7 @@ import Input from '@/app/components/base/input' import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' import { checkKeys } from '@/utils/var' import { FILE_STRUCT } from '@/app/components/workflow/constants' +import { Loop } from '@/app/components/base/icons/src/vender/workflow' type ObjectChildrenProps = { nodeId: string @@ -38,6 +39,7 @@ type ItemProps = { itemWidth?: number isSupportFileVar?: boolean isException?: boolean + isLoopVar?: boolean } const Item: FC = ({ @@ -50,6 +52,7 @@ const Item: FC = ({ itemWidth, isSupportFileVar, isException, + isLoopVar, }) => { const isFile = itemData.type === VarType.file const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0) @@ -112,9 +115,10 @@ const Item: FC = ({ onMouseDown={e => e.preventDefault()} >
- {!isEnv && !isChatVar && } + {!isEnv && !isChatVar && !isLoopVar && } {isEnv && } - {isChatVar && } + {isChatVar && } + {isLoopVar && } {!isEnv && !isChatVar && (
{itemData.variable}
)} @@ -317,6 +321,7 @@ const VarReferenceVars: FC = ({ itemWidth={itemWidth} isSupportFileVar={isSupportFileVar} isException={v.isException} + isLoopVar={item.isLoop} /> ))}
)) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts index 3e13dcb786..e1a6a8bd8d 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-available-var-list.ts @@ -24,11 +24,11 @@ const useAvailableVarList = (nodeId: string, { onlyLeafNodeVar: false, filterVar: () => true, }) => { - const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow() + const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const { getNodeAvailableVars } = useWorkflowVariables() const isChatMode = useIsChatMode() - const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)) + const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId)) const { parentNode: iterationNode, @@ -46,7 +46,7 @@ const useAvailableVarList = (nodeId: string, { return { availableVars, availableNodes, - availableNodesWithParent: iterationNode ? [...availableNodes, iterationNode] : availableNodes, + availableNodesWithParent: availableNodes, } } diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts index 0509848101..3c68fbd1fd 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-help-link.ts @@ -26,9 +26,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => { [BlockEnum.VariableAggregator]: 'variable-aggregator', [BlockEnum.Assigner]: 'variable-assigner', [BlockEnum.Iteration]: 'iteration', - [BlockEnum.IterationStart]: 'iteration', [BlockEnum.Loop]: 'loop', - [BlockEnum.LoopStart]: 'loop', [BlockEnum.ParameterExtractor]: 'parameter-extractor', [BlockEnum.HttpRequest]: 'http-request', [BlockEnum.Tool]: 'tools', @@ -52,9 +50,7 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => { [BlockEnum.VariableAggregator]: 'variable-aggregator', [BlockEnum.Assigner]: 'variable-assigner', [BlockEnum.Iteration]: 'iteration', - [BlockEnum.IterationStart]: 'iteration', [BlockEnum.Loop]: 'loop', - [BlockEnum.LoopStart]: 'loop', [BlockEnum.ParameterExtractor]: 'parameter-extractor', [BlockEnum.HttpRequest]: 'http-request', [BlockEnum.Tool]: 'tools', @@ -62,7 +58,12 @@ export const useNodeHelpLink = (nodeType: BlockEnum) => { [BlockEnum.ListFilter]: 'list-operator', [BlockEnum.Agent]: 'agent', } - }, [language]) + }, [language]) as Record + + const link = linkMap[nodeType] + + if (!link) + return '' - return `${prefixLink}${linkMap[nodeType]}` + return `${prefixLink}${link}` } diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 177e76e5ab..527b2f094d 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -1,6 +1,6 @@ import type { FC, - ReactNode, + ReactElement, } from 'react' import { cloneElement, @@ -46,7 +46,7 @@ import BlockIcon from '@/app/components/workflow/block-icon' import Tooltip from '@/app/components/base/tooltip' type BaseNodeProps = { - children: ReactNode + children: ReactElement } & NodeProps const BaseNode: FC = ({ @@ -104,6 +104,30 @@ const BaseNode: FC = ({ } }, [data._runningStatus, showSelectedBorder]) + const LoopIndex = useMemo(() => { + let text = '' + + if (data._runningStatus === NodeRunningStatus.Running) + text = t('workflow.nodes.loop.currentLoopCount', { count: data._loopIndex }) + if (data._runningStatus === NodeRunningStatus.Succeeded || data._runningStatus === NodeRunningStatus.Failed) + text = t('workflow.nodes.loop.totalLoopCount', { count: data._loopIndex }) + + if (text) { + return ( +
+ {text} +
+ ) + } + + return null + }, [data._loopIndex, data._runningStatus, t]) + return (
= ({ ) } { - data._loopLength && data._loopIndex && data._runningStatus === NodeRunningStatus.Running && ( -
- {data._loopIndex > data._loopLength ? data._loopLength : data._loopIndex}/{data._loopLength} -
- ) + data.type === BlockEnum.Loop && data._loopIndex && LoopIndex } { (data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && ( diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index 1632072eba..e334c0b281 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -95,10 +95,13 @@ const VarList: FC = ({ }, [onOpen]) const handleFilterToAssignedVar = useCallback((index: number) => { - return (payload: Var, valueSelector: ValueSelector) => { + return (payload: Var) => { const item = list[index] const assignedVarType = item.variable_selector ? getAssignedVarType?.(item.variable_selector) : undefined + if (item.variable_selector.join('.') === `${payload.nodeId}.${payload.variable}`) + return false + if (!filterToAssignedVar || !item.variable_selector || !assignedVarType || !item.operation) return true diff --git a/web/app/components/workflow/nodes/assigner/node.tsx b/web/app/components/workflow/nodes/assigner/node.tsx index 870ba9873c..2dd1ead4f8 100644 --- a/web/app/components/workflow/nodes/assigner/node.tsx +++ b/web/app/components/workflow/nodes/assigner/node.tsx @@ -13,7 +13,6 @@ const NodeComponent: FC> = ({ data, }) => { const { t } = useTranslation() - const nodes: Node[] = useNodes() if (data.version === '2') { const { items: operationItems } = data diff --git a/web/app/components/workflow/nodes/assigner/use-config.ts b/web/app/components/workflow/nodes/assigner/use-config.ts index ad7d066ef1..e7beb1f37a 100644 --- a/web/app/components/workflow/nodes/assigner/use-config.ts +++ b/web/app/components/workflow/nodes/assigner/use-config.ts @@ -31,7 +31,7 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { } const store = useStoreApi() - const { getBeforeNodesInSameBranch } = useWorkflow() + const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const { getNodes, @@ -39,11 +39,9 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { const currentNode = getNodes().find(n => n.id === id) const isInIteration = payload.isInIteration const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null - const isInLoop = payload.isInLoop - const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null const availableNodes = useMemo(() => { - return getBeforeNodesInSameBranch(id) - }, [getBeforeNodesInSameBranch, id]) + return getBeforeNodesInSameBranchIncludeParent(id) + }, [getBeforeNodesInSameBranchIncludeParent, id]) const { inputs, setInputs } = useNodeCrud(id, payload) const newSetInputs = useCallback((newInputs: AssignerNodeType) => { const finalInputs = produce(newInputs, (draft) => { @@ -56,13 +54,13 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { const { getCurrentVariableType } = useWorkflowVariables() const getAssignedVarType = useCallback((valueSelector: ValueSelector) => { return getCurrentVariableType({ - parentNode: isInIteration ? iterationNode : loopNode, + parentNode: isInIteration ? iterationNode : null, valueSelector: valueSelector || [], availableNodes, isChatMode, isConstant: false, }) - }, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode]) + }, [getCurrentVariableType, isInIteration, iterationNode, availableNodes, isChatMode]) const handleOperationListChanges = useCallback((items: AssignerNodeOperation[]) => { const newInputs = produce(inputs, (draft) => { @@ -91,6 +89,8 @@ const useConfig = (id: string, rawPayload: AssignerNodeType) => { }, []) const filterAssignedVar = useCallback((varPayload: Var, selector: ValueSelector) => { + if (varPayload.isLoopVariable) + return true return selector.join('.').startsWith('conversation') }, []) diff --git a/web/app/components/workflow/nodes/iteration/use-interactions.ts b/web/app/components/workflow/nodes/iteration/use-interactions.ts index c0c005bdf6..c294cfd6aa 100644 --- a/web/app/components/workflow/nodes/iteration/use-interactions.ts +++ b/web/app/components/workflow/nodes/iteration/use-interactions.ts @@ -6,7 +6,10 @@ import type { BlockEnum, Node, } from '../../types' -import { generateNewNode } from '../../utils' +import { + generateNewNode, + getNodeCustomTypeByNodeDataType, +} from '../../utils' import { ITERATION_PADDING, NODES_INITIAL_DATA, @@ -115,6 +118,7 @@ export const useNodeIterationInteractions = () => { const childNodeType = child.data.type as BlockEnum const nodesWithSameType = nodes.filter(node => node.data.type === childNodeType) const { newNode } = generateNewNode({ + type: getNodeCustomTypeByNodeDataType(childNodeType), data: { ...NODES_INITIAL_DATA[childNodeType], ...child.data, diff --git a/web/app/components/workflow/nodes/llm/types.ts b/web/app/components/workflow/nodes/llm/types.ts index a7774fca2e..a4931f7017 100644 --- a/web/app/components/workflow/nodes/llm/types.ts +++ b/web/app/components/workflow/nodes/llm/types.ts @@ -15,4 +15,51 @@ export type LLMNodeType = CommonNodeType & { enabled: boolean configs?: VisionSetting } + structured_output_enabled?: boolean + structured_output?: StructuredOutput +} + +export enum Type { + string = 'string', + number = 'number', + boolean = 'boolean', + object = 'object', + array = 'array', +} + +export enum ArrayType { + string = 'array[string]', + number = 'array[number]', + boolean = 'array[boolean]', + object = 'array[object]', +} + +export type TypeWithArray = Type | ArrayType + +type ArrayItemType = Exclude +export type ArrayItems = Omit & { type: ArrayItemType } + +export type SchemaEnumType = string[] | number[] + +export type Field = { + type: Type + properties?: { // Object has properties + [key: string]: Field + } + required?: string[] // Key of required properties in object + description?: string + items?: ArrayItems // Array has items. Define the item type + enum?: SchemaEnumType // Enum values + additionalProperties?: false // Required in object by api. Just set false +} + +export type StructuredOutput = { + schema: SchemaRoot +} + +export type SchemaRoot = { + type: Type.object + properties: Record + required?: string[] + additionalProperties: false } diff --git a/web/app/components/workflow/nodes/loop-end/default.ts b/web/app/components/workflow/nodes/loop-end/default.ts new file mode 100644 index 0000000000..c136704123 --- /dev/null +++ b/web/app/components/workflow/nodes/loop-end/default.ts @@ -0,0 +1,23 @@ +import type { NodeDefault } from '../../types' +import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks' +import type { + SimpleNodeType, +} from '@/app/components/workflow/simple-node/types' + +const nodeDefault: NodeDefault = { + defaultValue: {}, + getAvailablePrevNodes(isChatMode: boolean) { + const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS + return nodes + }, + getAvailableNextNodes() { + return [] + }, + checkValid() { + return { + isValid: true, + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx index 7a80dc2812..7aef364658 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-wrap.tsx @@ -138,9 +138,6 @@ const ConditionWrap: FC = ({ )}
- {!isSubVariable && ( -
- )}
) diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx new file mode 100644 index 0000000000..6fe4aa0a77 --- /dev/null +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/empty.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next' + +const Empty = () => { + const { t } = useTranslation() + + return ( +
+ {t('workflow.nodes.loop.setLoopVariables')} +
+ ) +} + +export default Empty diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx new file mode 100644 index 0000000000..4a05e457b3 --- /dev/null +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/form-item.tsx @@ -0,0 +1,144 @@ +import { + useCallback, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import type { + LoopVariable, +} from '@/app/components/workflow/nodes/loop/types' +import type { + Var, +} from '@/app/components/workflow/types' +import { + ValueType, + VarType, +} from '@/app/components/workflow/types' + +const objectPlaceholder = `# example +# { +# "name": "ray", +# "age": 20 +# }` +const arrayStringPlaceholder = `# example +# [ +# "value1", +# "value2" +# ]` +const arrayNumberPlaceholder = `# example +# [ +# 100, +# 200 +# ]` +const arrayObjectPlaceholder = `# example +# [ +# { +# "name": "ray", +# "age": 20 +# }, +# { +# "name": "lily", +# "age": 18 +# } +# ]` + +type FormItemProps = { + nodeId: string + item: LoopVariable + onChange: (value: any) => void +} +const FormItem = ({ + nodeId, + item, + onChange, +}: FormItemProps) => { + const { t } = useTranslation() + const { value_type, var_type, value } = item + + const handleInputChange = useCallback((e: any) => { + onChange(e.target.value) + }, [onChange]) + + const handleChange = useCallback((value: any) => { + onChange(value) + }, [onChange]) + + const filterVar = useCallback((variable: Var) => { + return variable.type === var_type + }, [var_type]) + + const editorMinHeight = useMemo(() => { + if (var_type === VarType.arrayObject) + return '240px' + return '120px' + }, [var_type]) + const placeholder = useMemo(() => { + if (var_type === VarType.arrayString) + return arrayStringPlaceholder + if (var_type === VarType.arrayNumber) + return arrayNumberPlaceholder + if (var_type === VarType.arrayObject) + return arrayObjectPlaceholder + return objectPlaceholder + }, [var_type]) + + return ( +
+ { + value_type === ValueType.variable && ( + + ) + } + { + value_type === ValueType.constant && var_type === VarType.string && ( +