diff --git a/api/core/helper/ssrf_proxy.py b/api/core/helper/ssrf_proxy.py index 374bd9d57b..64d73c7cd9 100644 --- a/api/core/helper/ssrf_proxy.py +++ b/api/core/helper/ssrf_proxy.py @@ -54,10 +54,12 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs): if response.status_code not in STATUS_FORCELIST: return response else: - logging.warning(f"Received status code {response.status_code} for URL {url} which is in the force list") + logging.warning( + f"Received status code {response.status_code} for URL {url} which is in the force list") except httpx.RequestError as e: - logging.warning(f"Request to URL {url} failed on attempt {retries + 1}: {e}") + logging.warning( + f"Request to URL {url} failed on attempt {retries + 1}: {e}") retries += 1 if retries <= max_retries: diff --git a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py index 30d7f09ec2..79b827797c 100644 --- a/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py +++ b/api/core/rag/datasource/vdb/lindorm/lindorm_vector.py @@ -20,7 +20,8 @@ from extensions.ext_redis import redis_client from models.dataset import Dataset logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") +logging.basicConfig(level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s") logging.getLogger("lindorm").setLevel(logging.WARN) @@ -76,7 +77,8 @@ class LindormVectorStore(BaseVector): @retry(stop=stop_after_attempt(3), wait=wait_fixed(60)) def __fetch_existing_ids(batch_ids: list[str]) -> set[str]: try: - existing_docs = self._client.mget(index=self._collection_name, body={"ids": batch_ids}, _source=False) + existing_docs = self._client.mget(index=self._collection_name, body={ + "ids": batch_ids}, _source=False) return {doc["_id"] for doc in existing_docs["docs"] if doc["found"]} except Exception as e: logger.exception(f"Error fetching batch {batch_ids}: {e}") @@ -88,7 +90,8 @@ class LindormVectorStore(BaseVector): existing_docs = self._client.mget( body={ "docs": [ - {"_index": self._collection_name, "_id": id, "routing": routing} + {"_index": self._collection_name, + "_id": id, "routing": routing} for id, routing in zip(batch_ids, route_ids) ] }, @@ -112,12 +115,13 @@ class LindormVectorStore(BaseVector): def batch(iterable, n): length = len(iterable) for idx in range(0, length, n): - yield iterable[idx : min(idx + n, length)] + yield iterable[idx: min(idx + n, length)] for ids_batch, texts_batch, metadatas_batch in zip( batch(ids, bulk_size), batch(texts, bulk_size), - batch(metadatas, bulk_size) if metadatas is not None else batch([None] * len(ids), bulk_size), + batch(metadatas, bulk_size) if metadatas is not None else batch( + [None] * len(ids), bulk_size), ): existing_ids_set = __fetch_existing_ids(ids_batch) for text, metadata, doc_id in zip(texts_batch, metadatas_batch, ids_batch): @@ -139,7 +143,8 @@ class LindormVectorStore(BaseVector): "_id": uuids[i], "_source": { Field.CONTENT_KEY.value: documents[i].page_content, - Field.VECTOR.value: embeddings[i], # Make sure you pass an array here + # Make sure you pass an array here + Field.VECTOR.value: embeddings[i], Field.METADATA_KEY.value: documents[i].metadata, }, } @@ -148,7 +153,8 @@ class LindormVectorStore(BaseVector): self.refresh() def get_ids_by_metadata_field(self, key: str, value: str): - query = {"query": {"term": {f"{Field.METADATA_KEY.value}.{key}.keyword": value}}} + query = { + "query": {"term": {f"{Field.METADATA_KEY.value}.{key}.keyword": value}}} response = self._client.search(index=self._collection_name, body=query) if response["hits"]["hits"]: return [hit["_id"] for hit in response["hits"]["hits"]] @@ -157,7 +163,8 @@ class LindormVectorStore(BaseVector): def delete_by_metadata_field(self, key: str, value: str): query_str = {"query": {"match": {f"metadata.{key}": f"{value}"}}} - results = self._client.search(index=self._collection_name, body=query_str) + results = self._client.search( + index=self._collection_name, body=query_str) ids = [hit["_id"] for hit in results["hits"]["hits"]] if ids: self.delete_by_ids(ids) @@ -167,15 +174,18 @@ class LindormVectorStore(BaseVector): if self._client.exists(index=self._collection_name, id=id): self._client.delete(index=self._collection_name, id=id) else: - logger.warning(f"DELETE BY ID: ID {id} does not exist in the index.") + logger.warning( + f"DELETE BY ID: ID {id} does not exist in the index.") def delete(self) -> None: try: if self._client.indices.exists(index=self._collection_name): - self._client.indices.delete(index=self._collection_name, params={"timeout": 60}) + self._client.indices.delete( + index=self._collection_name, params={"timeout": 60}) logger.info("Delete index success") else: - logger.warning(f"Index '{self._collection_name}' does not exist. No deletion performed.") + logger.warning( + f"Index '{self._collection_name}' does not exist. No deletion performed.") except Exception as e: logger.exception(f"Error occurred while deleting the index: {e}") raise e @@ -197,9 +207,11 @@ class LindormVectorStore(BaseVector): raise ValueError("All elements in query_vector should be floats") top_k = kwargs.get("top_k", 10) - query = default_vector_search_query(query_vector=query_vector, k=top_k, **kwargs) + query = default_vector_search_query( + query_vector=query_vector, k=top_k, **kwargs) try: - response = self._client.search(index=self._collection_name, body=query) + response = self._client.search( + index=self._collection_name, body=query) except Exception as e: logger.exception(f"Error executing search: {e}") raise @@ -244,7 +256,8 @@ class LindormVectorStore(BaseVector): filters=filters, routing=routing, ) - response = self._client.search(index=self._collection_name, body=full_text_query) + response = self._client.search( + index=self._collection_name, body=full_text_query) docs = [] for hit in response["hits"]["hits"]: docs.append( @@ -262,7 +275,8 @@ class LindormVectorStore(BaseVector): with redis_client.lock(lock_name, timeout=20): collection_exist_cache_key = f"vector_indexing_{self._collection_name}" if redis_client.get(collection_exist_cache_key): - logger.info(f"Collection {self._collection_name} already exists.") + logger.info( + f"Collection {self._collection_name} already exists.") return if self._client.indices.exists(index=self._collection_name): logger.info("{self._collection_name.lower()} already exists.") @@ -281,10 +295,13 @@ class LindormVectorStore(BaseVector): hnsw_ef_construction = kwargs.pop("hnsw_ef_construction", 500) ivfpq_m = kwargs.pop("ivfpq_m", dimension) nlist = kwargs.pop("nlist", 1000) - centroids_use_hnsw = kwargs.pop("centroids_use_hnsw", True if nlist >= 5000 else False) + centroids_use_hnsw = kwargs.pop( + "centroids_use_hnsw", True if nlist >= 5000 else False) centroids_hnsw_m = kwargs.pop("centroids_hnsw_m", 24) - centroids_hnsw_ef_construct = kwargs.pop("centroids_hnsw_ef_construct", 500) - centroids_hnsw_ef_search = kwargs.pop("centroids_hnsw_ef_search", 100) + centroids_hnsw_ef_construct = kwargs.pop( + "centroids_hnsw_ef_construct", 500) + centroids_hnsw_ef_search = kwargs.pop( + "centroids_hnsw_ef_search", 100) mapping = default_text_mapping( dimension, method_name, @@ -303,7 +320,8 @@ class LindormVectorStore(BaseVector): centroids_hnsw_ef_search=centroids_hnsw_ef_search, **kwargs, ) - self._client.indices.create(index=self._collection_name.lower(), body=mapping) + self._client.indices.create( + index=self._collection_name.lower(), body=mapping) redis_client.set(collection_exist_cache_key, 1, ex=3600) # logger.info(f"create index success: {self._collection_name}") @@ -364,7 +382,8 @@ def default_text_mapping(dimension: int, method_name: str, **kwargs: Any) -> dic } if excludes_from_source: - mapping["mappings"]["_source"] = {"excludes": excludes_from_source} # e.g. {"excludes": ["vector_field"]} + # e.g. {"excludes": ["vector_field"]} + mapping["mappings"]["_source"] = {"excludes": excludes_from_source} if method_name == "ivfpq" and routing_field is not None: mapping["settings"]["index"]["knn_routing"] = True @@ -405,7 +424,8 @@ def default_text_search_query( # build complex search_query when either of must/must_not/should/filter is specified if must: if not isinstance(must, list): - raise RuntimeError(f"unexpected [must] clause with {type(filters)}") + raise RuntimeError( + f"unexpected [must] clause with {type(filters)}") if query_clause not in must: must.append(query_clause) else: @@ -415,19 +435,22 @@ def default_text_search_query( if must_not: if not isinstance(must_not, list): - raise RuntimeError(f"unexpected [must_not] clause with {type(filters)}") + raise RuntimeError( + f"unexpected [must_not] clause with {type(filters)}") boolean_query["must_not"] = must_not if should: if not isinstance(should, list): - raise RuntimeError(f"unexpected [should] clause with {type(filters)}") + raise RuntimeError( + f"unexpected [should] clause with {type(filters)}") boolean_query["should"] = should if minimum_should_match != 0: boolean_query["minimum_should_match"] = minimum_should_match if filters: if not isinstance(filters, list): - raise RuntimeError(f"unexpected [filter] clause with {type(filters)}") + raise RuntimeError( + f"unexpected [filter] clause with {type(filters)}") boolean_query["filter"] = filters search_query = {"size": k, "query": {"bool": boolean_query}} @@ -471,8 +494,10 @@ def default_vector_search_query( if filters is not None: # when using filter, transform filter from List[Dict] to Dict as valid format - filters = {"bool": {"must": filters}} if len(filters) > 1 else filters[0] - search_query["query"]["knn"][vector_field]["filter"] = filters # filter should be Dict + filters = {"bool": {"must": filters}} if len( + filters) > 1 else filters[0] + # filter should be Dict + search_query["query"]["knn"][vector_field]["filter"] = filters if filter_type: final_ext["lvector"]["filter_type"] = filter_type @@ -489,7 +514,8 @@ class LindormVectorStoreFactory(AbstractVectorFactory): else: dataset_id = dataset.id collection_name = Dataset.gen_collection_name_by_id(dataset_id) - dataset.index_struct = json.dumps(self.gen_index_struct_dict(VectorType.LINDORM, collection_name)) + dataset.index_struct = json.dumps( + self.gen_index_struct_dict(VectorType.LINDORM, collection_name)) lindorm_config = LindormVectorStoreConfig( hosts=dify_config.LINDORM_URL, username=dify_config.LINDORM_USERNAME, diff --git a/api/core/workflow/nodes/question_classifier/question_classifier_node.py b/api/core/workflow/nodes/question_classifier/question_classifier_node.py index 744dfd3d8d..a285c40593 100644 --- a/api/core/workflow/nodes/question_classifier/question_classifier_node.py +++ b/api/core/workflow/nodes/question_classifier/question_classifier_node.py @@ -49,11 +49,13 @@ class QuestionClassifierNode(LLMNode): variable_pool = self.graph_runtime_state.variable_pool # extract variables - variable = variable_pool.get(node_data.query_variable_selector) if node_data.query_variable_selector else None + variable = variable_pool.get( + node_data.query_variable_selector) if node_data.query_variable_selector else None query = variable.value if variable else None variables = {"query": query} # fetch model config - model_instance, model_config = self._fetch_model_config(node_data.model) + model_instance, model_config = self._fetch_model_config( + node_data.model) # fetch memory memory = self._fetch_memory( node_data_memory=node_data.memory, @@ -61,7 +63,8 @@ class QuestionClassifierNode(LLMNode): ) # fetch instruction node_data.instruction = node_data.instruction or "" - node_data.instruction = variable_pool.convert_template(node_data.instruction).text + node_data.instruction = variable_pool.convert_template( + node_data.instruction).text files: Sequence[File] = ( self._fetch_files( @@ -184,12 +187,15 @@ class QuestionClassifierNode(LLMNode): variable_mapping = {"query": node_data.query_variable_selector} variable_selectors = [] if node_data.instruction: - variable_template_parser = VariableTemplateParser(template=node_data.instruction) - variable_selectors.extend(variable_template_parser.extract_variable_selectors()) + variable_template_parser = VariableTemplateParser( + template=node_data.instruction) + variable_selectors.extend( + variable_template_parser.extract_variable_selectors()) for variable_selector in variable_selectors: variable_mapping[variable_selector.variable] = variable_selector.value_selector - variable_mapping = {node_id + "." + key: value for key, value in variable_mapping.items()} + variable_mapping = {node_id + "." + key: value for key, + value in variable_mapping.items()} return variable_mapping @@ -210,7 +216,8 @@ class QuestionClassifierNode(LLMNode): context: Optional[str], ) -> int: prompt_transform = AdvancedPromptTransform(with_variable_tmpl=True) - prompt_template = self._get_prompt_template(node_data, query, None, 2000) + prompt_template = self._get_prompt_template( + node_data, query, None, 2000) prompt_messages = prompt_transform.get_prompt( prompt_template=prompt_template, inputs={}, @@ -223,13 +230,15 @@ class QuestionClassifierNode(LLMNode): ) rest_tokens = 2000 - model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE) + model_context_tokens = model_config.model_schema.model_properties.get( + ModelPropertyKey.CONTEXT_SIZE) if model_context_tokens: model_instance = ModelInstance( provider_model_bundle=model_config.provider_model_bundle, model=model_config.model ) - curr_message_tokens = model_instance.get_llm_num_tokens(prompt_messages) + curr_message_tokens = model_instance.get_llm_num_tokens( + prompt_messages) max_tokens = 0 for parameter_rule in model_config.model_schema.parameter_rules: @@ -270,7 +279,8 @@ class QuestionClassifierNode(LLMNode): prompt_messages: list[LLMNodeChatModelMessage] = [] if model_mode == ModelMode.CHAT: system_prompt_messages = LLMNodeChatModelMessage( - role=PromptMessageRole.SYSTEM, text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format(histories=memory_str) + role=PromptMessageRole.SYSTEM, text=QUESTION_CLASSIFIER_SYSTEM_PROMPT.format( + histories=memory_str) ) prompt_messages.append(system_prompt_messages) user_prompt_message_1 = LLMNodeChatModelMessage( @@ -311,4 +321,5 @@ class QuestionClassifierNode(LLMNode): ) else: - raise InvalidModelTypeError(f"Model mode {model_mode} not support.") + raise InvalidModelTypeError( + f"Model mode {model_mode} not support.") diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index 7c19de6078..af85b927a3 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -68,7 +68,8 @@ def test_executor_with_json_body_and_object_variable(): system_variables={}, user_inputs={}, ) - variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + variable_pool.add(["pre_node_id", "object"], { + "name": "John Doe", "age": 30, "email": "john@example.com"}) # Prepare the node data node_data = HttpRequestNodeData( @@ -102,7 +103,8 @@ def test_executor_with_json_body_and_object_variable(): assert executor.url == "https://api.example.com/data" assert executor.headers == {"Content-Type": "application/json"} assert executor.params == {} - assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"} + assert executor.json == {"name": "John Doe", + "age": 30, "email": "john@example.com"} assert executor.data is None assert executor.files is None assert executor.content is None @@ -123,7 +125,8 @@ def test_executor_with_json_body_and_nested_object_variable(): system_variables={}, user_inputs={}, ) - variable_pool.add(["pre_node_id", "object"], {"name": "John Doe", "age": 30, "email": "john@example.com"}) + variable_pool.add(["pre_node_id", "object"], { + "name": "John Doe", "age": 30, "email": "john@example.com"}) # Prepare the node data node_data = HttpRequestNodeData( @@ -157,7 +160,8 @@ def test_executor_with_json_body_and_nested_object_variable(): assert executor.url == "https://api.example.com/data" assert executor.headers == {"Content-Type": "application/json"} assert executor.params == {} - assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}} + assert executor.json == {"object": { + "name": "John Doe", "age": 30, "email": "john@example.com"}} assert executor.data is None assert executor.files is None assert executor.content is None diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py index 741a3a1894..9d6ab53644 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py @@ -23,7 +23,8 @@ def test_plain_text_to_dict(): assert _plain_text_to_dict("aa\n cc:") == {"aa": "", "cc": ""} assert _plain_text_to_dict("aa:bb\n cc:dd") == {"aa": "bb", "cc": "dd"} assert _plain_text_to_dict("aa:bb\n cc:dd\n") == {"aa": "bb", "cc": "dd"} - assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == {"aa": "bb", "cc": "dd"} + assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == { + "aa": "bb", "cc": "dd"} def test_http_request_node_binary_file(monkeypatch): @@ -189,7 +190,8 @@ def test_http_request_node_form_with_file(monkeypatch): def attr_checker(*args, **kwargs): assert kwargs["data"] == {"name": "test"} - assert kwargs["files"] == {"file": (None, b"test", "application/octet-stream")} + assert kwargs["files"] == { + "file": (None, b"test", "application/octet-stream")} return httpx.Response(200, content=b"") monkeypatch.setattr( diff --git a/web/.env.example b/web/.env.example index e0f6839b95..b17d637de7 100644 --- a/web/.env.example +++ b/web/.env.example @@ -29,3 +29,6 @@ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000 # CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP NEXT_PUBLIC_CSP_WHITELIST= + +# Github Access Token, used for invoking Github API +NEXT_PUBLIC_GITHUB_ACCESS_TOKEN= diff --git a/web/app/(commonLayout)/datasets/Container.tsx b/web/app/(commonLayout)/datasets/Container.tsx index 02b3cbb61d..c30cc18418 100644 --- a/web/app/(commonLayout)/datasets/Container.tsx +++ b/web/app/(commonLayout)/datasets/Container.tsx @@ -5,7 +5,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useRouter } from 'next/navigation' import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' -import useSWR from 'swr' // Components import ExternalAPIPanel from '../../components/datasets/external-api/external-api-panel' @@ -28,6 +27,7 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import { useAppContext } from '@/context/app-context' import { useExternalApiPanel } from '@/context/external-api-panel-context' +import { useQuery } from '@tanstack/react-query' const Container = () => { const { t } = useTranslation() @@ -47,7 +47,13 @@ const Container = () => { defaultTab: 'dataset', }) const containerRef = useRef(null) - const { data } = useSWR(activeTab === 'dataset' ? null : '/datasets/api-base-info', fetchDatasetApiBaseUrl) + const { data } = useQuery( + { + queryKey: ['datasetApiBaseInfo', activeTab], + queryFn: () => fetchDatasetApiBaseUrl('/datasets/api-base-info'), + enabled: activeTab !== 'dataset', + }, + ) const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index af36d4d961..ef07732997 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -8,6 +8,7 @@ import Header from '@/app/components/header' import { EventEmitterContextProvider } from '@/context/event-emitter' import { ProviderContextProvider } from '@/context/provider-context' import { ModalContextProvider } from '@/context/modal-context' +import { TanstackQueryIniter } from '@/context/query-client' const Layout = ({ children }: { children: ReactNode }) => { return ( @@ -21,7 +22,9 @@ const Layout = ({ children }: { children: ReactNode }) => {
- {children} + + {children} + diff --git a/web/app/(commonLayout)/plugins/test/update/page.tsx b/web/app/(commonLayout)/plugins/test/update/page.tsx new file mode 100644 index 0000000000..9d78b45979 --- /dev/null +++ b/web/app/(commonLayout)/plugins/test/update/page.tsx @@ -0,0 +1,67 @@ +'use client' +import { toolNeko } from '@/app/components/plugins/card/card-mock' +import { PluginSource } from '@/app/components/plugins/types' +import { useModalContext } from '@/context/modal-context' +import React from 'react' + +const UpdatePlugin = () => { + const { setShowUpdatePluginModal } = useModalContext() + const handleUpdateFromMarketPlace = () => { + setShowUpdatePluginModal({ + payload: { + type: PluginSource.marketplace, + marketPlace: { + originalPackageInfo: { + id: 'langgenius/neko:0.0.1@9e57d693739287c0efdc96847d7ed959ca93f70aa704471f2eb7ed3313821824', + payload: toolNeko as any, + }, + targetPackageInfo: { + id: 'target_xxx', + version: '1.2.3', + }, + }, + }, + onCancelCallback: () => { + console.log('canceled') + }, + onSaveCallback: () => { + console.log('saved') + }, + }) + } + const handleUpdateFromGithub = () => { + setShowUpdatePluginModal({ + payload: { + type: PluginSource.github, + github: { + originalPackageInfo: { + id: '111', + repo: 'aaa/bbb', + version: 'xxx', + url: 'aaa/bbb', + currVersion: '1.2.3', + currPackage: 'pack1', + } as any, + }, + }, + onCancelCallback: () => { + console.log('canceled') + }, + onSaveCallback: () => { + console.log('saved') + }, + }) + } + + return ( +
+
更新组件
+
+
从 Marketplace
+
从 GitHub
+
+
+ ) +} + +export default React.memo(UpdatePlugin) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index b66f331f5b..57d2e5f632 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -27,10 +27,12 @@ import { MAX_TOOLS_NUM } from '@/config' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import Tooltip from '@/app/components/base/tooltip' import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other' -import AddToolModal from '@/app/components/tools/add-tool-modal' +// import AddToolModal from '@/app/components/tools/add-tool-modal' import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import { updateBuiltInToolCredential } from '@/service/tools' import cn from '@/utils/classnames' +import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' +import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null const AgentTools: FC = () => { @@ -81,6 +83,21 @@ const AgentTools: FC = () => { const [isDeleting, setIsDeleting] = useState(-1) + const handleSelectTool = (tool: ToolDefaultValue) => { + const newModelConfig = produce(modelConfig, (draft) => { + draft.agentConfig.tools.push({ + provider_id: tool.provider_id, + provider_type: tool.provider_type as CollectionType, + provider_name: tool.provider_name, + tool_name: tool.tool_name, + tool_label: tool.tool_label, + tool_parameters: tool.params, + enabled: true, + }) + }) + setModelConfig(newModelConfig) + } + return ( <> { {tools.length < MAX_TOOLS_NUM && ( <>
- setIsShowChooseTool(true)} /> + } + isShow={isShowChooseTool} + onShowChange={setIsShowChooseTool} + disabled={false} + supportAddCustomTool + onSelect={handleSelectTool} + /> )} @@ -125,8 +149,8 @@ const AgentTools: FC = () => { {item.isDeleted && } {!item.isDeleted && (
- {typeof item.icon === 'string' &&
} - {typeof item.icon !== 'string' && } + {typeof item.icon === 'string' &&
} + {typeof item.icon !== 'string' && }
)}
{ ))}
- {isShowChooseTool && ( - setIsShowChooseTool(false)} /> - )} {isShowSettingTool && ( { const setCompletionParams = (value: FormValue) => { const params = { ...value } + // eslint-disable-next-line ts/no-use-before-define if ((!params.stop || params.stop.length === 0) && (modeModeTypeRef.current === ModelModeType.completion)) { params.stop = getTempStop() setTempStop([]) @@ -164,7 +166,7 @@ const Configuration: FC = () => { } const [modelConfig, doSetModelConfig] = useState({ - provider: 'openai', + provider: 'langgenius/openai/openai', model_id: 'gpt-3.5-turbo', mode: ModelModeType.unset, configs: { @@ -187,7 +189,7 @@ const Configuration: FC = () => { const isAgent = mode === 'agent-chat' - const isOpenAI = modelConfig.provider === 'openai' + const isOpenAI = modelConfig.provider === 'langgenius/openai/openai' const [collectionList, setCollectionList] = useState([]) useEffect(() => { @@ -356,6 +358,7 @@ const Configuration: FC = () => { const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true) const setPromptMode = async (mode: PromptMode) => { if (mode === PromptMode.advanced) { + // eslint-disable-next-line ts/no-use-before-define await migrateToDefaultPrompt() setCanReturnToSimpleMode(true) } @@ -540,8 +543,19 @@ const Configuration: FC = () => { if (modelConfig.retriever_resource) setCitationConfig(modelConfig.retriever_resource) - if (modelConfig.annotation_reply) - setAnnotationConfig(modelConfig.annotation_reply, true) + if (modelConfig.annotation_reply) { + let annotationConfig = modelConfig.annotation_reply + if (modelConfig.annotation_reply.enabled) { + annotationConfig = { + ...modelConfig.annotation_reply, + embedding_model: { + ...modelConfig.annotation_reply.embedding_model, + embedding_provider_name: correctProvider(modelConfig.annotation_reply.embedding_model.embedding_provider_name), + }, + } + } + setAnnotationConfig(annotationConfig, true) + } if (modelConfig.sensitive_word_avoidance) setModerationConfig(modelConfig.sensitive_word_avoidance) @@ -551,7 +565,7 @@ const Configuration: FC = () => { const config = { modelConfig: { - provider: model.provider, + provider: correctProvider(model.provider), model_id: model.name, mode: model.mode, configs: { @@ -605,6 +619,10 @@ const Configuration: FC = () => { ...tool, isDeleted: res.deleted_tools?.includes(tool.tool_name), notAuthor: collectionList.find(c => tool.provider_id === c.id)?.is_team_authorization === false, + ...(tool.provider_type === 'builtin' ? { + provider_id: correctProvider(tool.provider_name), + provider_name: correctProvider(tool.provider_name), + } : {}), } }), } : DEFAULT_AGENT_SETTING, @@ -622,6 +640,12 @@ const Configuration: FC = () => { retrieval_model: RETRIEVE_TYPE.multiWay, ...modelConfig.dataset_configs, ...retrievalConfig, + ...(retrievalConfig.reranking_model ? { + reranking_model: { + ...retrievalConfig.reranking_model, + reranking_provider_name: correctProvider(modelConfig.dataset_configs.reranking_model.reranking_provider_name), + }, + } : {}), }) setHasFetchedDetail(true) }) diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 4c12cab581..4e86d06b5b 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -44,6 +44,7 @@ import Tooltip from '@/app/components/base/tooltip' import { CopyIcon } from '@/app/components/base/copy-icon' import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' +import { correctProvider } from '@/utils' dayjs.extend(utc) dayjs.extend(timezone) @@ -324,7 +325,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { })?.name ?? 'custom' const modelName = (detail.model_config as any).model?.name - const provideName = (detail.model_config as any).model?.provider as any + const provideName = correctProvider((detail.model_config as any).model?.provider as any) const { currentModel, currentProvider, diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index e9e1a79e6f..c77d22dc01 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -60,7 +60,7 @@ const ModerationSettingModal: FC = ({ '/code-based-extension?module=moderation', fetchCodeBasedExtensionList, ) - const openaiProvider = modelProviders?.data.find(item => item.provider === 'openai') + const openaiProvider = modelProviders?.data.find(item => item.provider === 'langgenius/openai/openai') const systemOpenaiProviderEnabled = openaiProvider?.system_configuration.enabled const systemOpenaiProviderQuota = systemOpenaiProviderEnabled ? openaiProvider?.system_configuration.quota_configurations.find(item => item.quota_type === openaiProvider.system_configuration.current_quota_type) : undefined const systemOpenaiProviderCanUse = systemOpenaiProviderQuota?.is_valid diff --git a/web/app/components/base/markdown.tsx b/web/app/components/base/markdown.tsx index d9112507a7..48d1d2a0a5 100644 --- a/web/app/components/base/markdown.tsx +++ b/web/app/components/base/markdown.tsx @@ -1,3 +1,4 @@ +import type { Components } from 'react-markdown' import ReactMarkdown from 'react-markdown' import ReactEcharts from 'echarts-for-react' import 'katex/dist/katex.min.css' @@ -9,8 +10,7 @@ import RehypeRaw from 'rehype-raw' import SyntaxHighlighter from 'react-syntax-highlighter' import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' import type { RefObject } from 'react' -import { Component, memo, useEffect, useMemo, useRef, useState } from 'react' -import type { CodeComponent } from 'react-markdown/lib/ast-to-react' +import { Component, createContext, memo, useContext, useEffect, useMemo, useRef, useState } from 'react' import cn from '@/utils/classnames' import CopyBtn from '@/app/components/base/copy-btn' import SVGBtn from '@/app/components/base/svg' @@ -22,6 +22,7 @@ import AudioGallery from '@/app/components/base/audio-gallery' import SVGRenderer from '@/app/components/base/svg-gallery' import MarkdownButton from '@/app/components/base/markdown-blocks/button' import MarkdownForm from '@/app/components/base/markdown-blocks/form' +import type { ElementContentMap } from 'hast' // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD const capitalizationLanguageNameMap: Record = { @@ -56,7 +57,7 @@ const getCorrectCapitalizationLanguageName = (language: string) => { return language.charAt(0).toUpperCase() + language.substring(1) } -const preprocessLaTeX = (content: string) => { +const preprocessLaTeX = (content?: string) => { if (typeof content !== 'string') return content return content.replace(/\\\[(.*?)\\\]/g, (_, equation) => `$$${equation}$$`) @@ -99,6 +100,20 @@ const useLazyLoad = (ref: RefObject): boolean => { return isIntersecting } +const PreContext = createContext({ + // if children not in PreContext, just leave inline true + inline: true, +}) + +const PreBlock: Components['pre'] = (props) => { + const { ...rest } = props + return +
+  
+}
+
 // **Add code block
 // Avoid error #185 (Maximum update depth exceeded.
 // This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate.
@@ -112,7 +127,8 @@ const useLazyLoad = (ref: RefObject): boolean => {
 // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
 // or use the non-minified dev environment for full errors and additional helpful warnings.
 
-const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }) => {
+const CodeBlock: Components['code'] = memo(({ className, children, ...props }) => {
+  const { inline } = useContext(PreContext)
   const [isSVG, setIsSVG] = useState(true)
   const match = /language-(\w+)/.exec(className || '')
   const language = match?.[1]
@@ -122,7 +138,7 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
       try {
         return JSON.parse(String(children).replace(/\n$/, ''))
       }
-      catch (error) {}
+      catch {}
     }
     return JSON.parse('{"title":{"text":"ECharts error - Wrong JSON format."}}')
   }, [language, children])
@@ -192,52 +208,50 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
     
) }) -CodeBlock.displayName = 'CodeBlock' +// CodeBlock.displayName = 'CodeBlock' -const VideoBlock: CodeComponent = memo(({ node }) => { - const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) +const VideoBlock: Components['video'] = memo(({ node }) => { + const srcs = node!.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) if (srcs.length === 0) return null return }) -VideoBlock.displayName = 'VideoBlock' +// VideoBlock.displayName = 'VideoBlock' -const AudioBlock: CodeComponent = memo(({ node }) => { - const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) +const AudioBlock: Components['audio'] = memo(({ node }) => { + const srcs = node!.children.filter(child => 'properties' in child).map(child => (child as any).properties.src) if (srcs.length === 0) return null return }) -AudioBlock.displayName = 'AudioBlock' +// AudioBlock.displayName = 'AudioBlock' -const Paragraph = (paragraph: any) => { - const { node }: any = paragraph - const children_node = node.children - if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') { - return ( - <> - -

{paragraph.children.slice(1)}

- - ) - } - return

{paragraph.children}

+const Paragraph: Components['p'] = ({ node, children }) => { + const children_node = node!.children + if (children_node && children_node[0] && 'tagName' in children_node[0] && children_node[0].tagName === 'img') + return + return

{children}

} -const Img = ({ src }: any) => { - return () +const Img: Components['img'] = ({ src }) => { + return () } -const Link = ({ node, ...props }: any) => { - if (node.properties?.href && node.properties.href?.toString().startsWith('abbr')) { +const Link: Components['a'] = ({ node, ...props }) => { + if (node!.properties?.href && node!.properties.href?.toString().startsWith('abbr')) { // eslint-disable-next-line react-hooks/rules-of-hooks const { onSend } = useChatContext() - const hidden_text = decodeURIComponent(node.properties.href.toString().split('abbr:')[1]) - - return onSend?.(hidden_text)} title={node.children[0]?.value}>{node.children[0]?.value} + const hidden_text = decodeURIComponent(node!.properties.href.toString().split('abbr:')[1]) + const title = (node!.children[0] as ElementContentMap['text'])?.value + return onSend?.(hidden_text)} title={title}>{title} } else { - return {node.children[0] ? node.children[0]?.value : 'Download'} + const firstChild = node?.children?.[0] as ElementContentMap['text'] | undefined + return { + firstChild + ? firstChild.value + : 'Download' + } } } @@ -266,6 +280,7 @@ export function Markdown(props: { content: string; className?: string }) { ]} disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']} components={{ + pre: PreBlock, code: CodeBlock, img: Img, video: VideoBlock, @@ -275,7 +290,6 @@ export function Markdown(props: { content: string; className?: string }) { button: MarkdownButton, form: MarkdownForm, }} - linkTarget='_blank' > {/* Markdown detect has problem. */} {latexContent} diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx index e4154731ba..9fa518218f 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/node.tsx @@ -18,7 +18,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode { } static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode { - return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap) + return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__key) } isInline(): boolean { diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index c70cf24661..ba667955ce 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import React, { Fragment, useEffect, useState } from 'react' import { Combobox, Listbox, Transition } from '@headlessui/react' import { CheckIcon, ChevronDownIcon, ChevronUpIcon, XMarkIcon } from '@heroicons/react/20/solid' +import Badge from '../badge/index' import { useTranslation } from 'react-i18next' import classNames from '@/utils/classnames' import { @@ -289,6 +290,7 @@ type PortalSelectProps = { onSelect: (value: Item) => void items: Item[] placeholder?: string + installedValue?: string | number renderTrigger?: (value?: Item) => JSX.Element | null triggerClassName?: string triggerClassNameFn?: (open: boolean) => string @@ -302,6 +304,7 @@ const PortalSelect: FC = ({ onSelect, items, placeholder, + installedValue, renderTrigger, triggerClassName, triggerClassNameFn, @@ -313,7 +316,7 @@ const PortalSelect: FC = ({ const { t } = useTranslation() const [open, setOpen] = useState(false) const localPlaceholder = placeholder || t('common.placeholder.select') - const selectedItem = items.find(item => item.value === value) + const selectedItem = value ? items.find(item => item.value === value) : undefined return ( = ({ : (
{selectedItem?.name ?? localPlaceholder} - +
{installedValue && selectedItem && selectedItem.value !== installedValue && {installedValue} {'->'} {selectedItem.value} }
+
)} @@ -353,8 +357,8 @@ const PortalSelect: FC = ({
{ @@ -366,7 +370,10 @@ const PortalSelect: FC = ({ className='w-0 grow truncate' title={item.name} > - {item.name} + {item.name} + {item.value === installedValue && ( + INSTALLED + )} {!hideChecked && item.value === value && ( diff --git a/web/app/components/base/tab-slider/index.tsx b/web/app/components/base/tab-slider/index.tsx index 00abb9e28d..1b4e42e0d7 100644 --- a/web/app/components/base/tab-slider/index.tsx +++ b/web/app/components/base/tab-slider/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import { useEffect, useState } from 'react' import cn from '@/utils/classnames' import Badge, { BadgeState } from '@/app/components/base/badge/index' -import { usePluginPageContext } from '../../plugins/plugin-page/context' +import { useInstalledPluginList } from '@/service/use-plugins' type Option = { value: string text: string @@ -23,7 +23,7 @@ const TabSlider: FC = ({ }) => { const [activeIndex, setActiveIndex] = useState(options.findIndex(option => option.value === value)) const [sliderStyle, setSliderStyle] = useState({}) - const pluginList = usePluginPageContext(v => v.installedPluginList) + const { data: pluginList } = useInstalledPluginList() const updateSliderStyle = (index: number) => { const tabElement = document.getElementById(`tab-${index}`) @@ -40,7 +40,7 @@ const TabSlider: FC = ({ const newIndex = options.findIndex(option => option.value === value) setActiveIndex(newIndex) updateSliderStyle(newIndex) - }, [value, options]) + }, [value, options, pluginList]) return (
@@ -67,13 +67,15 @@ const TabSlider: FC = ({ }} > {option.text} + {/* if no plugin installed, the badge won't show */} {option.value === 'plugins' + && (pluginList?.plugins.length ?? 0) > 0 && - {pluginList.length} + {pluginList?.plugins.length} }
diff --git a/web/app/components/billing/upgrade-btn/index.tsx b/web/app/components/billing/upgrade-btn/index.tsx index d7885d7569..2da0c0814d 100644 --- a/web/app/components/billing/upgrade-btn/index.tsx +++ b/web/app/components/billing/upgrade-btn/index.tsx @@ -74,7 +74,7 @@ const UpgradeBtn: FC = ({ onClick={onClick} > -
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
+
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
diff --git a/web/app/components/develop/md.tsx b/web/app/components/develop/md.tsx index 26b4007c87..7cb0dd7dde 100644 --- a/web/app/components/develop/md.tsx +++ b/web/app/components/develop/md.tsx @@ -55,7 +55,7 @@ export const Heading = function H2({ export function Row({ children }: IChildrenProps) { return ( -
+
{children}
) 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 4d21ccdb59..ac9a5370ab 100644 --- a/web/app/components/header/account-dropdown/workplace-selector/index.tsx +++ b/web/app/components/header/account-dropdown/workplace-selector/index.tsx @@ -6,15 +6,12 @@ import { RiArrowDownSLine } from '@remixicon/react' import cn from '@/utils/classnames' import { switchWorkspace } from '@/service/common' import { useWorkspacesContext } from '@/context/workspace-context' -import HeaderBillingBtn from '@/app/components/billing/header-billing-btn' -import { useProviderContext } from '@/context/provider-context' import { ToastContext } from '@/app/components/base/toast' const WorkplaceSelector = () => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const { workspaces } = useWorkspacesContext() - const { enableBilling } = useProviderContext() const currentWorkspace = workspaces.find(v => v.current) const handleSwitchWorkspace = async (tenant_id: string) => { try { @@ -68,12 +65,7 @@ const WorkplaceSelector = () => { workspaces.map(workspace => (
handleSwitchWorkspace(workspace.id)}>
{workspace.name[0].toLocaleUpperCase()}
-
{workspace.name}
- {enableBilling && ( -
- -
- )} +
{workspace.name}
)) } diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 6d508f0de1..7faf3f3de7 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -123,7 +123,7 @@ const ModelProviderPage = ({ searchText }: Props) => { const [collapse, setCollapse] = useState(false) const { - plugins, + plugins = [], queryPlugins, queryPluginsWithDebounced, isLoading: isPluginsLoading, diff --git a/web/app/components/header/plugins-nav/index.tsx b/web/app/components/header/plugins-nav/index.tsx index c2ad9398f4..7cef0ce6fb 100644 --- a/web/app/components/header/plugins-nav/index.tsx +++ b/web/app/components/header/plugins-nav/index.tsx @@ -17,7 +17,7 @@ const PluginsNav = ({ -
+
diff --git a/web/app/components/plugins/base/key-value-item.tsx b/web/app/components/plugins/base/key-value-item.tsx index 50d3b53535..c249284bb9 100644 --- a/web/app/components/plugins/base/key-value-item.tsx +++ b/web/app/components/plugins/base/key-value-item.tsx @@ -2,7 +2,6 @@ import type { FC } from 'react' import React, { useCallback, useEffect, useState } from 'react' import copy from 'copy-to-clipboard' - import { RiClipboardLine, } from '@remixicon/react' @@ -11,16 +10,19 @@ import { ClipboardCheck } from '../../base/icons/src/vender/line/files' import Tooltip from '../../base/tooltip' import cn from '@/utils/classnames' import ActionButton from '@/app/components/base/action-button' + type Props = { label: string labelWidthClassName?: string value: string + valueMaxWidthClassName?: string } const KeyValueItem: FC = ({ label, labelWidthClassName = 'w-10', value, + valueMaxWidthClassName = 'max-w-[162px]', }) => { const { t } = useTranslation() const [isCopied, setIsCopied] = useState(false) @@ -46,7 +48,7 @@ const KeyValueItem: FC = ({
{label}
- + {value} diff --git a/web/app/components/plugins/card/card-mock.ts b/web/app/components/plugins/card/card-mock.ts index 2ecd59a12b..4217c4d33a 100644 --- a/web/app/components/plugins/card/card-mock.ts +++ b/web/app/components/plugins/card/card-mock.ts @@ -1,6 +1,64 @@ import type { PluginDeclaration } from '../types' import { PluginType } from '../types' +export const toolNeko: PluginDeclaration = { + version: '0.0.1', + author: 'langgenius', + name: 'neko', + description: { + en_US: 'Neko is a cute cat.', + zh_Hans: '这是一只可爱的小猫。', + pt_BR: 'Neko is a cute cat.', + ja_JP: 'Neko is a cute cat.', + }, + icon: '241e5209ecc8b5ce6b7a29a8e50388e9c75b89c3047c6ecd8e552f26de758883.svg', + label: { + en_US: 'Neko', + zh_Hans: 'Neko', + pt_BR: 'Neko', + ja_JP: 'Neko', + }, + category: 'extension' as any, + created_at: '2024-07-12T08:03:44.658609Z', + resource: { + memory: 1048576, + permission: { + tool: { + enabled: true, + }, + model: { + enabled: true, + llm: true, + text_embedding: false, + rerank: false, + tts: false, + speech2text: false, + moderation: false, + }, + node: null, + endpoint: { + enabled: true, + }, + storage: { + enabled: true, + size: 1048576, + }, + }, + }, + plugins: { + tools: null, + models: null, + endpoints: [ + 'provider/neko.yaml', + ], + }, + tags: [], + verified: false, + tool: null, + model: null, + endpoint: null, +} + export const toolNotion = { type: PluginType.tool, org: 'Notion', diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index b9ff8ecfda..b262727506 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -10,6 +10,7 @@ import Description from './base/description' import Placeholder from './base/placeholder' import cn from '@/utils/classnames' import { useGetLanguage } from '@/context/i18n' +import { getLanguage } from '@/i18n/language' export type Props = { className?: string @@ -22,6 +23,7 @@ export type Props = { footer?: React.ReactNode isLoading?: boolean loadingFileName?: string + locale?: string } const Card = ({ @@ -35,8 +37,10 @@ const Card = ({ footer, isLoading = false, loadingFileName, + locale: localeFromProps, }: Props) => { - const locale = useGetLanguage() + const defaultLocale = useGetLanguage() + const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale const { type, name, org, label, brief, icon, verified } = payload diff --git a/web/app/components/plugins/hooks.ts b/web/app/components/plugins/hooks.ts new file mode 100644 index 0000000000..484a7fbb5a --- /dev/null +++ b/web/app/components/plugins/hooks.ts @@ -0,0 +1,89 @@ +import { useTranslation } from 'react-i18next' +import type { TFunction } from 'i18next' + +type Tag = { + name: string + label: string +} + +export const useTags = (translateFromOut?: TFunction) => { + const { t: translation } = useTranslation() + const t = translateFromOut || translation + + const tags = [ + { + name: 'search', + label: t('pluginTags.tags.search'), + }, + { + name: 'image', + label: t('pluginTags.tags.image'), + }, + { + name: 'videos', + label: t('pluginTags.tags.videos'), + }, + { + name: 'weather', + label: t('pluginTags.tags.weather'), + }, + { + name: 'finance', + label: t('pluginTags.tags.finance'), + }, + { + name: 'design', + label: t('pluginTags.tags.design'), + }, + { + name: 'travel', + label: t('pluginTags.tags.travel'), + }, + { + name: 'social', + label: t('pluginTags.tags.social'), + }, + { + name: 'news', + label: t('pluginTags.tags.news'), + }, + { + name: 'medical', + label: t('pluginTags.tags.medical'), + }, + { + name: 'productivity', + label: t('pluginTags.tags.productivity'), + }, + { + name: 'education', + label: t('pluginTags.tags.education'), + }, + { + name: 'business', + label: t('pluginTags.tags.business'), + }, + { + name: 'entertainment', + label: t('pluginTags.tags.entertainment'), + }, + { + name: 'utilities', + label: t('pluginTags.tags.utilities'), + }, + { + name: 'other', + label: t('pluginTags.tags.other'), + }, + ] + + const tagsMap = tags.reduce((acc, tag) => { + acc[tag.name] = tag + return acc + }, {} as Record) + + return { + tags, + tagsMap, + } +} diff --git a/web/app/components/plugins/install-plugin/base/installed.tsx b/web/app/components/plugins/install-plugin/base/installed.tsx index 8322c3e5eb..442a61e372 100644 --- a/web/app/components/plugins/install-plugin/base/installed.tsx +++ b/web/app/components/plugins/install-plugin/base/installed.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import type { PluginDeclaration, PluginManifestInMarket } from '../../types' +import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types' import Card from '../../card' import Button from '@/app/components/base/button' import { pluginManifestInMarketToPluginProps, pluginManifestToCardPluginProps } from '../utils' @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import Badge, { BadgeState } from '@/app/components/base/badge/index' type Props = { - payload?: PluginDeclaration | PluginManifestInMarket | null + payload?: Plugin | PluginDeclaration | PluginManifestInMarket | null isMarketPayload?: boolean isFailed: boolean errMsg?: string | null diff --git a/web/app/components/plugins/install-plugin/hooks.ts b/web/app/components/plugins/install-plugin/hooks.ts new file mode 100644 index 0000000000..8ad26985cd --- /dev/null +++ b/web/app/components/plugins/install-plugin/hooks.ts @@ -0,0 +1,69 @@ +import Toast from '@/app/components/base/toast' +import { uploadGitHub } from '@/service/plugins' +import { Octokit } from '@octokit/core' +import { GITHUB_ACCESS_TOKEN } from '@/config' + +export const useGitHubReleases = () => { + const fetchReleases = async (owner: string, repo: string) => { + try { + const octokit = new Octokit({ + auth: GITHUB_ACCESS_TOKEN, + }) + const res = await octokit.request('GET /repos/{owner}/{repo}/releases', { + owner, + repo, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }) + if (res.status !== 200) throw new Error('Failed to fetch releases') + + const formattedReleases = res.data.map((release: any) => ({ + tag_name: release.tag_name, + assets: release.assets.map((asset: any) => ({ + browser_download_url: asset.browser_download_url, + name: asset.name, + })), + })) + + return formattedReleases + } + catch (error) { + Toast.notify({ + type: 'error', + message: 'Failed to fetch repository releases', + }) + return [] + } + } + + return { fetchReleases } +} + +export const useGitHubUpload = () => { + const handleUpload = async ( + repoUrl: string, + selectedVersion: string, + selectedPackage: string, + onSuccess?: (GitHubPackage: { manifest: any; unique_identifier: string }) => void, + ) => { + try { + const response = await uploadGitHub(repoUrl, selectedVersion, selectedPackage) + const GitHubPackage = { + manifest: response.manifest, + unique_identifier: response.unique_identifier, + } + if (onSuccess) onSuccess(GitHubPackage) + return GitHubPackage + } + catch (error) { + Toast.notify({ + type: 'error', + message: 'Error uploading package', + }) + throw error + } + } + + return { handleUpload } +} diff --git a/web/app/components/plugins/install-plugin/install-from-github/index.tsx b/web/app/components/plugins/install-plugin/install-from-github/index.tsx index 06c4c0797a..c74071e808 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/index.tsx @@ -1,30 +1,45 @@ 'use client' -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import Modal from '@/app/components/base/modal' import type { Item } from '@/app/components/base/select' -import type { GitHubUrlInfo, InstallState } from '@/app/components/plugins/types' +import type { InstallState } from '@/app/components/plugins/types' +import { useGitHubReleases } from '../hooks' +import { convertRepoToUrl, parseGitHubUrl } from '../utils' +import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types' import { InstallStepFromGitHub } from '../../types' import Toast from '@/app/components/base/toast' import SetURL from './steps/setURL' -import SetVersion from './steps/setVersion' -import SetPackage from './steps/setPackage' -import Installed from './steps/installed' +import SelectPackage from './steps/selectPackage' +import Installed from '../base/installed' +import Loaded from './steps/loaded' +import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import { useTranslation } from 'react-i18next' +const i18nPrefix = 'plugin.installFromGitHub' + type InstallFromGitHubProps = { + updatePayload?: UpdateFromGitHubPayload onClose: () => void + onSuccess: () => void } -const InstallFromGitHub: React.FC = ({ onClose }) => { +const InstallFromGitHub: React.FC = ({ updatePayload, onClose, onSuccess }) => { const { t } = useTranslation() + const { getIconUrl } = useGetIcon() + const { fetchReleases } = useGitHubReleases() const [state, setState] = useState({ - step: InstallStepFromGitHub.setUrl, - repoUrl: '', + step: updatePayload ? InstallStepFromGitHub.selectPackage : InstallStepFromGitHub.setUrl, + repoUrl: updatePayload?.originalPackageInfo?.repo + ? convertRepoToUrl(updatePayload.originalPackageInfo.repo) + : '', selectedVersion: '', selectedPackage: '', - releases: [], + releases: updatePayload ? updatePayload.originalPackageInfo.releases : [], }) + const [uniqueIdentifier, setUniqueIdentifier] = useState(null) + const [manifest, setManifest] = useState(null) + const [errorMsg, setErrorMsg] = useState(null) const versions: Item[] = state.releases.map(release => ({ value: release.tag_name, @@ -36,152 +51,151 @@ const InstallFromGitHub: React.FC = ({ onClose }) => { .find(release => release.tag_name === state.selectedVersion) ?.assets .map(asset => ({ - value: asset.browser_download_url, + value: asset.name, name: asset.name, })) || []) : [] - const parseGitHubUrl = (url: string): GitHubUrlInfo => { - const githubUrlRegex = /^https:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/ - const match = url.match(githubUrlRegex) + const getTitle = useCallback(() => { + if (state.step === InstallStepFromGitHub.installed) + return t(`${i18nPrefix}.installedSuccessfully`) + if (state.step === InstallStepFromGitHub.installFailed) + return t(`${i18nPrefix}.installFailed`) - if (match) { - return { - isValid: true, - owner: match[1], - repo: match[2], - } + return updatePayload ? t(`${i18nPrefix}.updatePlugin`) : t(`${i18nPrefix}.installPlugin`) + }, [state.step]) + + const handleUrlSubmit = async () => { + const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl) + if (!isValid || !owner || !repo) { + Toast.notify({ + type: 'error', + message: t('plugin.error.inValidGitHubUrl'), + }) + return } - - return { isValid: false } + await fetchReleases(owner, repo).then((fetchedReleases) => { + setState(prevState => ({ + ...prevState, + releases: fetchedReleases, + step: InstallStepFromGitHub.selectPackage, + })) + }) } - const handleInstall = async () => { - // try { - // const response = await installPackageFromGitHub({ repo: state.repoUrl, version: state.selectedVersion, package: state.selectedPackage }) - // if (response.plugin_unique_identifier) { - // setState(prevState => ({...prevState, step: InstallStep.installed})) - // console.log('Package installed:') - // } - // else { - // console.error('Failed to install package:') - // } - // } - // catch (error) { - // console.error('Error installing package:') - // } + const handleError = (e: any, isInstall: boolean) => { + const message = e?.response?.message || t('plugin.installModal.installFailedDesc') + setErrorMsg(message) + setState(prevState => ({ ...prevState, step: isInstall ? InstallStepFromGitHub.installFailed : InstallStepFromGitHub.uploadFailed })) + } + + const handleUploaded = async (GitHubPackage: any) => { + try { + const icon = await getIconUrl(GitHubPackage.manifest.icon) + setManifest({ + ...GitHubPackage.manifest, + icon, + }) + setUniqueIdentifier(GitHubPackage.uniqueIdentifier) + setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.readyToInstall })) + } + catch (e) { + handleError(e, false) + } + } + + const handleUploadFail = useCallback((errorMsg: string) => { + setErrorMsg(errorMsg) + setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed })) + }, []) + + const handleInstalled = useCallback(() => { setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed })) - } + onSuccess() + }, [onSuccess]) - const handleNext = async () => { - switch (state.step) { - case InstallStepFromGitHub.setUrl: { - const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl) - if (!isValid || !owner || !repo) { - Toast.notify({ - type: 'error', - message: t('plugin.error.inValidGitHubUrl'), - }) - break - } - try { - const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`) - if (!res.ok) - throw new Error('Failed to fetch releases') - const data = await res.json() - const formattedReleases = data.map((release: any) => ({ - tag_name: release.tag_name, - assets: release.assets.map((asset: any) => ({ - browser_download_url: asset.browser_download_url, - id: asset.id, - name: asset.name, - })), - })) - setState(prevState => ({ ...prevState, releases: formattedReleases, step: InstallStepFromGitHub.setVersion })) - } - catch (error) { - Toast.notify({ - type: 'error', - message: 'Failed to fetch repository release', - }) - } - break - } - case InstallStepFromGitHub.setVersion: - setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.setPackage })) - break - case InstallStepFromGitHub.setPackage: - handleInstall() - break - } - } + const handleFailed = useCallback((errorMsg?: string) => { + setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installFailed })) + if (errorMsg) + setErrorMsg(errorMsg) + }, []) const handleBack = () => { setState((prevState) => { switch (prevState.step) { - case InstallStepFromGitHub.setVersion: + case InstallStepFromGitHub.selectPackage: return { ...prevState, step: InstallStepFromGitHub.setUrl } - case InstallStepFromGitHub.setPackage: - return { ...prevState, step: InstallStepFromGitHub.setVersion } + case InstallStepFromGitHub.readyToInstall: + return { ...prevState, step: InstallStepFromGitHub.selectPackage } default: return prevState } }) } + return (
- {t('plugin.installFromGitHub.installPlugin')} + {getTitle()}
- {state.step !== InstallStepFromGitHub.installed && t('plugin.installFromGitHub.installNote')} + {!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
-
- {state.step === InstallStepFromGitHub.setUrl && ( - setState(prevState => ({ ...prevState, repoUrl: value }))} - onNext={handleNext} - onCancel={onClose} - /> - )} - {state.step === InstallStepFromGitHub.setVersion && ( - setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))} - onNext={handleNext} - onBack={handleBack} - /> - )} - {state.step === InstallStepFromGitHub.setPackage && ( - setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))} - onInstall={handleInstall} - onBack={handleBack} - /> - )} - {state.step === InstallStepFromGitHub.installed && ( - - )} -
+ {([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) + ? + :
+ {state.step === InstallStepFromGitHub.setUrl && ( + setState(prevState => ({ ...prevState, repoUrl: value }))} + onNext={handleUrlSubmit} + onCancel={onClose} + /> + )} + {state.step === InstallStepFromGitHub.selectPackage && ( + setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))} + selectedPackage={state.selectedPackage} + packages={packages} + onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))} + onUploaded={handleUploaded} + onFailed={handleUploadFail} + onBack={handleBack} + /> + )} + {state.step === InstallStepFromGitHub.readyToInstall && ( + + )} +
}
) } diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/installed.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/installed.tsx deleted file mode 100644 index 97be133451..0000000000 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/installed.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react' -import Button from '@/app/components/base/button' -import { useTranslation } from 'react-i18next' - -type InstalledProps = { - repoUrl: string - selectedVersion: string - selectedPackage: string - onClose: () => void -} - -const InfoRow = ({ label, value }: { label: string; value: string }) => ( -
-
-
- {label} -
-
-
-
- {value} -
-
-
-) - -const Installed: React.FC = ({ repoUrl, selectedVersion, selectedPackage, onClose }) => { - const { t } = useTranslation() - return ( - <> -
The plugin has been installed successfully.
-
- {[ - { label: t('plugin.installModal.labels.repository'), value: repoUrl }, - { label: t('plugin.installModal.labels.version'), value: selectedVersion }, - { label: t('plugin.installModal.labels.package'), value: selectedPackage }, - ].map(({ label, value }) => ( - - ))} -
-
- -
- - ) -} - -export default Installed diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx new file mode 100644 index 0000000000..dce2e735c8 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/loaded.tsx @@ -0,0 +1,119 @@ +'use client' + +import React from 'react' +import Button from '@/app/components/base/button' +import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types' +import Card from '../../../card' +import Badge, { BadgeState } from '@/app/components/base/badge/index' +import { pluginManifestToCardPluginProps } from '../../utils' +import { useTranslation } from 'react-i18next' +import { installPackageFromGitHub, uninstallPlugin } from '@/service/plugins' +import { RiLoader2Line } from '@remixicon/react' +import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store' +import checkTaskStatus from '../../base/check-task-status' +import { parseGitHubUrl } from '../../utils' + +type LoadedProps = { + updatePayload: UpdateFromGitHubPayload + uniqueIdentifier: string + payload: PluginDeclaration + repoUrl: string + selectedVersion: string + selectedPackage: string + onBack: () => void + onInstalled: () => void + onFailed: (message?: string) => void +} + +const i18nPrefix = 'plugin.installModal' + +const Loaded: React.FC = ({ + updatePayload, + uniqueIdentifier, + payload, + repoUrl, + selectedVersion, + selectedPackage, + onBack, + onInstalled, + onFailed, +}) => { + const { t } = useTranslation() + const [isInstalling, setIsInstalling] = React.useState(false) + const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) + const { check } = checkTaskStatus() + + const handleInstall = async () => { + if (isInstalling) return + setIsInstalling(true) + + try { + const { owner, repo } = parseGitHubUrl(repoUrl) + const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub( + `${owner}/${repo}`, + selectedVersion, + selectedPackage, + uniqueIdentifier, + ) + + if (updatePayload && isInstalled) + await uninstallPlugin(updatePayload.originalPackageInfo.id) + + if (isInstalled) { + onInstalled() + return + } + + setPluginTasksWithPolling() + await check({ + taskId, + pluginUniqueIdentifier: uniqueIdentifier, + }) + + onInstalled() + } + catch (e) { + if (typeof e === 'string') { + onFailed(e) + return + } + onFailed() + } + finally { + setIsInstalling(false) + } + } + + return ( + <> +
+

{t(`${i18nPrefix}.readyToInstall`)}

+
+
+ {payload.version}} + /> +
+
+ {!isInstalling && ( + + )} + +
+ + ) +} + +export default Loaded diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx new file mode 100644 index 0000000000..9b83c39867 --- /dev/null +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/selectPackage.tsx @@ -0,0 +1,126 @@ +'use client' + +import React from 'react' +import type { Item } from '@/app/components/base/select' +import { PortalSelect } from '@/app/components/base/select' +import Button from '@/app/components/base/button' +import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../../types' +import { useTranslation } from 'react-i18next' +import { useGitHubUpload } from '../../hooks' + +const i18nPrefix = 'plugin.installFromGitHub' + +type SelectPackageProps = { + updatePayload: UpdateFromGitHubPayload + repoUrl: string + selectedVersion: string + versions: Item[] + onSelectVersion: (item: Item) => void + selectedPackage: string + packages: Item[] + onSelectPackage: (item: Item) => void + onUploaded: (result: { + uniqueIdentifier: string + manifest: PluginDeclaration + }) => void + onFailed: (errorMsg: string) => void + onBack: () => void +} + +const SelectPackage: React.FC = ({ + updatePayload, + repoUrl, + selectedVersion, + versions, + onSelectVersion, + selectedPackage, + packages, + onSelectPackage, + onUploaded, + onFailed, + onBack, +}) => { + const { t } = useTranslation() + const isEdit = Boolean(updatePayload) + const [isUploading, setIsUploading] = React.useState(false) + const { handleUpload } = useGitHubUpload() + + const handleUploadPackage = async () => { + if (isUploading) return + setIsUploading(true) + + try { + const repo = repoUrl.replace('https://github.com/', '') + await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => { + onUploaded({ + uniqueIdentifier: GitHubPackage.unique_identifier, + manifest: GitHubPackage.manifest, + }) + }) + } + catch (e: any) { + if (e.response?.message) + onFailed(e.response?.message) + else + onFailed(t(`${i18nPrefix}.uploadFailed`)) + } + finally { + setIsUploading(false) + } + } + + return ( + <> + + + + +
+ {!isEdit + && + } + +
+ + ) +} + +export default SelectPackage diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/setPackage.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/setPackage.tsx deleted file mode 100644 index 2db55aa56f..0000000000 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/setPackage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import type { Item } from '@/app/components/base/select' -import { PortalSelect } from '@/app/components/base/select' -import Button from '@/app/components/base/button' -import { useTranslation } from 'react-i18next' - -type SetPackageProps = { - selectedPackage: string - packages: Item[] - onSelect: (item: Item) => void - onInstall: () => void - onBack: () => void -} - -const SetPackage: React.FC = ({ selectedPackage, packages, onSelect, onInstall, onBack }) => { - const { t } = useTranslation() - return ( - <> - - -
- - -
- - ) -} - -export default SetPackage diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx index 9ec6cd6eee..c6ce006f37 100644 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx +++ b/web/app/components/plugins/install-plugin/install-from-github/steps/setURL.tsx @@ -1,3 +1,5 @@ +'use client' + import React from 'react' import Button from '@/app/components/base/button' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/install-plugin/install-from-github/steps/setVersion.tsx b/web/app/components/plugins/install-plugin/install-from-github/steps/setVersion.tsx deleted file mode 100644 index 042ab2b093..0000000000 --- a/web/app/components/plugins/install-plugin/install-from-github/steps/setVersion.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import type { Item } from '@/app/components/base/select' -import { PortalSelect } from '@/app/components/base/select' -import Button from '@/app/components/base/button' -import { useTranslation } from 'react-i18next' - -type SetVersionProps = { - selectedVersion: string - versions: Item[] - onSelect: (item: Item) => void - onNext: () => void - onBack: () => void -} - -const SetVersion: React.FC = ({ selectedVersion, versions, onSelect, onNext, onBack }) => { - const { t } = useTranslation() - return ( - <> - - -
- - -
- - ) -} - -export default SetVersion diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx index 5ecf9d27f3..86f31c36f2 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/index.tsx @@ -9,7 +9,7 @@ import Install from './steps/install' import Installed from '../base/installed' import { useTranslation } from 'react-i18next' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' -import { usePluginPageContext } from '../../plugin-page/context' +import { useInvalidateInstalledPluginList } from '@/service/use-plugins' const i18nPrefix = 'plugin.installModal' @@ -29,7 +29,7 @@ const InstallFromLocalPackage: React.FC = ({ const [uniqueIdentifier, setUniqueIdentifier] = useState(null) const [manifest, setManifest] = useState(null) const [errorMsg, setErrorMsg] = useState(null) - const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList) + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const getTitle = useCallback(() => { if (step === InstallStep.uploadFailed) @@ -40,7 +40,7 @@ const InstallFromLocalPackage: React.FC = ({ return t(`${i18nPrefix}.installFailed`) return t(`${i18nPrefix}.installPlugin`) - }, [step]) + }, [step, t]) const { getIconUrl } = useGetIcon() @@ -59,7 +59,7 @@ const InstallFromLocalPackage: React.FC = ({ icon, }) setStep(InstallStep.readyToInstall) - }, []) + }, [getIconUrl]) const handleUploadFail = useCallback((errorMsg: string) => { setErrorMsg(errorMsg) @@ -67,9 +67,9 @@ const InstallFromLocalPackage: React.FC = ({ }, []) const handleInstalled = useCallback(() => { - mutateInstalledPluginList() + invalidateInstalledPluginList() setStep(InstallStep.installed) - }, [mutateInstalledPluginList]) + }, [invalidateInstalledPluginList]) const handleFailed = useCallback((errorMsg?: string) => { setStep(InstallStep.installFailed) diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index 9e61d9eb95..da5357d87d 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -10,6 +10,7 @@ import { RiLoader2Line } from '@remixicon/react' import Badge, { BadgeState } from '@/app/components/base/badge/index' import { installPackageFromLocal } from '@/service/plugins' import checkTaskStatus from '../../base/check-task-status' +import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store' const i18nPrefix = 'plugin.installModal' @@ -17,6 +18,7 @@ type Props = { uniqueIdentifier: string payload: PluginDeclaration onCancel: () => void + onStartToInstall?: () => void onInstalled: () => void onFailed: (message?: string) => void } @@ -25,6 +27,7 @@ const Installed: FC = ({ uniqueIdentifier, payload, onCancel, + onStartToInstall, onInstalled, onFailed, }) => { @@ -40,9 +43,12 @@ const Installed: FC = ({ onCancel() } + const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) const handleInstall = async () => { if (isInstalling) return setIsInstalling(true) + onStartToInstall?.() + try { const { all_installed: isInstalled, @@ -52,6 +58,7 @@ const Installed: FC = ({ onInstalled() return } + setPluginTasksWithPolling() await check({ taskId, pluginUniqueIdentifier: uniqueIdentifier, diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx index d9fa05ff21..793977519d 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/uploading.tsx @@ -30,7 +30,7 @@ const Uploading: FC = ({ const handleUpload = async () => { try { const res = await uploadPackageFile(file) - // onUploaded(res) + onUploaded(res) } catch (e: any) { if (e.response?.message) { diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx index 17eca59dc3..b721a84454 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/index.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react' import Modal from '@/app/components/base/modal' -import type { PluginManifestInMarket } from '../../types' +import type { Plugin, PluginManifestInMarket } from '../../types' import { InstallStep } from '../../types' import Install from './steps/install' import Installed from '../base/installed' @@ -12,7 +12,7 @@ const i18nPrefix = 'plugin.installModal' type InstallFromMarketplaceProps = { uniqueIdentifier: string - manifest: PluginManifestInMarket + manifest: PluginManifestInMarket | Plugin onSuccess: () => void onClose: () => void } @@ -36,7 +36,7 @@ const InstallFromMarketplace: React.FC = ({ if (step === InstallStep.installFailed) return t(`${i18nPrefix}.installFailed`) return t(`${i18nPrefix}.installPlugin`) - }, [step]) + }, [step, t]) const handleInstalled = useCallback(() => { setStep(InstallStep.installed) diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index 8a6c5d3c0e..bc32e642a5 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import React, { useMemo } from 'react' import { RiInformation2Line } from '@remixicon/react' -import type { PluginManifestInMarket } from '../../../types' +import type { Plugin, PluginManifestInMarket } from '../../../types' import Card from '../../../card' import { pluginManifestInMarketToPluginProps } from '../../utils' import Button from '@/app/components/base/button' @@ -16,8 +16,9 @@ const i18nPrefix = 'plugin.installModal' type Props = { uniqueIdentifier: string - payload: PluginManifestInMarket + payload: PluginManifestInMarket | Plugin onCancel: () => void + onStartToInstall?: () => void onInstalled: () => void onFailed: (message?: string) => void } @@ -26,6 +27,7 @@ const Installed: FC = ({ uniqueIdentifier, payload, onCancel, + onStartToInstall, onInstalled, onFailed, }) => { @@ -43,6 +45,7 @@ const Installed: FC = ({ const handleInstall = async () => { if (isInstalling) return + onStartToInstall?.() setIsInstalling(true) try { @@ -90,7 +93,7 @@ const Installed: FC = ({ ) }) - }, [payload]) + }, [payload.latest_version, supportCheckInstalled]) return ( <> @@ -101,7 +104,7 @@ const Installed: FC = ({
diff --git a/web/app/components/plugins/install-plugin/utils.ts b/web/app/components/plugins/install-plugin/utils.ts index 8b3e850deb..dc3113e6a5 100644 --- a/web/app/components/plugins/install-plugin/utils.ts +++ b/web/app/components/plugins/install-plugin/utils.ts @@ -1,12 +1,15 @@ import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' +import type { GitHubUrlInfo } from '@/app/components/plugins/types' export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => { return { + plugin_id: pluginManifest.plugin_unique_identifier, type: pluginManifest.category, category: pluginManifest.category, name: pluginManifest.name, version: pluginManifest.version, latest_version: '', + latest_package_identifier: '', org: pluginManifest.author, label: pluginManifest.label, brief: pluginManifest.description, @@ -18,16 +21,19 @@ export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaratio endpoint: { settings: [], }, + tags: [], } } export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManifestInMarket): Plugin => { return { + plugin_id: pluginManifest.plugin_unique_identifier, type: pluginManifest.category, category: pluginManifest.category, name: pluginManifest.name, version: pluginManifest.latest_version, latest_version: pluginManifest.latest_version, + latest_package_identifier: '', org: pluginManifest.org, label: pluginManifest.label, brief: pluginManifest.brief, @@ -39,5 +45,15 @@ export const pluginManifestInMarketToPluginProps = (pluginManifest: PluginManife endpoint: { settings: [], }, + tags: [], } } + +export const parseGitHubUrl = (url: string): GitHubUrlInfo => { + const match = url.match(/^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/?$/) + return match ? { isValid: true, owner: match[1], repo: match[2] } : { isValid: false } +} + +export const convertRepoToUrl = (repo: string) => { + return repo ? `https://github.com/${repo}` : '' +} diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 692b3eb5a5..0c87e32919 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -100,6 +100,15 @@ export const MarketplaceContextProvider = ({ setSearchPluginText(text) searchPluginTextRef.current = text + if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { + queryMarketplaceCollectionsAndPlugins({ + category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, + }) + setPlugins(undefined) + + return + } + queryPluginsWithDebounced({ query: text, category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, @@ -107,12 +116,21 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, }) - }, [queryPluginsWithDebounced]) + }, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, setPlugins]) const handleFilterPluginTagsChange = useCallback((tags: string[]) => { setFilterPluginTags(tags) filterPluginTagsRef.current = tags + if (!searchPluginTextRef.current && !filterPluginTagsRef.current.length) { + queryMarketplaceCollectionsAndPlugins({ + category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, + }) + setPlugins(undefined) + + return + } + queryPlugins({ query: searchPluginTextRef.current, category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, @@ -120,7 +138,7 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, }) - }, [queryPlugins]) + }, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins]) const handleActivePluginTypeChange = useCallback((type: string) => { setActivePluginType(type) @@ -130,7 +148,7 @@ export const MarketplaceContextProvider = ({ queryMarketplaceCollectionsAndPlugins({ category: type === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : type, }) - setPlugins([]) + setPlugins(undefined) return } diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 754a4b12a9..3b0454c3c6 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -1,25 +1,38 @@ -const Description = () => { +import { + getLocaleOnServer, + useTranslation as translate, +} from '@/i18n/server' + +type DescriptionProps = { + locale?: string +} +const Description = async ({ + locale: localeFromProps, +}: DescriptionProps) => { + const localeDefault = getLocaleOnServer() + const { t } = await translate(localeFromProps || localeDefault, 'plugin') + return ( <> -

+

Empower your AI development

-

+

Discover - models + {t('category.models')} , - tools + {t('category.tools')} , - extensions + {t('category.extensions')} and - bundles + {t('category.bundles')} in Dify Marketplace

diff --git a/web/app/components/plugins/marketplace/empty/index.tsx b/web/app/components/plugins/marketplace/empty/index.tsx index a6e71c9eee..cc3957d3ff 100644 --- a/web/app/components/plugins/marketplace/empty/index.tsx +++ b/web/app/components/plugins/marketplace/empty/index.tsx @@ -4,7 +4,7 @@ import Line from './line' const Empty = () => { return (
{ Array.from({ length: 16 }).map((_, index) => ( diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index 89a0908ee6..47ad603276 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -2,6 +2,7 @@ import { useCallback, useState, } from 'react' +import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import type { Plugin } from '../types' import type { @@ -13,6 +14,7 @@ import { getMarketplaceCollectionsAndPlugins, getMarketplacePlugins, } from './utils' +import i18n from '@/i18n/i18next-config' export const useMarketplaceCollectionsAndPlugins = () => { const [isLoading, setIsLoading] = useState(false) @@ -40,7 +42,7 @@ export const useMarketplaceCollectionsAndPlugins = () => { export const useMarketplacePlugins = () => { const [isLoading, setIsLoading] = useState(false) - const [plugins, setPlugins] = useState([]) + const [plugins, setPlugins] = useState() const queryPlugins = useCallback(async (query: PluginsSearchParams) => { setIsLoading(true) @@ -63,3 +65,14 @@ export const useMarketplacePlugins = () => { setIsLoading, } } + +export const useMixedTranslation = (localeFromOuter?: string) => { + let t = useTranslation().t + + if (localeFromOuter) + t = i18n.getFixedT(localeFromOuter) + + return { + t, + } +} diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 0c87cce924..742df86ea0 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -7,20 +7,23 @@ import ListWrapper from './list/list-wrapper' import { getMarketplaceCollectionsAndPlugins } from './utils' type MarketplaceProps = { + locale?: string showInstallButton?: boolean } const Marketplace = async ({ + locale, showInstallButton = true, }: MarketplaceProps) => { const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins() return ( - + - - + + , ) => { - const containerRef = usePluginPageContext(v => v.containerRef) const intersected = useMarketplaceContext(v => v.intersected) const setIntersected = useMarketplaceContext(v => v.setIntersected) useEffect(() => { + const container = document.getElementById('marketplace-container') let observer: IntersectionObserver | undefined - if (containerRef?.current && anchorRef.current) { + if (container && anchorRef.current) { observer = new IntersectionObserver((entries) => { const isIntersecting = entries[0].isIntersecting @@ -21,10 +20,10 @@ export const useScrollIntersection = ( if (!isIntersecting && intersected) setIntersected(false) }, { - root: containerRef.current, + root: container, }) observer.observe(anchorRef.current) } return () => observer?.disconnect() - }, [containerRef, anchorRef, intersected, setIntersected]) + }, [anchorRef, intersected, setIntersected]) } diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 4ef5dccb65..3465e095c4 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -1,26 +1,42 @@ 'use client' import { RiArrowRightUpLine } from '@remixicon/react' -import { useTranslation } from 'react-i18next' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import type { Plugin } from '@/app/components/plugins/types' import { MARKETPLACE_URL_PREFIX } from '@/config' import Button from '@/app/components/base/button' +import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' +import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' +import { useBoolean } from 'ahooks' type CardWrapperProps = { plugin: Plugin showInstallButton?: boolean + locale?: string } const CardWrapper = ({ plugin, showInstallButton, + locale, }: CardWrapperProps) => { - const { t } = useTranslation() + const { t } = useMixedTranslation(locale) + const [isShowInstallFromMarketplace, { + setTrue: showInstallFromMarketplace, + setFalse: hideInstallFromMarketplace, + }] = useBoolean(false) + return ( -
+
{ + if (!showInstallButton) + window.open(`${MARKETPLACE_URL_PREFIX}/plugin/${plugin.org}/${plugin.name}`) + }} + > {t('plugin.detailPanel.operation.install')} @@ -48,6 +65,16 @@ const CardWrapper = ({
) } + { + isShowInstallFromMarketplace && ( + + ) + }
) } diff --git a/web/app/components/plugins/marketplace/list/index.tsx b/web/app/components/plugins/marketplace/list/index.tsx index 7d1ace6297..1fe1e7306b 100644 --- a/web/app/components/plugins/marketplace/list/index.tsx +++ b/web/app/components/plugins/marketplace/list/index.tsx @@ -10,12 +10,14 @@ type ListProps = { marketplaceCollectionPluginsMap: Record plugins?: Plugin[] showInstallButton?: boolean + locale?: string } const List = ({ marketplaceCollections, marketplaceCollectionPluginsMap, plugins, showInstallButton, + locale, }: ListProps) => { return ( <> @@ -25,6 +27,7 @@ const List = ({ marketplaceCollections={marketplaceCollections} marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap} showInstallButton={showInstallButton} + locale={locale} /> ) } @@ -37,6 +40,7 @@ const List = ({ key={plugin.name} plugin={plugin} showInstallButton={showInstallButton} + locale={locale} /> )) } diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index 6d56380c52..57b087d1f5 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -7,11 +7,13 @@ type ListWithCollectionProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record showInstallButton?: boolean + locale?: string } const ListWithCollection = ({ marketplaceCollections, marketplaceCollectionPluginsMap, showInstallButton, + locale, }: ListWithCollectionProps) => { return ( <> @@ -30,6 +32,7 @@ const ListWithCollection = ({ key={plugin.name} plugin={plugin} showInstallButton={showInstallButton} + locale={locale} /> )) } diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index bcb929ca2f..50f4c5d244 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -9,18 +9,20 @@ type ListWrapperProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record showInstallButton?: boolean + locale?: string } const ListWrapper = ({ marketplaceCollections, marketplaceCollectionPluginsMap, showInstallButton, + locale, }: ListWrapperProps) => { const plugins = useMarketplaceContext(v => v.plugins) const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient) const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient) return ( -
+
{ plugins && (
@@ -35,6 +37,7 @@ const ListWrapper = ({ marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap} plugins={plugins} showInstallButton={showInstallButton} + locale={locale} />
) diff --git a/web/app/components/plugins/marketplace/plugin-type-switch.tsx b/web/app/components/plugins/marketplace/plugin-type-switch.tsx index 35f5349343..6a44524a0c 100644 --- a/web/app/components/plugins/marketplace/plugin-type-switch.tsx +++ b/web/app/components/plugins/marketplace/plugin-type-switch.tsx @@ -1,5 +1,4 @@ 'use client' - import { RiArchive2Line, RiBrain2Line, @@ -8,6 +7,7 @@ import { } from '@remixicon/react' import { PluginType } from '../types' import { useMarketplaceContext } from './context' +import { useMixedTranslation } from './hooks' import cn from '@/utils/classnames' export const PLUGIN_TYPE_SEARCH_MAP = { @@ -17,40 +17,47 @@ export const PLUGIN_TYPE_SEARCH_MAP = { extension: PluginType.extension, bundle: 'bundle', } -const options = [ - { - value: PLUGIN_TYPE_SEARCH_MAP.all, - text: 'All', - icon: null, - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.model, - text: 'Models', - icon: , - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.tool, - text: 'Tools', - icon: , - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.extension, - text: 'Extensions', - icon: , - }, - { - value: PLUGIN_TYPE_SEARCH_MAP.bundle, - text: 'Bundles', - icon: , - }, -] -const PluginTypeSwitch = () => { +type PluginTypeSwitchProps = { + locale?: string +} +const PluginTypeSwitch = ({ + locale, +}: PluginTypeSwitchProps) => { + const { t } = useMixedTranslation(locale) const activePluginType = useMarketplaceContext(s => s.activePluginType) const handleActivePluginTypeChange = useMarketplaceContext(s => s.handleActivePluginTypeChange) + const options = [ + { + value: PLUGIN_TYPE_SEARCH_MAP.all, + text: 'All', + icon: null, + }, + { + value: PLUGIN_TYPE_SEARCH_MAP.model, + text: t('plugin.category.models'), + icon: , + }, + { + value: PLUGIN_TYPE_SEARCH_MAP.tool, + text: t('plugin.category.tools'), + icon: , + }, + { + value: PLUGIN_TYPE_SEARCH_MAP.extension, + text: t('plugin.category.extensions'), + icon: , + }, + { + value: PLUGIN_TYPE_SEARCH_MAP.bundle, + text: t('plugin.category.bundles'), + icon: , + }, + ] + return (
{ options.map(option => ( diff --git a/web/app/components/plugins/marketplace/search-box/index.tsx b/web/app/components/plugins/marketplace/search-box/index.tsx index 7ca9ce17e0..513f8b98ad 100644 --- a/web/app/components/plugins/marketplace/search-box/index.tsx +++ b/web/app/components/plugins/marketplace/search-box/index.tsx @@ -12,6 +12,7 @@ type SearchBoxProps = { onTagsChange: (tags: string[]) => void size?: 'small' | 'large' placeholder?: string + locale?: string } const SearchBox = ({ search, @@ -20,7 +21,8 @@ const SearchBox = ({ tags, onTagsChange, size = 'small', - placeholder = 'Search tools...', + placeholder = '', + locale, }: SearchBoxProps) => { return (
-
+
{ +type SearchBoxWrapperProps = { + locale?: string +} +const SearchBoxWrapper = ({ + locale, +}: SearchBoxWrapperProps) => { + const { t } = useMixedTranslation(locale) const intersected = useMarketplaceContext(v => v.intersected) const searchPluginText = useMarketplaceContext(v => v.searchPluginText) const handleSearchPluginTextChange = useMarketplaceContext(v => v.handleSearchPluginTextChange) @@ -13,7 +20,7 @@ const SearchBoxWrapper = () => { return ( { tags={filterPluginTags} onTagsChange={handleFilterPluginTagsChange} size='large' + locale={locale} + placeholder={t('plugin.searchPlugins')} /> ) } diff --git a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx index 5114765544..416cc99b91 100644 --- a/web/app/components/plugins/marketplace/search-box/tags-filter.tsx +++ b/web/app/components/plugins/marketplace/search-box/tags-filter.tsx @@ -14,30 +14,26 @@ import { import Checkbox from '@/app/components/base/checkbox' import cn from '@/utils/classnames' import Input from '@/app/components/base/input' +import { useTags } from '@/app/components/plugins/hooks' +import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' type TagsFilterProps = { tags: string[] onTagsChange: (tags: string[]) => void size: 'small' | 'large' + locale?: string } const TagsFilter = ({ tags, onTagsChange, size, + locale, }: TagsFilterProps) => { + const { t } = useMixedTranslation(locale) const [open, setOpen] = useState(false) const [searchText, setSearchText] = useState('') - const options = [ - { - value: 'search', - text: 'Search', - }, - { - value: 'image', - text: 'Image', - }, - ] - const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase())) + const { tags: options, tagsMap } = useTags(t) + const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchText.toLowerCase())) const handleCheck = (id: string) => { if (tags.includes(id)) onTagsChange(tags.filter((tag: string) => tag !== id)) @@ -73,10 +69,10 @@ const TagsFilter = ({ size === 'small' && 'px-0.5 py-1', )}> { - !selectedTagsLength && 'All Tags' + !selectedTagsLength && t('pluginTags.allTags') } { - !!selectedTagsLength && tags.slice(0, 2).join(',') + !!selectedTagsLength && tags.map(tag => tagsMap[tag].label).slice(0, 2).join(',') } { selectedTagsLength > 2 && ( @@ -108,23 +104,23 @@ const TagsFilter = ({ showLeftIcon value={searchText} onChange={e => setSearchText(e.target.value)} - placeholder='Search tags' + placeholder={t('pluginTags.searchTags') || ''} />
{ filteredOptions.map(option => (
handleCheck(option.value)} + onClick={() => handleCheck(option.name)} >
- {option.text} + {option.label}
)) diff --git a/web/app/components/plugins/marketplace/utils.ts b/web/app/components/plugins/marketplace/utils.ts index 99b34fa570..a8e50b5e20 100644 --- a/web/app/components/plugins/marketplace/utils.ts +++ b/web/app/components/plugins/marketplace/utils.ts @@ -14,13 +14,13 @@ export const getMarketplaceCollectionsAndPlugins = async (query?: CollectionsAnd let marketplaceCollections = [] as MarketplaceCollection[] let marketplaceCollectionPluginsMap = {} as Record try { - const marketplaceCollectionsData = await globalThis.fetch(`${MARKETPLACE_API_PREFIX}/collections`, { cache: 'no-store' }) + const marketplaceCollectionsData = await globalThis.fetch(`${MARKETPLACE_API_PREFIX}/collections?page=1&page_size=100`, { cache: 'no-store' }) const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json() marketplaceCollections = marketplaceCollectionsDataJson.data.collections await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => { - let url = `${MARKETPLACE_API_PREFIX}/collections/${collection.name}/plugins` + let url = `${MARKETPLACE_API_PREFIX}/collections/${collection.name}/plugins?page=1&page_size=100` if (query?.category) - url += `?category=${query.category}` + url += `&category=${query.category}` const marketplaceCollectionPluginsData = await globalThis.fetch(url, { cache: 'no-store' }) const marketplaceCollectionPluginsDataJson = await marketplaceCollectionPluginsData.json() const plugins = marketplaceCollectionPluginsDataJson.data.plugins.map((plugin: Plugin) => { diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 52ebe3608f..c90f1ffb0d 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,37 +1,102 @@ -import React from 'react' +import React, { useState } from 'react' +import useSWR from 'swr' import { useTranslation } from 'react-i18next' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' +import { useAppContext } from '@/context/app-context' import Button from '@/app/components/base/button' +import Toast from '@/app/components/base/toast' import Indicator from '@/app/components/header/indicator' - -const ActionCard = () => { - return ( -
-
Notion Page Search
-
A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query.
-
- ) -} +import ToolItem from '@/app/components/tools/provider/tool-item' +import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' +import { + fetchBuiltInToolList, + fetchCollectionDetail, + removeBuiltInToolCredential, + updateBuiltInToolCredential, +} from '@/service/tools' const ActionList = () => { const { t } = useTranslation() - // TODO use tool-item add api in tool providers + const { isCurrentWorkspaceManager } = useAppContext() + const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) + const { data: provider } = useSWR( + `builtin/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`, + fetchCollectionDetail, + ) + const { data } = useSWR( + `${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`, + fetchBuiltInToolList, + ) + + const [showSettingAuth, setShowSettingAuth] = useState(false) + + const handleCredentialSettingUpdate = () => {} + + if (!data || !provider) + return null + return (
- {t('plugin.detailPanel.actionNum', { num: 3 })} - + {t('plugin.detailPanel.actionNum', { num: data.length })} + {provider.is_team_authorization && provider.allow_delete && ( + + )}
- + {!provider.is_team_authorization && provider.allow_delete && ( + + )}
- - - + {data.map(tool => ( + + ))}
+ {showSettingAuth && ( + setShowSettingAuth(false)} + onSaved={async (value) => { + await updateBuiltInToolCredential(provider.name, value) + Toast.notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + handleCredentialSettingUpdate() + setShowSettingAuth(false) + }} + onRemove={async () => { + await removeBuiltInToolCredential(provider.name) + Toast.notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + handleCredentialSettingUpdate() + setShowSettingAuth(false) + }} + /> + )}
) } diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 5ec0e4fbe2..ad9760feff 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -1,6 +1,5 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { useBoolean } from 'ahooks' import { RiBugLine, @@ -23,7 +22,9 @@ import Confirm from '@/app/components/base/confirm' import Tooltip from '@/app/components/base/tooltip' import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' import { Github } from '@/app/components/base/icons/src/public/common' -import I18n from '@/context/i18n' +import { uninstallPlugin } from '@/service/plugins' +import { useGetLanguage } from '@/context/i18n' +import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import cn from '@/utils/classnames' const i18nPrefix = 'plugin.action' @@ -40,16 +41,25 @@ const DetailHeader = ({ onDelete, }: Props) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const locale = useGetLanguage() + const { + installation_id, + source, + tenant_id, + version, + latest_version, + meta, + } = detail + const { author, name, label, description, icon, verified } = detail.declaration + const isFromGitHub = source === PluginSource.github + // Only plugin installed from GitHub need to check if it's the new version const hasNewVersion = useMemo(() => { - if (!detail) - return false - return false - // return pluginDetail.latest_version !== pluginDetail.version - }, [detail]) + return source === PluginSource.github && latest_version !== version + }, [source, latest_version, version]) - const handleUpdate = () => {} + // #plugin TODO# update plugin + const handleUpdate = () => { } const [isShowPluginInfo, { setTrue: showPluginInfo, @@ -61,19 +71,37 @@ const DetailHeader = ({ setFalse: hideDeleteConfirm, }] = useBoolean(false) - const usedInApps = 3 + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleDelete = useCallback(async () => { + showDeleting() + const res = await uninstallPlugin(installation_id) + hideDeleting() + if (res.success) { + hideDeleteConfirm() + onDelete() + } + }, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onDelete]) + + // #plugin TODO# used in apps + // const usedInApps = 3 return (
- +
+ +
- - {detail.declaration.verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} + <Title title={label[locale]} /> + {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} <Badge className='mx-1' - text={detail.version} + text={version} hasRedCornerMark={hasNewVersion} /> {hasNewVersion && ( @@ -81,32 +109,31 @@ const DetailHeader = ({ )} </div> <div className='mb-1 flex justify-between items-center h-4'> - <div className='flex items-center'> + <div className='mt-0.5 flex items-center'> <OrgInfo - className="mt-0.5" packageNameClassName='w-auto' - orgName={detail.declaration.author} - packageName={detail.declaration.name} + orgName={author} + packageName={name} /> <div className='ml-1 mr-0.5 text-text-quaternary system-xs-regular'>·</div> {detail.source === PluginSource.marketplace && ( <Tooltip popupContent={t('plugin.detailPanel.categoryTip.marketplace')} > - <BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary hover:text-text-accent' /> + <div><BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary hover:text-text-accent' /></div> </Tooltip> )} {detail.source === PluginSource.github && ( <Tooltip popupContent={t('plugin.detailPanel.categoryTip.github')} > - <Github className='w-3.5 h-3.5 text-text-secondary hover:text-text-primary' /> + <div><Github className='w-3.5 h-3.5 text-text-secondary hover:text-text-primary' /></div> </Tooltip> )} {detail.source === PluginSource.local && ( <Tooltip popupContent={t('plugin.detailPanel.categoryTip.local')} > - <RiHardDrive3Line className='w-3.5 h-3.5 text-text-tertiary' /> + <div><RiHardDrive3Line className='w-3.5 h-3.5 text-text-tertiary' /></div> </Tooltip> )} {detail.source === PluginSource.debugging && ( <Tooltip popupContent={t('plugin.detailPanel.categoryTip.debugging')} > - <RiBugLine className='w-3.5 h-3.5 text-text-tertiary hover:text-text-warning' /> + <div><RiBugLine className='w-3.5 h-3.5 text-text-tertiary hover:text-text-warning' /></div> </Tooltip> )} </div> @@ -115,19 +142,21 @@ const DetailHeader = ({ <div className='flex gap-1'> <OperationDropdown onInfo={showPluginInfo} + onCheckVersion={handleUpdate} onRemove={showDeleteConfirm} + detailUrl={`${MARKETPLACE_URL_PREFIX}/plugin/${author}/${name}`} /> <ActionButton onClick={onHide}> <RiCloseLine className='w-4 h-4' /> </ActionButton> </div> </div> - <Description className='mt-3' text={detail.declaration.description[locale]} descriptionLineRows={2}></Description> + <Description className='mt-3' text={description[locale]} descriptionLineRows={2}></Description> {isShowPluginInfo && ( <PluginInfo - repository={detail.meta?.repo} - release={detail.version} - packageName={detail.meta?.package} + repository={isFromGitHub ? meta?.repo : ''} + release={version} + packageName={meta?.package || ''} onHide={hidePluginInfo} /> )} @@ -137,12 +166,14 @@ const DetailHeader = ({ title={t(`${i18nPrefix}.delete`)} content={ <div> - {t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{detail.declaration.label[locale]}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br /> - {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} + {t(`${i18nPrefix}.deleteContentLeft`)}<span className='system-md-semibold'>{label[locale]}</span>{t(`${i18nPrefix}.deleteContentRight`)}<br /> + {/* {usedInApps > 0 && t(`${i18nPrefix}.usedInApps`, { num: usedInApps })} */} </div> } onCancel={hideDeleteConfirm} - onConfirm={onDelete} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} /> )} </div> diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index de87750ad4..495d451f90 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -1,30 +1,35 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import useSWR from 'swr' import { useBoolean } from 'ahooks' import { RiAddLine } from '@remixicon/react' -import type { EndpointListItem, PluginEndpointDeclaration } from '../types' import EndpointModal from './endpoint-modal' import EndpointCard from './endpoint-card' import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ActionButton from '@/app/components/base/action-button' import Tooltip from '@/app/components/base/tooltip' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import { createEndpoint, + fetchEndpointList, } from '@/service/plugins' -type Props = { - pluginUniqueID: string - declaration: PluginEndpointDeclaration - list: EndpointListItem[] -} - -const EndpointList = ({ - pluginUniqueID, - declaration, - list, -}: Props) => { +const EndpointList = () => { const { t } = useTranslation() - + const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) + const pluginUniqueID = pluginDetail.plugin_unique_identifier + const declaration = pluginDetail.declaration.endpoint + const { data } = useSWR( + { + url: '/workspaces/current/endpoints/list/plugin', + params: { + plugin_id: pluginDetail.plugin_id, + page: 1, + page_size: 100, + }, + }, + fetchEndpointList, + ) const [isShowEndpointModal, { setTrue: showEndpointModal, setFalse: hideEndpointModal, @@ -50,6 +55,9 @@ const EndpointList = ({ } } + if (!data) + return null + return ( <div className='px-4 py-2 border-t border-divider-subtle'> <div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'> @@ -65,11 +73,11 @@ const EndpointList = ({ <RiAddLine className='w-4 h-4' /> </ActionButton> </div> - {list.length === 0 && ( + {data.endpoints.length === 0 && ( <div className='mb-1 p-3 flex justify-center rounded-[10px] bg-background-section text-text-tertiary system-xs-regular'>{t('plugin.detailPanel.endpointsEmpty')}</div> )} <div className='flex flex-col gap-2'> - {list.map((item, index) => ( + {data.endpoints.map((item, index) => ( <EndpointCard key={index} data={item} diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 76ae8d7625..0dd85a4f87 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -1,29 +1,25 @@ 'use client' import React from 'react' import type { FC } from 'react' -import { useTranslation } from 'react-i18next' -import type { EndpointListItem, PluginDetail } from '../types' import DetailHeader from './detail-header' import EndpointList from './endpoint-list' import ActionList from './action-list' import ModelList from './model-list' import Drawer from '@/app/components/base/drawer' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import cn from '@/utils/classnames' type Props = { - pluginDetail: PluginDetail | undefined - endpointList: EndpointListItem[] - onHide: () => void + onDelete: () => void } const PluginDetailPanel: FC<Props> = ({ - pluginDetail, - endpointList = [], - onHide, + onDelete, }) => { - const { t } = useTranslation() + const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) + const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail) - const handleDelete = () => {} + const handleHide = () => setCurrentPluginDetail(undefined) if (!pluginDetail) return null @@ -32,7 +28,7 @@ const PluginDetailPanel: FC<Props> = ({ <Drawer isOpen={!!pluginDetail} clickOutsideNotOpen={false} - onClose={onHide} + onClose={handleHide} footer={null} mask={false} positionCenter={false} @@ -42,17 +38,11 @@ const PluginDetailPanel: FC<Props> = ({ <> <DetailHeader detail={pluginDetail} - onHide={onHide} - onDelete={handleDelete} + onHide={handleHide} + onDelete={onDelete} /> <div className='grow overflow-y-auto'> - {!!pluginDetail.declaration.endpoint && ( - <EndpointList - pluginUniqueID={pluginDetail.plugin_unique_identifier} - list={endpointList} - declaration={pluginDetail.declaration.endpoint} - /> - )} + {!!pluginDetail.declaration.endpoint && <EndpointList />} {!!pluginDetail.declaration.tool && <ActionList />} {!!pluginDetail.declaration.model && <ModelList />} </div> diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index fcfecd9121..0d79d73020 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -1,28 +1,43 @@ import React from 'react' +import useSWR from 'swr' import { useTranslation } from 'react-i18next' -// import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' -// import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' +import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' +import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' +import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' +import { fetchModelProviderModelList } from '@/service/common' const ModelList = () => { const { t } = useTranslation() + const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) + + const { data: res } = useSWR( + `/workspaces/current/model-providers/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}/models`, + fetchModelProviderModelList, + ) + + if (!res) + return null return ( <div className='px-4 py-2'> - <div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold-uppercase'>{t('plugin.detailPanel.modelNum', { num: 3 })}</div> + <div className='mb-1 h-6 flex items-center text-text-secondary system-sm-semibold-uppercase'>{t('plugin.detailPanel.modelNum', { num: res.data.length })}</div> <div className='h-8 flex items-center'> - {/* <ModelIcon - className='shrink-0 mr-2' - provider={provider} - modelName={model.model} - /> - <ModelName - className='grow text-sm font-normal text-gray-900' - modelItem={model} - showModelType - showMode - showContextSize - > - </ModelName> */} + {res.data.map(model => ( + <div key={model.model} className='h-6 py-1 flex items-center'> + <ModelIcon + className='shrink-0 mr-2' + provider={currentPluginDetail.declaration.model} + modelName={model.model} + /> + <ModelName + className='grow text-text-secondary system-md-regular' + modelItem={model} + showModelType + showMode + showContextSize + /> + </div> + ))} </div> </div> ) diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index e8186d1958..b23b29d462 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -14,12 +14,16 @@ import cn from '@/utils/classnames' type Props = { onInfo: () => void + onCheckVersion: () => void onRemove: () => void + detailUrl: string } const OperationDropdown: FC<Props> = ({ onInfo, + onCheckVersion, onRemove, + detailUrl, }) => { const { t } = useTranslation() const [open, doSetOpen] = useState(false) @@ -44,22 +48,40 @@ const OperationDropdown: FC<Props> = ({ }} > <PortalToFollowElemTrigger onClick={handleTrigger}> - <ActionButton className={cn(open && 'bg-state-base-hover')}> - <RiMoreFill className='w-4 h-4' /> - </ActionButton> + <div> + <ActionButton className={cn(open && 'bg-state-base-hover')}> + <RiMoreFill className='w-4 h-4' /> + </ActionButton> + </div> </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-50'> <div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'> - <div onClick={onInfo} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div> - {/* ##plugin TODO## check update */} - <div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.checkUpdate')}</div> - {/* ##plugin TODO## router action */} - <div className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'> - <div className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</div> + <div + onClick={() => { + onInfo() + handleTrigger() + }} + className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' + >{t('plugin.detailPanel.operation.info')}</div> + <div + onClick={() => { + onCheckVersion() + handleTrigger() + }} + className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' + >{t('plugin.detailPanel.operation.checkUpdate')}</div> + <a href={detailUrl} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'> + <span className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</span> <RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' /> - </div> + </a> <div className='my-1 h-px bg-divider-subtle'></div> - <div onClick={onRemove} className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div> + <div + onClick={() => { + onRemove() + handleTrigger() + }} + className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:text-text-destructive hover:bg-state-destructive-hover' + >{t('plugin.detailPanel.operation.remove')}</div> </div> </PortalToFollowElemContent> </PortalToFollowElem> diff --git a/web/app/components/plugins/plugin-item/action.tsx b/web/app/components/plugins/plugin-item/action.tsx index 89b6f2ac3e..540c234407 100644 --- a/web/app/components/plugins/plugin-item/action.tsx +++ b/web/app/components/plugins/plugin-item/action.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React, { useCallback } from 'react' -import type { MetaData } from '../types' +import { type MetaData, PluginSource } from '../types' import { RiDeleteBinLine, RiInformation2Line, RiLoopLeftLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useTranslation } from 'react-i18next' @@ -10,23 +10,31 @@ import ActionButton from '../../base/action-button' import Tooltip from '../../base/tooltip' import Confirm from '../../base/confirm' import { uninstallPlugin } from '@/service/plugins' -import { usePluginPageContext } from '../plugin-page/context' +import { useGitHubReleases } from '../install-plugin/hooks' +import { compareVersion, getLatestVersion } from '@/utils/semver' +import Toast from '@/app/components/base/toast' +import { useModalContext } from '@/context/modal-context' +import { useInvalidateInstalledPluginList } from '@/service/use-plugins' const i18nPrefix = 'plugin.action' type Props = { - pluginId: string + author: string + installationId: string pluginName: string + version: string usedInApps: number isShowFetchNewVersion: boolean isShowInfo: boolean isShowDelete: boolean onDelete: () => void - meta: MetaData + meta?: MetaData } const Action: FC<Props> = ({ - pluginId, + author, + installationId, pluginName, + version, isShowFetchNewVersion, isShowInfo, isShowDelete, @@ -38,13 +46,54 @@ const Action: FC<Props> = ({ setTrue: showPluginInfo, setFalse: hidePluginInfo, }] = useBoolean(false) - const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList) const [deleting, { setTrue: showDeleting, setFalse: hideDeleting, }] = useBoolean(false) + const { fetchReleases } = useGitHubReleases() + const { setShowUpdatePluginModal } = useModalContext() + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() - const handleFetchNewVersion = () => { } + const handleFetchNewVersion = async () => { + try { + const fetchedReleases = await fetchReleases(author, pluginName) + if (fetchedReleases.length === 0) + return + const versions = fetchedReleases.map(release => release.tag_name) + const latestVersion = getLatestVersion(versions) + if (compareVersion(latestVersion, version) === 1) { + setShowUpdatePluginModal({ + onSaveCallback: () => { + invalidateInstalledPluginList() + }, + payload: { + type: PluginSource.github, + github: { + originalPackageInfo: { + id: installationId, + repo: meta!.repo, + version: meta!.version, + package: meta!.package, + releases: fetchedReleases, + }, + }, + }, + }) + } + else { + Toast.notify({ + type: 'info', + message: 'No new version available', + }) + } + } + catch { + Toast.notify({ + type: 'error', + message: 'Failed to compare versions', + }) + } + } const [isShowDeleteConfirm, { setTrue: showDeleteConfirm, @@ -53,14 +102,13 @@ const Action: FC<Props> = ({ const handleDelete = useCallback(async () => { showDeleting() - const res = await uninstallPlugin(pluginId) + const res = await uninstallPlugin(installationId) hideDeleting() if (res.success) { hideDeleteConfirm() - mutateInstalledPluginList() onDelete() } - }, [pluginId, onDelete]) + }, [installationId, onDelete]) return ( <div className='flex space-x-1'> {/* Only plugin installed from GitHub need to check if it's the new version */} @@ -99,9 +147,9 @@ const Action: FC<Props> = ({ {isShowPluginInfo && ( <PluginInfo - repository={meta.repo} - release={meta.version} - packageName={meta.package} + repository={meta!.repo} + release={meta!.version} + packageName={meta!.package} onHide={hidePluginInfo} /> )} diff --git a/web/app/components/plugins/plugin-item/index.tsx b/web/app/components/plugins/plugin-item/index.tsx index 15a2bb9c39..5a6a3a6ca2 100644 --- a/web/app/components/plugins/plugin-item/index.tsx +++ b/web/app/components/plugins/plugin-item/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import React, { useMemo } from 'react' -import { useContext } from 'use-context-selector' import { RiArrowRightUpLine, RiBugLine, @@ -10,29 +9,34 @@ import { RiVerifiedBadgeLine, } from '@remixicon/react' import { useTranslation } from 'react-i18next' +import { usePluginPageContext } from '../plugin-page/context' import { Github } from '../../base/icons/src/public/common' import Badge from '../../base/badge' -import { type InstalledPlugin, PluginSource } from '../types' +import { type PluginDetail, PluginSource } from '../types' import CornerMark from '../card/base/corner-mark' import Description from '../card/base/description' import OrgInfo from '../card/base/org-info' import Title from '../card/base/title' import Action from './action' import cn from '@/utils/classnames' -import I18n from '@/context/i18n' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' +import { useLanguage } from '../../header/account-setting/model-provider-page/hooks' +import { useInvalidateInstalledPluginList } from '@/service/use-plugins' type Props = { className?: string - plugin: InstalledPlugin + plugin: PluginDetail } const PluginItem: FC<Props> = ({ className, plugin, }) => { - const { locale } = useContext(I18n) + const locale = useLanguage() const { t } = useTranslation() + const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) + const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail) + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { source, @@ -40,27 +44,24 @@ const PluginItem: FC<Props> = ({ installation_id, endpoints_active, meta, + plugin_id, version, - latest_version, } = plugin const { category, author, name, label, description, icon, verified } = plugin.declaration - // Only plugin installed from GitHub need to check if it's the new version - const hasNewVersion = useMemo(() => { - return source === PluginSource.github && latest_version !== version - }, [source, latest_version, version]) const orgName = useMemo(() => { return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : '' }, [source, author]) - - const tLocale = useMemo(() => { - return locale.replace('-', '_') - }, [locale]) return ( - <div className={`p-1 ${source === PluginSource.debugging - ? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]' - : 'bg-background-section-burn'} - rounded-xl`} + <div + className={cn( + 'p-1 rounded-xl border-[1.5px] border-background-section-burn', + currentPluginDetail?.plugin_id === plugin_id && 'border-components-option-card-option-selected-border', + source === PluginSource.debugging + ? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]' + : 'bg-background-section-burn', + )} + onClick={() => setCurrentPluginDetail(plugin)} > <div className={cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}> <CornerMark text={category} /> @@ -68,28 +69,35 @@ const PluginItem: FC<Props> = ({ <div className="flex"> <div className='flex items-center justify-center w-10 h-10 overflow-hidden border-components-panel-border-subtle border-[1px] rounded-xl'> <img + className='w-full h-full' src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} alt={`plugin-${installation_id}-logo`} /> </div> <div className="ml-3 w-0 grow"> <div className="flex items-center h-5"> - <Title title={label[tLocale]} /> + <Title title={label[locale]} /> {verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />} - <Badge className='ml-1' text={plugin.version} hasRedCornerMark={hasNewVersion} /> + <Badge className='ml-1' text={plugin.version} /> </div> <div className='flex items-center justify-between'> - <Description text={description[tLocale]} descriptionLineRows={1}></Description> - <Action - pluginId={installation_id} - pluginName={label[tLocale]} - usedInApps={5} - isShowFetchNewVersion={hasNewVersion} - isShowInfo={source === PluginSource.github} - isShowDelete - meta={meta} - onDelete={() => {}} - /> + <Description text={description[locale]} descriptionLineRows={1}></Description> + <div onClick={e => e.stopPropagation()}> + <Action + installationId={installation_id} + author={author} + pluginName={name} + version={version} + usedInApps={5} + isShowFetchNewVersion={source === PluginSource.github} + isShowInfo={source === PluginSource.github} + isShowDelete + meta={meta} + onDelete={() => { + invalidateInstalledPluginList() + }} + /> + </div> </div> </div> </div> @@ -112,7 +120,7 @@ const PluginItem: FC<Props> = ({ <div className='flex items-center'> {source === PluginSource.github && <> - <a href={meta.repo} target='_blank' className='flex items-center gap-1'> + <a href={`https://github.com/${meta!.repo}`} target='_blank' className='flex items-center gap-1'> <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('plugin.from')}</div> <div className='flex items-center space-x-0.5 text-text-secondary'> <Github className='w-3 h-3' /> diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index 42736f3edd..697d28a022 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import { + useMemo, useRef, useState, } from 'react' @@ -9,20 +10,24 @@ import { createContext, useContextSelector, } from 'use-context-selector' -import type { InstalledPlugin, Permissions } from '../types' +import { useSelector as useAppContextSelector } from '@/context/app-context' +import type { Permissions, PluginDetail } from '../types' import type { FilterState } from './filter-management' import { PermissionType } from '../types' -import { fetchInstalledPluginList } from '@/service/plugins' -import useSWR from 'swr' +import { useTranslation } from 'react-i18next' +import { useTabSearchParams } from '@/hooks/use-tab-searchparams' export type PluginPageContextValue = { containerRef: React.RefObject<HTMLDivElement> permissions: Permissions setPermissions: (permissions: PluginPageContextValue['permissions']) => void - installedPluginList: InstalledPlugin[] - mutateInstalledPluginList: () => void + currentPluginDetail: PluginDetail | undefined + setCurrentPluginDetail: (plugin: PluginDetail) => void filters: FilterState setFilters: (filter: FilterState) => void + activeTab: string + setActiveTab: (tab: string) => void + options: Array<{ value: string, text: string }> } export const PluginPageContext = createContext<PluginPageContextValue>({ @@ -31,15 +36,18 @@ export const PluginPageContext = createContext<PluginPageContextValue>({ install_permission: PermissionType.noOne, debug_permission: PermissionType.noOne, }, - setPermissions: () => { }, - installedPluginList: [], - mutateInstalledPluginList: () => {}, + setPermissions: () => {}, + currentPluginDetail: undefined, + setCurrentPluginDetail: () => {}, filters: { categories: [], tags: [], searchQuery: '', }, setFilters: () => {}, + activeTab: '', + setActiveTab: () => {}, + options: [], }) type PluginPageContextProviderProps = { @@ -53,6 +61,7 @@ export function usePluginPageContext(selector: (value: PluginPageContextValue) = export const PluginPageContextProvider = ({ children, }: PluginPageContextProviderProps) => { + const { t } = useTranslation() const containerRef = useRef<HTMLDivElement>(null) const [permissions, setPermissions] = useState<PluginPageContextValue['permissions']>({ install_permission: PermissionType.noOne, @@ -63,7 +72,22 @@ export const PluginPageContextProvider = ({ tags: [], searchQuery: '', }) - const { data, mutate: mutateInstalledPluginList } = useSWR({ url: '/workspaces/current/plugin/list' }, fetchInstalledPluginList) + const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>() + + const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const options = useMemo(() => { + return [ + { value: 'plugins', text: t('common.menus.plugins') }, + ...( + enable_marketplace + ? [{ value: 'discover', text: 'Explore Marketplace' }] + : [] + ), + ] + }, [t, enable_marketplace]) + const [activeTab, setActiveTab] = useTabSearchParams({ + defaultTab: options[0].value, + }) return ( <PluginPageContext.Provider @@ -71,10 +95,13 @@ export const PluginPageContextProvider = ({ containerRef, permissions, setPermissions, - installedPluginList: data?.plugins || [], - mutateInstalledPluginList, + currentPluginDetail, + setCurrentPluginDetail, filters, setFilters, + activeTab, + setActiveTab, + options, }} > {children} diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx new file mode 100644 index 0000000000..3092e0f444 --- /dev/null +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -0,0 +1,118 @@ +import React, { useMemo, useRef, useState } from 'react' +import { MagicBox } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' +import { FileZip } from '@/app/components/base/icons/src/vender/solid/files' +import { Github } from '@/app/components/base/icons/src/vender/solid/general' +import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-from-github' +import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package' +import { usePluginPageContext } from '../context' +import { Group } from '@/app/components/base/icons/src/vender/other' +import { useSelector as useAppContextSelector } from '@/context/app-context' +import Line from '../../marketplace/empty/line' +import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' + +const Empty = () => { + const fileInputRef = useRef<HTMLInputElement>(null) + const [selectedAction, setSelectedAction] = useState<string | null>(null) + const [selectedFile, setSelectedFile] = useState<File | null>(null) + const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const setActiveTab = usePluginPageContext(v => v.setActiveTab) + + const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { + const file = event.target.files?.[0] + if (file) { + setSelectedFile(file) + setSelectedAction('local') + } + } + const filters = usePluginPageContext(v => v.filters) + const { data: pluginList } = useInstalledPluginList() + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() + + const text = useMemo(() => { + if (pluginList?.plugins.length === 0) + return 'No plugins installed' + if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery) + return 'No plugins found' + }, [pluginList, filters]) + + return ( + <div className='grow w-full relative z-0'> + {/* skeleton */} + <div className='h-full w-full px-12 absolute top-0 grid grid-cols-2 gap-2 overflow-hidden z-10'> + {Array.from({ length: 20 }).fill(0).map((_, i) => ( + <div key={i} className='h-[100px] bg-components-card-bg rounded-xl'/> + ))} + </div> + {/* mask */} + <div className='h-full w-full absolute z-20 bg-gradient-to-b from-background-gradient-mask-transparent to-white'/> + <div className='flex items-center justify-center h-full relative z-30'> + <div className='flex flex-col items-center gap-y-3'> + <div className='relative -z-10 flex items-center justify-center w-[52px] h-[52px] rounded-xl + bg-components-card-bg border-[1px] border-dashed border-divider-deep shadow-xl shadow-shadow-shadow-5'> + <Group className='text-text-tertiary w-5 h-5' /> + <Line className='absolute -right-[1px] top-1/2 -translate-y-1/2' /> + <Line className='absolute -left-[1px] top-1/2 -translate-y-1/2' /> + <Line className='absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' /> + <Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' /> + </div> + <div className='text-text-tertiary text-sm font-normal'> + {text} + </div> + <div className='flex flex-col w-[240px]'> + <input + type='file' + ref={fileInputRef} + style={{ display: 'none' }} + onChange={handleFileChange} + accept='.difypkg' + /> + <div className='w-full flex flex-col gap-y-1'> + {[ + ...( + (enable_marketplace || true) + ? [{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' }] + : [] + ), + { icon: Github, text: 'GitHub', action: 'github' }, + { icon: FileZip, text: 'Local Package File', action: 'local' }, + ].map(({ icon: Icon, text, action }) => ( + <div + key={action} + className='flex items-center px-3 py-2 gap-x-1 rounded-lg bg-components-button-secondary-bg + hover:bg-state-base-hover cursor-pointer border-[0.5px] shadow-shadow-shadow-3 shadow-xs' + onClick={() => { + if (action === 'local') + fileInputRef.current?.click() + else if (action === 'marketplace') + setActiveTab('discover') + else + setSelectedAction(action) + }} + > + <Icon className="w-4 h-4 text-text-tertiary" /> + <span className='text-text-secondary system-md-regular'>{`Install from ${text}`}</span> + </div> + ))} + </div> + </div> + </div> + {selectedAction === 'github' && <InstallFromGitHub + onSuccess={() => { invalidateInstalledPluginList() }} + onClose={() => setSelectedAction(null)} + />} + {selectedAction === 'local' && selectedFile + && (<InstallFromLocalPackage + file={selectedFile} + onClose={() => setSelectedAction(null)} + onSuccess={() => { }} + /> + ) + } + </div> + </div> + ) +} + +Empty.displayName = 'Empty' + +export default React.memo(Empty) diff --git a/web/app/components/plugins/plugin-page/filter-management/category-filter.tsx b/web/app/components/plugins/plugin-page/filter-management/category-filter.tsx index b7a60a7e43..8544bef95c 100644 --- a/web/app/components/plugins/plugin-page/filter-management/category-filter.tsx +++ b/web/app/components/plugins/plugin-page/filter-management/category-filter.tsx @@ -87,7 +87,12 @@ const CategoriesFilter = ({ !!selectedTagsLength && ( <RiCloseCircleFill className='w-4 h-4 text-text-quaternary cursor-pointer' - onClick={() => onChange([])} + onClick={ + (e) => { + e.stopPropagation() + onChange([]) + } + } /> ) } diff --git a/web/app/components/plugins/plugin-page/filter-management/index.tsx b/web/app/components/plugins/plugin-page/filter-management/index.tsx index 1b09f4875e..c7a0bc7cd9 100644 --- a/web/app/components/plugins/plugin-page/filter-management/index.tsx +++ b/web/app/components/plugins/plugin-page/filter-management/index.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react' import CategoriesFilter from './category-filter' import TagFilter from './tag-filter' import SearchBox from './search-box' +import { usePluginPageContext } from '../context' export type FilterState = { categories: string[] @@ -14,11 +15,8 @@ type FilterManagementProps = { } const FilterManagement: React.FC<FilterManagementProps> = ({ onFilterChange }) => { - const [filters, setFilters] = useState<FilterState>({ - categories: [], - tags: [], - searchQuery: '', - }) + const initFilters = usePluginPageContext(v => v.filters) as FilterState + const [filters, setFilters] = useState<FilterState>(initFilters) const updateFilters = (newFilters: Partial<FilterState>) => { const updatedFilters = { ...filters, ...newFilters } diff --git a/web/app/components/plugins/plugin-page/hooks.ts b/web/app/components/plugins/plugin-page/hooks.ts deleted file mode 100644 index 395d90164a..0000000000 --- a/web/app/components/plugins/plugin-page/hooks.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - useCallback, - useEffect, - useState, -} from 'react' -import { useRequest } from 'ahooks' -import type { PluginTask } from '../types' -import { fetchPluginTasks } from '@/service/plugins' - -export const usePluginTasks = () => { - const [pluginTasks, setPluginTasks] = useState<PluginTask[]>([]) - - const handleUpdatePluginTasks = async (callback: (tasks: PluginTask[]) => void) => { - const { tasks } = await fetchPluginTasks() - setPluginTasks(tasks) - callback(tasks) - } - - const { run, cancel } = useRequest(handleUpdatePluginTasks, { - manual: true, - pollingInterval: 3000, - pollingErrorRetryCount: 2, - }) - - const checkHasPluginTasks = useCallback((tasks: PluginTask[]) => { - if (!tasks.length) - cancel() - }, [cancel]) - - useEffect(() => { - run(checkHasPluginTasks) - }, [run, checkHasPluginTasks]) - - return { - pluginTasks, - } -} diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index d8b7d649d8..f7b4f4ef74 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next' import { RiDragDropLine, RiEqualizer2Line, - RiInstallFill, } from '@remixicon/react' import { useBoolean } from 'ahooks' import InstallFromLocalPackage from '../install-plugin/install-from-local-package' @@ -17,8 +16,8 @@ import InstallPluginDropdown from './install-plugin-dropdown' import { useUploader } from './use-uploader' import usePermission from './use-permission' import DebugInfo from './debug-info' -import { usePluginTasks } from './hooks' -import { useTabSearchParams } from '@/hooks/use-tab-searchparams' +import { usePluginTasksStore } from './store' +import InstallInfo from './install-info' import Button from '@/app/components/base/button' import TabSlider from '@/app/components/base/tab-slider' import Tooltip from '@/app/components/base/tooltip' @@ -100,22 +99,11 @@ const PluginPage = ({ }] = useBoolean() const [currentFile, setCurrentFile] = useState<File | null>(null) const containerRef = usePluginPageContext(v => v.containerRef) + const options = usePluginPageContext(v => v.options) + const [activeTab, setActiveTab] = usePluginPageContext(v => [v.activeTab, v.setActiveTab]) const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) const [installed, total] = [2, 3] // Replace this with the actual progress const progressPercentage = (installed / total) * 100 - const options = useMemo(() => { - return [ - { value: 'plugins', text: t('common.menus.plugins') }, - ...( - enable_marketplace - ? [{ value: 'discover', text: 'Explore Marketplace' }] - : [] - ), - ] - }, [t, enable_marketplace]) - const [activeTab, setActiveTab] = useTabSearchParams({ - defaultTab: options[0].value, - }) const uploaderProps = useUploader({ onFileChange: setCurrentFile, @@ -125,10 +113,15 @@ const PluginPage = ({ const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps - const { pluginTasks } = usePluginTasks() + const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) + + useEffect(() => { + setPluginTasksWithPolling() + }, [setPluginTasksWithPolling]) return ( <div + id='marketplace-container' ref={containerRef} className={cn('grow relative flex flex-col overflow-y-auto border-t border-divider-subtle', activeTab === 'plugins' ? 'rounded-t-xl bg-components-panel-bg' @@ -149,22 +142,7 @@ const PluginPage = ({ /> </div> <div className='flex flex-shrink-0 items-center gap-1'> - <div className='relative'> - <Button - className='relative overflow-hidden border !border-[rgba(178,202,255,1)] !bg-[rgba(255,255,255,0.95)] cursor-default' - > - <div - className='absolute left-0 top-0 h-full bg-state-accent-active' - style={{ width: `${progressPercentage}%` }} - ></div> - <div className='relative z-10 flex items-center'> - <RiInstallFill className='w-4 h-4 text-text-accent' /> - <div className='flex px-0.5 justify-center items-center gap-1'> - <span className='text-text-accent system-sm-medium'>{activeTab === 'plugins' ? `Installing ${installed}/${total} plugins` : `${installed}/${total}`}</span> - </div> - </div> - </Button> - </div> + <InstallInfo /> {canManagement && ( <InstallPluginDropdown onSwitchToMarketplaceTab={() => setActiveTab('discover')} diff --git a/web/app/components/plugins/plugin-page/install-info.tsx b/web/app/components/plugins/plugin-page/install-info.tsx new file mode 100644 index 0000000000..bb0a31f4be --- /dev/null +++ b/web/app/components/plugins/plugin-page/install-info.tsx @@ -0,0 +1,86 @@ +import { + useState, +} from 'react' +import { + RiCheckboxCircleFill, + RiErrorWarningFill, + RiInstallLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Tooltip from '@/app/components/base/tooltip' +import Button from '@/app/components/base/button' +// import ProgressCircle from '@/app/components/base/progress-bar/progress-circle' +import { useMemo } from 'react' +import cn from '@/utils/classnames' + +const InstallInfo = () => { + const [open, setOpen] = useState(false) + const status = 'error' + const statusError = useMemo(() => status === 'error', [status]) + + return ( + <div className='flex items-center'> + <PortalToFollowElem + open={open} + onOpenChange={setOpen} + placement='bottom-start' + offset={{ + mainAxis: 4, + crossAxis: 79, + }} + > + <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> + <Tooltip popupContent='Installing 1/3 plugins...'> + <div + className={cn( + 'relative flex items-center justify-center w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover', + statusError && 'border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt', + )} + > + <RiInstallLine + className={cn( + 'w-4 h-4 text-components-button-secondary-text', + statusError && 'text-components-button-destructive-secondary-text', + )} + /> + <div className='absolute -right-1 -top-1'> + {/* <ProgressCircle + percentage={33} + circleFillColor='fill-components-progress-brand-bg' + sectorFillColor='fill-components-progress-error-bg' + circleStrokeColor='stroke-components-progress-error-bg' + /> */} + <RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' /> + </div> + </div> + </Tooltip> + </PortalToFollowElemTrigger> + <PortalToFollowElemContent className='z-10'> + <div className='p-1 pb-2 w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'> + <div className='flex items-center px-2 pt-1 h-7 system-sm-semibold-uppercase'>3 plugins failed to install</div> + <div className='flex items-center p-1 pl-2 h-8 rounded-lg hover:bg-state-base-hover'> + <div className='relative flex items-center justify-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'> + <RiErrorWarningFill className='absolute -right-0.5 -bottom-0.5 w-3 h-3 text-text-destructive' /> + </div> + <div className='grow system-md-regular text-text-secondary truncate'> + DuckDuckGo Search + </div> + <Button + size='small' + variant='ghost-accent' + > + Clear + </Button> + </div> + </div> + </PortalToFollowElemContent> + </PortalToFollowElem> + </div> + ) +} + +export default InstallInfo diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 605a9f36f0..3d1351b67c 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -15,6 +15,7 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { useSelector as useAppContextSelector } from '@/context/app-context' +import { useInvalidateInstalledPluginList } from '@/service/use-plugins' type Props = { onSwitchToMarketplaceTab: () => void @@ -27,6 +28,7 @@ const InstallPluginDropdown = ({ const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null) const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0] @@ -79,7 +81,7 @@ const InstallPluginDropdown = ({ onChange={handleFileChange} accept='.difypkg' /> - <div className='p-1 w-full'> + <div className='w-full'> {[ ...( (enable_marketplace || true) @@ -91,7 +93,7 @@ const InstallPluginDropdown = ({ ].map(({ icon: Icon, text, action }) => ( <div key={action} - className='flex items-center w-full px-2 py-1.5 gap-1 rounded-lg hover:bg-state-base-hover cursor-pointer' + className='flex items-center w-full px-2 py-1.5 gap-1 rounded-lg hover:bg-state-base-hover !cursor-pointer' onClick={() => { if (action === 'local') { fileInputRef.current?.click() @@ -114,12 +116,15 @@ const InstallPluginDropdown = ({ </div> </PortalToFollowElemContent> </div> - {selectedAction === 'github' && <InstallFromGitHub onClose={() => setSelectedAction(null)} />} + {selectedAction === 'github' && <InstallFromGitHub + onSuccess={() => { invalidateInstalledPluginList() }} + onClose={() => setSelectedAction(null)} + />} {selectedAction === 'local' && selectedFile && (<InstallFromLocalPackage file={selectedFile} onClose={() => setSelectedAction(null)} - onSuccess={() => { }} + onSuccess={() => {}} /> ) } diff --git a/web/app/components/plugins/plugin-page/list/index.tsx b/web/app/components/plugins/plugin-page/list/index.tsx index 23f6e403e5..57fea8c8b5 100644 --- a/web/app/components/plugins/plugin-page/list/index.tsx +++ b/web/app/components/plugins/plugin-page/list/index.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react' import PluginItem from '../../plugin-item' -import type { InstalledPlugin } from '../../types' +import type { PluginDetail } from '../../types' type IPluginListProps = { - pluginList: InstalledPlugin[] + pluginList: PluginDetail[] } const PluginList: FC<IPluginListProps> = ({ pluginList }) => { diff --git a/web/app/components/plugins/plugin-page/plugin-info.tsx b/web/app/components/plugins/plugin-page/plugin-info.tsx index ebec25f216..abd297905a 100644 --- a/web/app/components/plugins/plugin-page/plugin-info.tsx +++ b/web/app/components/plugins/plugin-page/plugin-info.tsx @@ -4,12 +4,13 @@ import React from 'react' import { useTranslation } from 'react-i18next' import KeyValueItem from '../base/key-value-item' import Modal from '../../base/modal' +import { convertRepoToUrl } from '../install-plugin/utils' const i18nPrefix = 'plugin.pluginInfoModal' type Props = { - repository: string - release: string - packageName: string + repository?: string + release?: string + packageName?: string onHide: () => void } @@ -30,9 +31,9 @@ const PlugInfo: FC<Props> = ({ closable > <div className='mt-5 space-y-3'> - <KeyValueItem label={t(`${i18nPrefix}.repository`)} labelWidthClassName={labelWidthClassName} value={repository} /> - <KeyValueItem label={t(`${i18nPrefix}.release`)} labelWidthClassName={labelWidthClassName} value={release} /> - <KeyValueItem label={t(`${i18nPrefix}.packageName`)} labelWidthClassName={labelWidthClassName} value={packageName} /> + {repository && <KeyValueItem label={t(`${i18nPrefix}.repository`)} labelWidthClassName={labelWidthClassName} value={`${convertRepoToUrl(repository)}`} valueMaxWidthClassName='max-w-[190px]' />} + {release && <KeyValueItem label={t(`${i18nPrefix}.release`)} labelWidthClassName={labelWidthClassName} value={release} />} + {packageName && <KeyValueItem label={t(`${i18nPrefix}.packageName`)} labelWidthClassName={labelWidthClassName} value={packageName} />} </div> </Modal> ) diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 8dbbf8eaa5..466df72066 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -1,36 +1,36 @@ 'use client' -import { useMemo, useState } from 'react' -import type { EndpointListItem, InstalledPlugin, PluginDetail } from '../types' +import { useMemo } from 'react' import type { FilterState } from './filter-management' import FilterManagement from './filter-management' import List from './list' +import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' -import { toolNotion, toolNotionEndpoints } from '@/app/components/plugins/plugin-detail-panel/mock' import { usePluginPageContext } from './context' import { useDebounceFn } from 'ahooks' +import Empty from './empty' +import Loading from '../../base/loading' const PluginsPanel = () => { - const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters]) - const pluginList = usePluginPageContext(v => v.installedPluginList) as InstalledPlugin[] + const [filters, setFilters] = usePluginPageContext(v => [v.filters, v.setFilters]) as [FilterState, (filter: FilterState) => void] + const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList() + const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { run: handleFilterChange } = useDebounceFn((filters: FilterState) => { setFilters(filters) }, { wait: 500 }) const filteredList = useMemo(() => { - // todo: filter by tags - const { categories, searchQuery } = filters - const filteredList = pluginList.filter((plugin) => { + const { categories, searchQuery, tags } = filters + const filteredList = pluginList?.plugins.filter((plugin) => { return ( (categories.length === 0 || categories.includes(plugin.declaration.category)) + && (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag))) && (searchQuery === '' || plugin.plugin_id.toLowerCase().includes(searchQuery.toLowerCase())) ) }) return filteredList }, [pluginList, filters]) - const [currentPluginDetail, setCurrentPluginDetail] = useState<PluginDetail | undefined>(toolNotion as any) - const [currentPluginEndpoints, setCurrentEndpoints] = useState<EndpointListItem[]>(toolNotionEndpoints as any) return ( <> <div className='flex flex-col pt-1 pb-3 px-12 justify-center items-start gap-3 self-stretch'> @@ -39,19 +39,16 @@ const PluginsPanel = () => { onFilterChange={handleFilterChange} /> </div> - <div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'> - <div className='w-full'> - <List pluginList={filteredList} /> + {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( + <div className='flex px-12 items-start content-start gap-2 flex-grow self-stretch flex-wrap'> + <div className='w-full'> + <List pluginList={filteredList || []} /> + </div> </div> - </div> - <PluginDetailPanel - pluginDetail={currentPluginDetail} - endpointList={currentPluginEndpoints} - onHide={() => { - setCurrentPluginDetail(undefined) - setCurrentEndpoints([]) - }} - /> + ) : ( + <Empty /> + )} + <PluginDetailPanel onDelete={() => invalidateInstalledPluginList()}/> </> ) } diff --git a/web/app/components/plugins/plugin-page/store.tsx b/web/app/components/plugins/plugin-page/store.tsx new file mode 100644 index 0000000000..25074b973f --- /dev/null +++ b/web/app/components/plugins/plugin-page/store.tsx @@ -0,0 +1,40 @@ +import { create } from 'zustand' +import type { PluginTask } from '../types' +import { fetchPluginTasks } from '@/service/plugins' + +type PluginTasksStore = { + pluginTasks: PluginTask[] + setPluginTasks: (tasks: PluginTask[]) => void + setPluginTasksWithPolling: () => void +} + +let pluginTasksTimer: NodeJS.Timeout | null = null + +export const usePluginTasksStore = create<PluginTasksStore>(set => ({ + pluginTasks: [], + setPluginTasks: (tasks: PluginTask[]) => set({ pluginTasks: tasks }), + setPluginTasksWithPolling: async () => { + if (pluginTasksTimer) { + clearTimeout(pluginTasksTimer) + pluginTasksTimer = null + } + const handleUpdatePluginTasks = async () => { + const { tasks } = await fetchPluginTasks() + set({ pluginTasks: tasks }) + + if (tasks.length && !tasks.every(task => task.status === 'success')) { + pluginTasksTimer = setTimeout(() => { + handleUpdatePluginTasks() + }, 5000) + } + else { + if (pluginTasksTimer) { + clearTimeout(pluginTasksTimer) + pluginTasksTimer = null + } + } + } + + handleUpdatePluginTasks() + }, +})) diff --git a/web/app/components/plugins/provider-card.tsx b/web/app/components/plugins/provider-card.tsx index 15b78acd82..5c8ab1891e 100644 --- a/web/app/components/plugins/provider-card.tsx +++ b/web/app/components/plugins/provider-card.tsx @@ -2,7 +2,7 @@ import React from 'react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' -import { RiArrowRightUpLine, RiVerifiedBadgeLine } from '@remixicon/react' +import { RiArrowRightUpLine } from '@remixicon/react' import Badge from '../base/badge' import type { Plugin } from './types' import Description from './card/base/description' @@ -12,7 +12,9 @@ import DownloadCount from './card/base/download-count' import Button from '@/app/components/base/button' import { useGetLanguage } from '@/context/i18n' import { MARKETPLACE_URL_PREFIX } from '@/config' +import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import cn from '@/utils/classnames' +import { useBoolean } from 'ahooks' type Props = { className?: string @@ -24,6 +26,10 @@ const ProviderCard: FC<Props> = ({ payload, }) => { const { t } = useTranslation() + const [isShowInstallFromMarketplace, { + setTrue: showInstallFromMarketplace, + setFalse: hideInstallFromMarketplace, + }] = useBoolean(false) const language = useGetLanguage() const { org, label } = payload @@ -35,7 +41,7 @@ const ProviderCard: FC<Props> = ({ <div className="ml-3 w-0 grow"> <div className="flex items-center h-5"> <Title title={label[language]} /> - <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> + {/* <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" /> */} </div> <div className='mb-1 flex justify-between items-center h-4'> <div className='flex items-center'> @@ -58,6 +64,7 @@ const ProviderCard: FC<Props> = ({ <Button className='flex-grow' variant='primary' + onClick={showInstallFromMarketplace} > {t('plugin.detailPanel.operation.install')} </Button> @@ -71,6 +78,16 @@ const ProviderCard: FC<Props> = ({ </a> </Button> </div> + { + isShowInstallFromMarketplace && ( + <InstallFromMarketplace + manifest={payload as any} + uniqueIdentifier={payload.latest_package_identifier} + onClose={hideInstallFromMarketplace} + onSuccess={hideInstallFromMarketplace} + /> + ) + } </div> ) } diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 3c7894a576..95b255de62 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -54,6 +54,7 @@ export type EndpointListItem = { // Plugin manifest export type PluginDeclaration = { + plugin_unique_identifier: string version: string author: string icon: string @@ -68,9 +69,11 @@ export type PluginDeclaration = { endpoint: PluginEndpointDeclaration tool: PluginToolDeclaration model: any // TODO + tags: string[] } export type PluginManifestInMarket = { + plugin_unique_identifier: string name: string org: string icon: string @@ -80,6 +83,7 @@ export type PluginManifestInMarket = { brief: Record<Locale, string> introduction: string verified: boolean + install_count: number } export type PluginDetail = { @@ -95,8 +99,9 @@ export type PluginDetail = { endpoints_setups: number endpoints_active: number version: string + latest_version: string source: PluginSource - meta?: any + meta?: MetaData } export type Plugin = { @@ -106,6 +111,7 @@ export type Plugin = { plugin_id: string version: string latest_version: string + latest_package_identifier: string icon: string verified: boolean label: Record<Locale, string> @@ -132,11 +138,45 @@ export type Permissions = { debug_permission: PermissionType } +export type UpdateFromMarketPlacePayload = { + originalPackageInfo: { + id: string + payload: PluginDeclaration + }, + targetPackageInfo: { + id: string + version: string + } +} + +export type UpdateFromGitHubPayload = { + originalPackageInfo: { + id: string + repo: string + version: string + package: string + releases: GitHubRepoReleaseResponse[] + } +} + +export type UpdatePluginPayload = { + type: PluginSource + marketPlace?: UpdateFromMarketPlacePayload + github?: UpdateFromGitHubPayload +} + +export type UpdatePluginModalType = UpdatePluginPayload & { + onCancel: () => void + onSave: () => void +} + export enum InstallStepFromGitHub { setUrl = 'url', - setVersion = 'version', - setPackage = 'package', + selectPackage = 'selecting', + readyToInstall = 'readyToInstall', + uploadFailed = 'uploadFailed', installed = 'installed', + installFailed = 'failed', } export type InstallState = { @@ -163,7 +203,7 @@ export type EndpointOperationResponse = { result: 'success' | 'error' } export type EndpointsRequest = { - limit: number + page_size: number page: number plugin_id: string } @@ -206,6 +246,11 @@ export type InstallPackageResponse = { task_id: string } +export type uploadGitHubResponse = { + unique_identifier: string + manifest: PluginDeclaration +} + export type DebugInfo = { key: string host: string @@ -249,20 +294,8 @@ export type MetaData = { package: string } -export type InstalledPlugin = { - plugin_id: string - installation_id: string - declaration: PluginDeclaration - source: PluginSource - tenant_id: string - version: string - latest_version: string - endpoints_active: number - meta: MetaData -} - export type InstalledPluginListResponse = { - plugins: InstalledPlugin[] + plugins: PluginDetail[] } export type UninstallPluginResponse = { diff --git a/web/app/components/plugins/update-plugin/from-github.tsx b/web/app/components/plugins/update-plugin/from-github.tsx new file mode 100644 index 0000000000..9bc2f2ad3e --- /dev/null +++ b/web/app/components/plugins/update-plugin/from-github.tsx @@ -0,0 +1,26 @@ +'use client' +import type { FC } from 'react' +import React from 'react' +import type { UpdateFromGitHubPayload } from '../types' +import InstallFromGitHub from '../install-plugin/install-from-github' + +type Props = { + payload: UpdateFromGitHubPayload + onSave: () => void + onCancel: () => void +} + +const FromGitHub: FC<Props> = ({ + payload, + onSave, + onCancel, +}) => { + return ( + <InstallFromGitHub + updatePayload={payload} + onClose={onCancel} + onSuccess={onSave} + /> + ) +} +export default React.memo(FromGitHub) diff --git a/web/app/components/plugins/update-plugin/from-market-place.tsx b/web/app/components/plugins/update-plugin/from-market-place.tsx new file mode 100644 index 0000000000..e0b54a1acf --- /dev/null +++ b/web/app/components/plugins/update-plugin/from-market-place.tsx @@ -0,0 +1,153 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { RiInformation2Line } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import Card from '@/app/components/plugins/card' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Badge, { BadgeState } from '@/app/components/base/badge/index' +import type { UpdateFromMarketPlacePayload } from '../types' +import { pluginManifestToCardPluginProps } from '@/app/components/plugins/install-plugin/utils' +import useGetIcon from '../install-plugin/base/use-get-icon' +import { updateFromMarketPlace } from '@/service/plugins' +import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status' +import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store' + +const i18nPrefix = 'plugin.upgrade' + +type Props = { + payload: UpdateFromMarketPlacePayload + onSave: () => void + onCancel: () => void +} + +enum UploadStep { + notStarted = 'notStarted', + upgrading = 'upgrading', + installed = 'installed', +} + +const UpdatePluginModal: FC<Props> = ({ + payload, + onSave, + onCancel, +}) => { + const { + originalPackageInfo, + targetPackageInfo, + } = payload + const { t } = useTranslation() + const { getIconUrl } = useGetIcon() + const [icon, setIcon] = useState<string>(originalPackageInfo.payload.icon) + useEffect(() => { + (async () => { + const icon = await getIconUrl(originalPackageInfo.payload.icon) + setIcon(icon) + })() + }, [originalPackageInfo, getIconUrl]) + const { + check, + stop, + } = checkTaskStatus() + const handleCancel = () => { + stop() + onCancel() + } + + const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted) + const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) + + const configBtnText = useMemo(() => { + return ({ + [UploadStep.notStarted]: t(`${i18nPrefix}.upgrade`), + [UploadStep.upgrading]: t(`${i18nPrefix}.upgrading`), + [UploadStep.installed]: t(`${i18nPrefix}.close`), + })[uploadStep] + }, [t, uploadStep]) + + const handleConfirm = useCallback(async () => { + if (uploadStep === UploadStep.notStarted) { + setUploadStep(UploadStep.upgrading) + const { + all_installed: isInstalled, + task_id: taskId, + } = await updateFromMarketPlace({ + original_plugin_unique_identifier: originalPackageInfo.id, + new_plugin_unique_identifier: targetPackageInfo.id, + }) + if (isInstalled) { + onSave() + return + } + setPluginTasksWithPolling() + await check({ + taskId, + pluginUniqueIdentifier: targetPackageInfo.id, + }) + onSave() + } + if (uploadStep === UploadStep.installed) { + onSave() + onCancel() + } + }, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, setPluginTasksWithPolling, targetPackageInfo.id]) + const usedInAppInfo = useMemo(() => { + return ( + <div className='flex px-0.5 justify-center items-center gap-0.5'> + <div className='text-text-warning system-xs-medium'>{t(`${i18nPrefix}.usedInApps`, { num: 3 })}</div> + {/* show the used apps */} + <RiInformation2Line className='w-4 h-4 text-text-tertiary' /> + </div> + ) + }, [t]) + return ( + <Modal + isShow={true} + onClose={onCancel} + className='min-w-[560px]' + closable + title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)} + > + <div className='mt-3 mb-2 text-text-secondary system-md-regular'> + {t(`${i18nPrefix}.description`)} + </div> + <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'> + <Card + installed={uploadStep === UploadStep.installed} + payload={pluginManifestToCardPluginProps({ + ...originalPackageInfo.payload, + icon: icon!, + })} + className='w-full' + titleLeft={ + <> + <Badge className='mx-1' size="s" state={BadgeState.Warning}> + {`${originalPackageInfo.payload.version} -> ${targetPackageInfo.version}`} + </Badge> + {false && usedInAppInfo} + </> + } + /> + </div> + <div className='flex pt-5 justify-end items-center gap-2 self-stretch'> + {uploadStep === UploadStep.notStarted && ( + <Button + onClick={handleCancel} + > + {t('common.operation.cancel')} + </Button> + )} + <Button + variant='primary' + loading={uploadStep === UploadStep.upgrading} + onClick={handleConfirm} + disabled={uploadStep === UploadStep.upgrading} + > + {configBtnText} + </Button> + </div> + </Modal> + ) +} +export default React.memo(UpdatePluginModal) diff --git a/web/app/components/plugins/update-plugin/index.tsx b/web/app/components/plugins/update-plugin/index.tsx index 0a358debac..f9b49a6073 100644 --- a/web/app/components/plugins/update-plugin/index.tsx +++ b/web/app/components/plugins/update-plugin/index.tsx @@ -1,97 +1,33 @@ 'use client' import type { FC } from 'react' -import React, { useCallback, useMemo, useState } from 'react' -import { RiInformation2Line } from '@remixicon/react' -import { useTranslation } from 'react-i18next' -import Card from '@/app/components/plugins/card' -import Modal from '@/app/components/base/modal' -import Button from '@/app/components/base/button' -import Badge, { BadgeState } from '@/app/components/base/badge/index' -import { toolNotion } from '@/app/components/plugins/card/card-mock' +import React from 'react' +import type { UpdatePluginModalType } from '../types' +import { PluginSource } from '../types' +import UpdateFromGitHub from './from-github' +import UpdateFromMarketplace from './from-market-place' -const i18nPrefix = 'plugin.upgrade' - -type Props = { - onHide: () => void -} - -enum UploadStep { - notStarted = 'notStarted', - upgrading = 'upgrading', - installed = 'installed', -} - -const UpdatePluginModal: FC<Props> = ({ - onHide, +const UpdatePlugin: FC<UpdatePluginModalType> = ({ + type, + marketPlace, + github, + onCancel, + onSave, }) => { - const { t } = useTranslation() - const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted) - const configBtnText = useMemo(() => { - return ({ - [UploadStep.notStarted]: t(`${i18nPrefix}.upgrade`), - [UploadStep.upgrading]: t(`${i18nPrefix}.upgrading`), - [UploadStep.installed]: t(`${i18nPrefix}.close`), - })[uploadStep] - }, [uploadStep]) - const handleConfirm = useCallback(() => { - if (uploadStep === UploadStep.notStarted) { - setUploadStep(UploadStep.upgrading) - setTimeout(() => { - setUploadStep(UploadStep.installed) - }, 1500) - return - } - if (uploadStep === UploadStep.installed) - onHide() - }, [uploadStep]) + if (type === PluginSource.github) { + return ( + <UpdateFromGitHub + payload={github!} + onSave={onSave} + onCancel={onCancel} + /> + ) + } return ( - <Modal - isShow={true} - onClose={onHide} - className='min-w-[560px]' - closable - title={t(`${i18nPrefix}.${uploadStep === UploadStep.installed ? 'successfulTitle' : 'title'}`)} - > - <div className='mt-3 mb-2 text-text-secondary system-md-regular'> - {t(`${i18nPrefix}.description`)} - </div> - <div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'> - <Card - installed={uploadStep === UploadStep.installed} - payload={toolNotion as any} - className='w-full' - titleLeft={ - <> - <Badge className='mx-1' size="s" state={BadgeState.Warning}> - {'1.2.0 -> 1.3.2'} - </Badge> - <div className='flex px-0.5 justify-center items-center gap-0.5'> - <div className='text-text-warning system-xs-medium'>{t(`${i18nPrefix}.usedInApps`, { num: 3 })}</div> - {/* show the used apps */} - <RiInformation2Line className='w-4 h-4 text-text-tertiary' /> - </div> - </> - } - /> - </div> - <div className='flex pt-5 justify-end items-center gap-2 self-stretch'> - {uploadStep === UploadStep.notStarted && ( - <Button - onClick={onHide} - > - {t('common.operation.cancel')} - </Button> - )} - <Button - variant='primary' - loading={uploadStep === UploadStep.upgrading} - onClick={handleConfirm} - disabled={uploadStep === UploadStep.upgrading} - > - {configBtnText} - </Button> - </div> - </Modal> + <UpdateFromMarketplace + payload={marketPlace!} + onSave={onSave} + onCancel={onCancel} + /> ) } -export default React.memo(UpdatePluginModal) +export default React.memo(UpdatePlugin) diff --git a/web/app/components/tools/edit-custom-collection-modal/modal.tsx b/web/app/components/tools/edit-custom-collection-modal/modal.tsx new file mode 100644 index 0000000000..099012b277 --- /dev/null +++ b/web/app/components/tools/edit-custom-collection-modal/modal.tsx @@ -0,0 +1,361 @@ +'use client' +import type { FC } from 'react' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDebounce, useGetState } from 'ahooks' +import produce from 'immer' +import { LinkExternal02, Settings01 } from '../../base/icons/src/vender/line/general' +import type { Credential, CustomCollectionBackend, CustomParamSchema, Emoji } from '../types' +import { AuthHeaderPrefix, AuthType } from '../types' +import GetSchema from './get-schema' +import ConfigCredentials from './config-credentials' +import TestApi from './test-api' +import cn from '@/utils/classnames' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import EmojiPicker from '@/app/components/base/emoji-picker' +import AppIcon from '@/app/components/base/app-icon' +import { parseParamsSchema } from '@/service/tools' +import LabelSelector from '@/app/components/tools/labels/selector' +import Toast from '@/app/components/base/toast' +import Modal from '../../base/modal' +import Button from '@/app/components/base/button' + +const fieldNameClassNames = 'py-2 leading-5 text-sm font-medium text-gray-900' +type Props = { + positionLeft?: boolean + payload: any + onHide: () => void + onAdd?: (payload: CustomCollectionBackend) => void + onRemove?: () => void + onEdit?: (payload: CustomCollectionBackend) => void +} +// Add and Edit +const EditCustomCollectionModal: FC<Props> = ({ + payload, + onHide, + onAdd, + onEdit, + onRemove, +}) => { + const { t } = useTranslation() + const isAdd = !payload + const isEdit = !!payload + + const [editFirst, setEditFirst] = useState(!isAdd) + const [paramsSchemas, setParamsSchemas] = useState<CustomParamSchema[]>(payload?.tools || []) + const [customCollection, setCustomCollection, getCustomCollection] = useGetState<CustomCollectionBackend>(isAdd + ? { + provider: '', + credentials: { + auth_type: AuthType.none, + api_key_header: 'Authorization', + api_key_header_prefix: AuthHeaderPrefix.basic, + }, + icon: { + content: '🕵️', + background: '#FEF7C3', + }, + schema_type: '', + schema: '', + } + : payload) + + const originalProvider = isEdit ? payload.provider : '' + + const [showEmojiPicker, setShowEmojiPicker] = useState(false) + const emoji = customCollection.icon + const setEmoji = (emoji: Emoji) => { + const newCollection = produce(customCollection, (draft) => { + draft.icon = emoji + }) + setCustomCollection(newCollection) + } + const schema = customCollection.schema + const debouncedSchema = useDebounce(schema, { wait: 500 }) + const setSchema = (schema: any) => { + const newCollection = produce(customCollection, (draft) => { + draft.schema = schema + }) + setCustomCollection(newCollection) + } + + useEffect(() => { + if (!debouncedSchema) + return + if (isEdit && editFirst) { + setEditFirst(false) + return + } + (async () => { + try { + const { parameters_schema, schema_type } = await parseParamsSchema(debouncedSchema) + const customCollection = getCustomCollection() + const newCollection = produce(customCollection, (draft) => { + draft.schema_type = schema_type + }) + setCustomCollection(newCollection) + setParamsSchemas(parameters_schema) + } + catch (e) { + const customCollection = getCustomCollection() + const newCollection = produce(customCollection, (draft) => { + draft.schema_type = '' + }) + setCustomCollection(newCollection) + setParamsSchemas([]) + } + })() + }, [debouncedSchema]) + + const [credentialsModalShow, setCredentialsModalShow] = useState(false) + const credential = customCollection.credentials + const setCredential = (credential: Credential) => { + const newCollection = produce(customCollection, (draft) => { + draft.credentials = credential + }) + setCustomCollection(newCollection) + } + + const [currTool, setCurrTool] = useState<CustomParamSchema | null>(null) + const [isShowTestApi, setIsShowTestApi] = useState(false) + + const [labels, setLabels] = useState<string[]>(payload?.labels || []) + const handleLabelSelect = (value: string[]) => { + setLabels(value) + } + + const handleSave = () => { + // const postData = clone(customCollection) + const postData = produce(customCollection, (draft) => { + delete draft.tools + + if (draft.credentials.auth_type === AuthType.none) { + delete draft.credentials.api_key_header + delete draft.credentials.api_key_header_prefix + delete draft.credentials.api_key_value + } + + draft.labels = labels + }) + + let errorMessage = '' + if (!postData.provider) + errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.name') }) + + if (!postData.schema) + errorMessage = t('common.errorMsg.fieldRequired', { field: t('tools.createTool.schema') }) + + if (errorMessage) { + Toast.notify({ + type: 'error', + message: errorMessage, + }) + return + } + + if (isAdd) { + onAdd?.(postData) + return + } + + onEdit?.({ + ...postData, + original_provider: originalProvider, + }) + } + + const getPath = (url: string) => { + if (!url) + return '' + + try { + const path = decodeURI(new URL(url).pathname) + return path || '' + } + catch (e) { + return url + } + } + + return ( + <> + <Modal + isShow + onClose={onHide} + closable + className='!p-0 !max-w-[630px] !h-[calc(100vh-16px)]' + > + <div className='flex flex-col h-full'> + <div className='ml-6 mt-6 text-base font-semibold text-gray-900'> + {t('tools.createTool.title')} + </div> + <div className='grow h-0 overflow-y-auto px-6 py-3 space-y-4'> + <div> + <div className={fieldNameClassNames}>{t('tools.createTool.name')} <span className='ml-1 text-red-500'>*</span></div> + <div className='flex items-center justify-between gap-3'> + <AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.content} background={emoji.background} /> + <Input + className='h-10 grow' placeholder={t('tools.createTool.toolNamePlaceHolder')!} + value={customCollection.provider} + onChange={(e) => { + const newCollection = produce(customCollection, (draft) => { + draft.provider = e.target.value + }) + setCustomCollection(newCollection) + }} + /> + </div> + </div> + + {/* Schema */} + <div className='select-none'> + <div className='flex justify-between items-center'> + <div className='flex items-center'> + <div className={fieldNameClassNames}>{t('tools.createTool.schema')}<span className='ml-1 text-red-500'>*</span></div> + <div className='mx-2 w-px h-3 bg-black/5'></div> + <a + href="https://swagger.io/specification/" + target='_blank' rel='noopener noreferrer' + className='flex items-center h-[18px] space-x-1 text-[#155EEF]' + > + <div className='text-xs font-normal'>{t('tools.createTool.viewSchemaSpec')}</div> + <LinkExternal02 className='w-3 h-3' /> + </a> + </div> + <GetSchema onChange={setSchema} /> + + </div> + <Textarea + className='h-[240px] resize-none' + value={schema} + onChange={e => setSchema(e.target.value)} + placeholder={t('tools.createTool.schemaPlaceHolder')!} + /> + </div> + + {/* Available Tools */} + <div> + <div className={fieldNameClassNames}>{t('tools.createTool.availableTools.title')}</div> + <div className='rounded-lg border border-gray-200 w-full overflow-x-auto'> + <table className='w-full leading-[18px] text-xs text-gray-700 font-normal'> + <thead className='text-gray-500 uppercase'> + <tr className={cn(paramsSchemas.length > 0 && 'border-b', 'border-gray-200')}> + <th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.name')}</th> + <th className="p-2 pl-3 font-medium w-[236px]">{t('tools.createTool.availableTools.description')}</th> + <th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.method')}</th> + <th className="p-2 pl-3 font-medium">{t('tools.createTool.availableTools.path')}</th> + <th className="p-2 pl-3 font-medium w-[54px]">{t('tools.createTool.availableTools.action')}</th> + </tr> + </thead> + <tbody> + {paramsSchemas.map((item, index) => ( + <tr key={index} className='border-b last:border-0 border-gray-200'> + <td className="p-2 pl-3">{item.operation_id}</td> + <td className="p-2 pl-3 text-gray-500 w-[236px]">{item.summary}</td> + <td className="p-2 pl-3">{item.method}</td> + <td className="p-2 pl-3">{getPath(item.server_url)}</td> + <td className="p-2 pl-3 w-[62px]"> + <Button + size='small' + onClick={() => { + setCurrTool(item) + setIsShowTestApi(true) + }} + > + {t('tools.createTool.availableTools.test')} + </Button> + </td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + + {/* Authorization method */} + <div> + <div className={fieldNameClassNames}>{t('tools.createTool.authMethod.title')}</div> + <div className='flex items-center h-9 justify-between px-2.5 bg-gray-100 rounded-lg cursor-pointer' onClick={() => setCredentialsModalShow(true)}> + <div className='text-sm font-normal text-gray-900'>{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}</div> + <Settings01 className='w-4 h-4 text-gray-700 opacity-60' /> + </div> + </div> + + {/* Labels */} + <div> + <div className='py-2 leading-5 text-sm font-medium text-gray-900'>{t('tools.createTool.toolInput.label')}</div> + <LabelSelector value={labels} onChange={handleLabelSelect} /> + </div> + + {/* Privacy Policy */} + <div> + <div className={fieldNameClassNames}>{t('tools.createTool.privacyPolicy')}</div> + <Input + value={customCollection.privacy_policy} + onChange={(e) => { + const newCollection = produce(customCollection, (draft) => { + draft.privacy_policy = e.target.value + }) + setCustomCollection(newCollection) + }} + className='h-10 grow' placeholder={t('tools.createTool.privacyPolicyPlaceholder') || ''} /> + </div> + + <div> + <div className={fieldNameClassNames}>{t('tools.createTool.customDisclaimer')}</div> + <Input + value={customCollection.custom_disclaimer} + onChange={(e) => { + const newCollection = produce(customCollection, (draft) => { + draft.custom_disclaimer = e.target.value + }) + setCustomCollection(newCollection) + }} + className='h-10 grow' placeholder={t('tools.createTool.customDisclaimerPlaceholder') || ''} /> + </div> + + </div> + <div className={cn(isEdit ? 'justify-between' : 'justify-end', 'mt-2 shrink-0 flex py-4 px-6 rounded-b-[10px] bg-gray-50 border-t border-black/5')} > + { + isEdit && ( + <Button onClick={onRemove} className='text-red-500 border-red-50 hover:border-red-500'>{t('common.operation.delete')}</Button> + ) + } + <div className='flex space-x-2 '> + <Button onClick={onHide}>{t('common.operation.cancel')}</Button> + <Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button> + </div> + </div> + {showEmojiPicker && <EmojiPicker + onSelect={(icon, icon_background) => { + setEmoji({ content: icon, background: icon_background }) + setShowEmojiPicker(false) + }} + onClose={() => { + setShowEmojiPicker(false) + }} + />} + {credentialsModalShow && ( + <ConfigCredentials + positionCenter={isAdd} + credential={credential} + onChange={setCredential} + onHide={() => setCredentialsModalShow(false)} + />) + } + {isShowTestApi && ( + <TestApi + positionCenter={isAdd} + tool={currTool as CustomParamSchema} + customCollection={customCollection} + onHide={() => setIsShowTestApi(false)} + /> + )} + </div> + </Modal> + </> + + ) +} +export default React.memo(EditCustomCollectionModal) diff --git a/web/app/components/tools/labels/constant.ts b/web/app/components/tools/labels/constant.ts index 3f073859d9..ad4836e6a8 100644 --- a/web/app/components/tools/labels/constant.ts +++ b/web/app/components/tools/labels/constant.ts @@ -1,6 +1,4 @@ -import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations' export type Label = { name: string - icon: string - label: TypeWithI18N + label: string } diff --git a/web/app/components/tools/labels/filter.tsx b/web/app/components/tools/labels/filter.tsx index 20db687e79..8f6e954b92 100644 --- a/web/app/components/tools/labels/filter.tsx +++ b/web/app/components/tools/labels/filter.tsx @@ -1,10 +1,8 @@ import type { FC } from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { useDebounceFn, useMount } from 'ahooks' +import { useDebounceFn } from 'ahooks' import { RiArrowDownSLine } from '@remixicon/react' -import { useStore as useLabelStore } from './store' import cn from '@/utils/classnames' import { PortalToFollowElem, @@ -16,9 +14,7 @@ import { Tag01, Tag03 } from '@/app/components/base/icons/src/vender/line/financ import { Check } from '@/app/components/base/icons/src/vender/line/general' import { XCircle } from '@/app/components/base/icons/src/vender/solid/general' import type { Label } from '@/app/components/tools/labels/constant' -import { fetchLabelList } from '@/service/tools' -import I18n from '@/context/i18n' -import { getLanguage } from '@/i18n/language' +import { useTags } from '@/app/components/plugins/hooks' type LabelFilterProps = { value: string[] @@ -29,12 +25,9 @@ const LabelFilter: FC<LabelFilterProps> = ({ onChange, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) - const language = getLanguage(locale) const [open, setOpen] = useState(false) - const labelList = useLabelStore(s => s.labelList) - const setLabelList = useLabelStore(s => s.setLabelList) + const { tags: labelList } = useTags() const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') @@ -61,12 +54,6 @@ const LabelFilter: FC<LabelFilterProps> = ({ onChange([...value, label.name]) } - useMount(() => { - fetchLabelList().then((res) => { - setLabelList(res) - }) - }) - return ( <PortalToFollowElem open={open} @@ -90,7 +77,7 @@ const LabelFilter: FC<LabelFilterProps> = ({ </div> <div className='text-[13px] leading-[18px] text-gray-700'> {!value.length && t('common.tag.placeholder')} - {!!value.length && currentLabel?.label[language]} + {!!value.length && currentLabel?.label} </div> {value.length > 1 && ( <div className='text-xs font-medium leading-[18px] text-gray-500'>{`+${value.length - 1}`}</div> @@ -128,7 +115,7 @@ const LabelFilter: FC<LabelFilterProps> = ({ className='flex items-center gap-2 pl-3 py-[6px] pr-2 rounded-lg cursor-pointer hover:bg-gray-100' onClick={() => selectLabel(label)} > - <div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div> + <div title={label.label} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label}</div> {value.includes(label.name) && <Check className='shrink-0 w-4 h-4 text-primary-600' />} </div> ))} diff --git a/web/app/components/tools/labels/selector.tsx b/web/app/components/tools/labels/selector.tsx index 3f33e45b91..88b910e87c 100644 --- a/web/app/components/tools/labels/selector.tsx +++ b/web/app/components/tools/labels/selector.tsx @@ -1,10 +1,8 @@ import type { FC } from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { useDebounceFn, useMount } from 'ahooks' +import { useDebounceFn } from 'ahooks' import { RiArrowDownSLine } from '@remixicon/react' -import { useStore as useLabelStore } from './store' import cn from '@/utils/classnames' import { PortalToFollowElem, @@ -15,9 +13,7 @@ import Input from '@/app/components/base/input' import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import Checkbox from '@/app/components/base/checkbox' import type { Label } from '@/app/components/tools/labels/constant' -import { fetchLabelList } from '@/service/tools' -import I18n from '@/context/i18n' -import { getLanguage } from '@/i18n/language' +import { useTags } from '@/app/components/plugins/hooks' type LabelSelectorProps = { value: string[] @@ -28,12 +24,9 @@ const LabelSelector: FC<LabelSelectorProps> = ({ onChange, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) - const language = getLanguage(locale) const [open, setOpen] = useState(false) - const labelList = useLabelStore(s => s.labelList) - const setLabelList = useLabelStore(s => s.setLabelList) + const { tags: labelList } = useTags() const [keywords, setKeywords] = useState('') const [searchKeywords, setSearchKeywords] = useState('') @@ -50,8 +43,8 @@ const LabelSelector: FC<LabelSelectorProps> = ({ }, [labelList, searchKeywords]) const selectedLabels = useMemo(() => { - return value.map(v => labelList.find(l => l.name === v)?.label[language]).join(', ') - }, [value, labelList, language]) + return value.map(v => labelList.find(l => l.name === v)?.label).join(', ') + }, [value, labelList]) const selectLabel = (label: Label) => { if (value.includes(label.name)) @@ -60,12 +53,6 @@ const LabelSelector: FC<LabelSelectorProps> = ({ onChange([...value, label.name]) } - useMount(() => { - fetchLabelList().then((res) => { - setLabelList(res) - }) - }) - return ( <PortalToFollowElem open={open} @@ -114,7 +101,7 @@ const LabelSelector: FC<LabelSelectorProps> = ({ checked={value.includes(label.name)} onCheck={() => { }} /> - <div title={label.label[language]} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label[language]}</div> + <div title={label.label} className='grow text-sm text-gray-700 leading-5 truncate'>{label.label}</div> </div> ))} {!filteredLabelList.length && ( diff --git a/web/app/components/tools/labels/store.ts b/web/app/components/tools/labels/store.ts deleted file mode 100644 index c19991dfd4..0000000000 --- a/web/app/components/tools/labels/store.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from 'zustand' -import type { Label } from './constant' - -type State = { - labelList: Label[] -} - -type Action = { - setLabelList: (labelList?: Label[]) => void -} - -export const useStore = create<State & Action>(set => ({ - labelList: [], - setLabelList: labelList => set(() => ({ labelList })), -})) diff --git a/web/app/components/tools/marketplace/hooks.ts b/web/app/components/tools/marketplace/hooks.ts index d1558e7aaf..82f019ef14 100644 --- a/web/app/components/tools/marketplace/hooks.ts +++ b/web/app/components/tools/marketplace/hooks.ts @@ -37,7 +37,7 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin } else { queryMarketplaceCollectionsAndPlugins() - setPlugins([]) + setPlugins(undefined) } }, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, setPlugins]) diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx index fff22fedc5..f2092227a0 100644 --- a/web/app/components/tools/marketplace/index.tsx +++ b/web/app/components/tools/marketplace/index.tsx @@ -1,7 +1,9 @@ import { RiArrowUpDoubleLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' import { useMarketplace } from './hooks' import List from '@/app/components/plugins/marketplace/list' import Loading from '@/app/components/base/loading' +import { getLocaleOnClient } from '@/i18n' type MarketplaceProps = { searchPluginText: string @@ -13,6 +15,8 @@ const Marketplace = ({ filterPluginTags, onMarketplaceScroll, }: MarketplaceProps) => { + const locale = getLocaleOnClient() + const { t } = useTranslation() const { isLoading, marketplaceCollections, @@ -31,19 +35,19 @@ const Marketplace = ({ <div className='flex items-center text-center body-md-regular text-text-tertiary'> Discover <span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected"> - models + {t('plugin.category.models')} </span> , <span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected"> - tools + {t('plugin.category.tools')} </span> , <span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected"> - extensions + {t('plugin.category.extensions')} </span> and <span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected"> - bundles + {t('plugin.category.bundles')} </span> in Dify Marketplace </div> @@ -62,6 +66,7 @@ const Marketplace = ({ marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}} plugins={plugins} showInstallButton + locale={locale} /> ) } diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index 89f3cce3b7..96f36a2095 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -293,21 +293,23 @@ const ProviderDetail = ({ <div className='pt-3'> {isDetailLoading && <div className='flex h-[200px]'><Loading type='app' /></div>} {/* Builtin type */} - {!isDetailLoading && (collection.type === CollectionType.builtIn) && needAuth && isAuthed && ( + {!isDetailLoading && (collection.type === CollectionType.builtIn) && isAuthed && ( <div className='mb-1 h-6 flex items-center justify-between text-text-secondary system-sm-semibold-uppercase'> {t('plugin.detailPanel.actionNum', { num: 3 })} - <Button - variant='secondary' - size='small' - onClick={() => { - if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model) - showSettingAuthModal() - }} - disabled={!isCurrentWorkspaceManager} - > - <Indicator className='mr-2' color={'green'} /> - {t('tools.auth.authorized')} - </Button> + {needAuth && ( + <Button + variant='secondary' + size='small' + onClick={() => { + if (collection.type === CollectionType.builtIn || collection.type === CollectionType.model) + showSettingAuthModal() + }} + disabled={!isCurrentWorkspaceManager} + > + <Indicator className='mr-2' color={'green'} /> + {t('tools.auth.authorized')} + </Button> + )} </div> )} {!isDetailLoading && (collection.type === CollectionType.builtIn) && needAuth && !isAuthed && ( diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index db80492ace..43d887a4d5 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -1,4 +1,5 @@ import { + useEffect, useMemo, useRef, useState, @@ -14,9 +15,10 @@ import ViewTypeSelect, { ViewType } from './view-type-select' import cn from '@/utils/classnames' import { useGetLanguage } from '@/context/i18n' import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list' -import { extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock' import ActionButton from '../../base/action-button' import { RiAddLine } from '@remixicon/react' +import { PluginType } from '../../plugins/types' +import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' type AllToolsProps = { className?: string @@ -26,6 +28,8 @@ type AllToolsProps = { workflowTools: ToolWithProvider[] onSelect: OnSelectBlock supportAddCustomTool?: boolean + onAddedCustomTool?: () => void + onShowAddCustomCollectionModal?: () => void } const AllTools = ({ className, @@ -35,6 +39,7 @@ const AllTools = ({ workflowTools, customTools, supportAddCustomTool, + onShowAddCustomCollectionModal, }: AllToolsProps) => { const language = useGetLanguage() const tabs = useToolTabs() @@ -59,6 +64,21 @@ const AllTools = ({ }) }, [activeTab, buildInTools, customTools, workflowTools, searchText, language]) + const { + queryPluginsWithDebounced: fetchPlugins, + plugins: notInstalledPlugins = [], + } = useMarketplacePlugins() + + useEffect(() => { + if (searchText) { + fetchPlugins({ + query: searchText, + category: PluginType.tool, + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchText]) + const pluginRef = useRef(null) const wrapElemRef = useRef<HTMLDivElement>(null) @@ -84,9 +104,15 @@ const AllTools = ({ </div> <ViewTypeSelect viewType={activeView} onChange={setActiveView} /> {supportAddCustomTool && ( - <ActionButton> - <RiAddLine className='w-4 h-4' /> - </ActionButton> + <div className='flex items-center'> + <div className='mr-1.5 w-px h-3.5 bg-divider-regular'></div> + <ActionButton + className='bg-components-button-primary-bg hover:bg-components-button-primary-bg text-components-button-primary-text hover:text-components-button-primary-text' + onClick={onShowAddCustomCollectionModal} + > + <RiAddLine className='w-4 h-4' /> + </ActionButton> + </div> )} </div> <div @@ -103,7 +129,7 @@ const AllTools = ({ {/* Plugins from marketplace */} <PluginList wrapElemRef={wrapElemRef} - list={[toolNotion, extensionDallE, modelGPT4] as any} ref={pluginRef} + list={notInstalledPlugins as any} ref={pluginRef} searchText={searchText} /> </div> diff --git a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx index b5a73b9743..d257533d62 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/item.tsx @@ -26,6 +26,8 @@ const Item: FC<Props> = ({ const { t } = useTranslation() const [open, setOpen] = React.useState(false) const { locale } = useContext(I18n) + const getLocalizedText = (obj: Record<string, string> | undefined) => + obj?.[locale] || obj?.['en-US'] || obj?.en_US || '' return ( <div className='group/plugin flex rounded-lg py-1 pr-1 pl-3 hover:bg-state-base-hover'> @@ -35,8 +37,8 @@ const Item: FC<Props> = ({ /> <div className='ml-2 w-0 grow flex'> <div className='w-0 grow'> - <div className='h-4 leading-4 text-text-primary system-sm-medium truncate '>{payload.label[locale]}</div> - <div className='h-5 leading-5 text-text-tertiary system-xs-regular truncate'>{payload.brief[locale]}</div> + <div className='h-4 leading-4 text-text-primary system-sm-medium truncate '>{getLocalizedText(payload.label)}</div> + <div className='h-5 leading-5 text-text-tertiary system-xs-regular truncate'>{getLocalizedText(payload.brief)}</div> <div className='flex text-text-tertiary system-xs-regular space-x-1'> <div>{payload.org}</div> <div>·</div> diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index 3da4e04af5..6c82bd5c0c 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -1,5 +1,5 @@ 'use client' -import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' +import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import useStickyScroll, { ScrollPosition } from '../use-sticky-scroll' import Item from './item' @@ -30,7 +30,6 @@ const List = ({ wrapElemRef, nextToStickyELemRef, }) - const stickyClassName = useMemo(() => { switch (scrollPosition) { case ScrollPosition.aboveTheWrap: @@ -38,7 +37,7 @@ const List = ({ case ScrollPosition.showing: return 'bottom-0 pt-3 pb-1' case ScrollPosition.belowTheWrap: - return 'bottom-0 items-center rounded-b-xl border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg cursor-pointer' + return 'bottom-0 items-center rounded-b-xl border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg rounded-b-lg cursor-pointer' } }, [scrollPosition]) @@ -46,6 +45,11 @@ const List = ({ handleScroll, })) + useEffect(() => { + handleScroll() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [list]) + const handleHeadClick = () => { if (scrollPosition === ScrollPosition.belowTheWrap) { nextToStickyELemRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }) @@ -57,7 +61,7 @@ const List = ({ if (hasSearchText) { return ( <Link - className='sticky bottom-0 z-10 flex h-8 px-4 py-1 system-sm-medium items-center rounded-b-xl border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg text-text-accent-light-mode-only cursor-pointer' + className='sticky bottom-0 z-10 flex h-8 px-4 py-1 system-sm-medium items-center border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur rounded-b-lg shadow-lg text-text-accent-light-mode-only cursor-pointer' href={`${marketplaceUrlPrefix}/plugins`} target='_blank' > diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index 9ae8d625f9..88e88018f1 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import React from 'react' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { PortalToFollowElem, PortalToFollowElemContent, @@ -13,13 +13,17 @@ import type { } from '@floating-ui/react' import AllTools from '@/app/components/workflow/block-selector/all-tools' import type { ToolDefaultValue } from './types' -import { - fetchAllBuiltInTools, - fetchAllCustomTools, - fetchAllWorkflowTools, -} from '@/service/tools' -import type { BlockEnum, ToolWithProvider } from '@/app/components/workflow/types' +import type { BlockEnum } from '@/app/components/workflow/types' import SearchBox from '@/app/components/plugins/marketplace/search-box' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal/modal' +import { + createCustomCollection, +} from '@/service/tools' +import type { CustomCollectionBackend } from '@/app/components/tools/types' +import Toast from '@/app/components/base/toast' +import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' type Props = { disabled: boolean @@ -42,22 +46,15 @@ const ToolPicker: FC<Props> = ({ onSelect, supportAddCustomTool, }) => { + const { t } = useTranslation() const [searchText, setSearchText] = useState('') - const [buildInTools, setBuildInTools] = useState<ToolWithProvider[]>([]) - const [customTools, setCustomTools] = useState<ToolWithProvider[]>([]) - const [workflowTools, setWorkflowTools] = useState<ToolWithProvider[]>([]) + const { data: buildInTools } = useAllBuiltInTools() + const { data: customTools } = useAllCustomTools() + const invalidateCustomTools = useInvalidateAllCustomTools() + const { data: workflowTools } = useAllWorkflowTools() - useEffect(() => { - (async () => { - const buildInTools = await fetchAllBuiltInTools() - const customTools = await fetchAllCustomTools() - const workflowTools = await fetchAllWorkflowTools() - setBuildInTools(buildInTools) - setCustomTools(customTools) - setWorkflowTools(workflowTools) - })() - }, []) + const handleAddedCustomTool = invalidateCustomTools const handleTriggerClick = () => { if (disabled) return @@ -68,6 +65,32 @@ const ToolPicker: FC<Props> = ({ onSelect(tool!) } + const [isShowEditCollectionToolModal, { + setFalse: hideEditCustomCollectionModal, + setTrue: showEditCustomCollectionModal, + }] = useBoolean(false) + + const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { + await createCustomCollection(data) + Toast.notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + hideEditCustomCollectionModal() + handleAddedCustomTool() + } + + if (isShowEditCollectionToolModal) { + return ( + <EditCustomToolModal + positionLeft + payload={null} + onHide={hideEditCustomCollectionModal} + onAdd={doCreateCustomToolCollection} + /> + ) + } + return ( <PortalToFollowElem placement={placement} @@ -76,30 +99,34 @@ const ToolPicker: FC<Props> = ({ onOpenChange={onShowChange} > <PortalToFollowElemTrigger - asChild onClick={handleTriggerClick} > {trigger} </PortalToFollowElemTrigger> <PortalToFollowElemContent className='z-[1000]'> - <div className="relative w-[320px] min-h-20 bg-white"> - <SearchBox - search={searchText} - onSearchChange={setSearchText} - tags={[]} - onTagsChange={() => {}} - size='small' - placeholder='Search tools...' - /> + { } + <div className="relative w-[320px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg"> + <div className='p-2 pb-1'> + <SearchBox + search={searchText} + onSearchChange={setSearchText} + tags={[]} + onTagsChange={() => { }} + size='small' + placeholder={t('plugin.searchTools')!} + /> + </div> <AllTools className='mt-1' searchText={searchText} onSelect={handleSelect} - buildInTools={buildInTools} - customTools={customTools} - workflowTools={workflowTools} + buildInTools={buildInTools || []} + customTools={customTools || []} + workflowTools={workflowTools || []} supportAddCustomTool={supportAddCustomTool} + onAddedCustomTool={handleAddedCustomTool} + onShowAddCustomCollectionModal={showEditCustomCollectionModal} /> </div> </PortalToFollowElemContent> diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index c233200b05..e33f625861 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -10,14 +10,12 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' type Props = { - className?: string provider: ToolWithProvider payload: Tool onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void } const ToolItem: FC<Props> = ({ - className, provider, payload, onSelect, @@ -46,6 +44,12 @@ const ToolItem: FC<Props> = ({ key={payload.name} className='rounded-lg pl-[21px] hover:bg-state-base-hover cursor-pointer' onClick={() => { + const params: Record<string, string> = {} + if (payload.parameters) { + payload.parameters.forEach((item) => { + params[item.name] = '' + }) + } onSelect(BlockEnum.Tool, { provider_id: provider.id, provider_type: provider.type, @@ -53,6 +57,7 @@ const ToolItem: FC<Props> = ({ tool_name: payload.name, tool_label: payload.label[language], title: payload.label[language], + params, }) }} > diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index 679f0b0e2e..f7433b8e60 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -66,6 +66,12 @@ const Tool: FC<Props> = ({ toggleFold() return } + // TODO: get workflow and custom tool params + // if (payload.parameters) { + // payload.parameters.forEach((item) => { + // params[item.name] = '' + // }) + // } onSelect(BlockEnum.Tool, { provider_id: payload.id, provider_type: payload.type, @@ -73,6 +79,7 @@ const Tool: FC<Props> = ({ tool_name: payload.name, tool_label: payload.label[language], title: payload.label[language], + params: {}, }) }} > diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index affa2488b9..9bdbf5cb3c 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -25,4 +25,5 @@ export type ToolDefaultValue = { tool_name: string tool_label: string title: string + params: Record<string, any> } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 3c27b5c91b..03ad421572 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -481,7 +481,6 @@ export const useWorkflowInit = () => { return acc }, {} as Record<string, string>), environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], - // #TODO chatVar sync# conversationVariables: res.conversation_variables || [], }) setSyncWorkflowDraftHash(res.hash) diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 255b8fea3b..9b6ad033bf 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -72,7 +72,7 @@ export type CommonNodeType<T = {}> = { height?: number } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>> -export interface CommonEdgeType { +export type CommonEdgeType = { _hovering?: boolean _connectedNodeIsHovering?: boolean _connectedNodeIsSelected?: boolean @@ -86,14 +86,14 @@ export interface CommonEdgeType { export type Node<T = {}> = ReactFlowNode<CommonNodeType<T>> export type SelectedNode = Pick<Node, 'id' | 'data'> -export interface NodeProps<T = unknown> { id: string; data: CommonNodeType<T> } -export interface NodePanelProps<T> { +export type NodeProps<T = unknown> = { id: string; data: CommonNodeType<T> } +export type NodePanelProps<T> = { id: string data: CommonNodeType<T> } export type Edge = ReactFlowEdge<CommonEdgeType> -export interface WorkflowDataUpdater { +export type WorkflowDataUpdater = { nodes: Node[] edges: Edge[] viewport: Viewport @@ -101,7 +101,7 @@ export interface WorkflowDataUpdater { export type ValueSelector = string[] // [nodeId, key | obj key path] -export interface Variable { +export type Variable = { variable: string label?: string | { nodeType: BlockEnum @@ -116,14 +116,14 @@ export interface Variable { isParagraph?: boolean } -export interface EnvironmentVariable { +export type EnvironmentVariable = { id: string name: string value: any value_type: 'string' | 'number' | 'secret' } -export interface ConversationVariable { +export type ConversationVariable = { id: string name: string value_type: ChatVarType @@ -131,13 +131,13 @@ export interface ConversationVariable { description: string } -export interface GlobalVariable { +export type GlobalVariable = { name: string value_type: 'string' | 'number' description: string } -export interface VariableWithValue { +export type VariableWithValue = { key: string value: string } @@ -173,7 +173,7 @@ export type InputVar = { value_selector?: ValueSelector } & Partial<UploadFileSetting> -export interface ModelConfig { +export type ModelConfig = { provider: string name: string mode: string @@ -191,7 +191,7 @@ export enum EditionType { jinja2 = 'jinja2', } -export interface PromptItem { +export type PromptItem = { id?: string role?: PromptRole text: string @@ -204,12 +204,12 @@ export enum MemoryRole { assistant = 'assistant', } -export interface RolePrefix { +export type RolePrefix = { user: string assistant: string } -export interface Memory { +export type Memory = { role_prefix?: RolePrefix window: { enabled: boolean @@ -233,7 +233,7 @@ export enum VarType { any = 'any', } -export interface Var { +export type Var = { variable: string type: VarType children?: Var[] // if type is obj, has the children struct @@ -244,21 +244,21 @@ export interface Var { des?: string } -export interface NodeOutPutVar { +export type NodeOutPutVar = { nodeId: string title: string vars: Var[] isStartNode?: boolean } -export interface Block { +export type Block = { classification?: string type: BlockEnum title: string description?: string } -export interface NodeDefault<T> { +export type NodeDefault<T> = { defaultValue: Partial<T> getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[] getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[] @@ -298,19 +298,19 @@ export type OnNodeAdd = ( } ) => void -export interface CheckValidRes { +export type CheckValidRes = { isValid: boolean errorMessage?: string } -export interface RunFile { +export type RunFile = { type: string transfer_method: TransferMethod[] url?: string upload_file_id?: string } -export interface WorkflowRunningData { +export type WorkflowRunningData = { task_id?: string message_id?: string conversation_id?: string @@ -335,7 +335,7 @@ export interface WorkflowRunningData { tracing?: NodeTracing[] } -export interface HistoryWorkflowData { +export type HistoryWorkflowData = { id: string sequence_number: number status: string @@ -347,7 +347,7 @@ export enum ChangeType { remove = 'remove', } -export interface MoreInfo { +export type MoreInfo = { type: ChangeType payload?: { beforeKey: string @@ -367,7 +367,7 @@ export enum SupportUploadFileTypes { custom = 'custom', } -export interface UploadFileSetting { +export type UploadFileSetting = { allowed_file_upload_methods: TransferMethod[] allowed_file_types: SupportUploadFileTypes[] allowed_file_extensions?: string[] @@ -375,7 +375,7 @@ export interface UploadFileSetting { number_limits?: number } -export interface VisionSetting { +export type VisionSetting = { variable_selector: ValueSelector detail: Resolution } diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index aaf333f4d7..f7d15dae21 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -35,6 +35,7 @@ import type { ToolNodeType } from './nodes/tool/types' import type { IterationNodeType } from './nodes/iteration/types' import { CollectionType } from '@/app/components/tools/types' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import { correctProvider } from '@/utils' const WHITE = 'WHITE' const GRAY = 'GRAY' @@ -275,6 +276,19 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated } + // legacy provider handle + if (node.data.type === BlockEnum.LLM) + (node as any).data.model.provider = correctProvider((node as any).data.model.provider) + + if (node.data.type === BlockEnum.KnowledgeRetrieval && (node as any).data.multiple_retrieval_config.reranking_model) + (node as any).data.multiple_retrieval_config.reranking_model.provider = correctProvider((node as any).data.multiple_retrieval_config.reranking_model.provider) + + if (node.data.type === BlockEnum.QuestionClassifier) + (node as any).data.model.provider = correctProvider((node as any).data.model.provider) + + if (node.data.type === BlockEnum.ParameterExtractor) + (node as any).data.model.provider = correctProvider((node as any).data.model.provider) + return node }) } @@ -428,7 +442,7 @@ export const genNewNodeTitleFromOld = (oldTitle: string) => { if (match) { const title = match[1] - const num = parseInt(match[2], 10) + const num = Number.parseInt(match[2], 10) return `${title} (${num + 1})` } else { diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 0fc56c4509..8fa7f92851 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -45,6 +45,7 @@ const LocaleLayout = ({ data-public-maintenance-notice={process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE} data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT} data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS} + data-public-github-access-token={process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN} > <BrowserInitor> <SentryInitor> diff --git a/web/config/index.ts b/web/config/index.ts index 85cb850393..1de973f9a2 100644 --- a/web/config/index.ts +++ b/web/config/index.ts @@ -271,3 +271,5 @@ else if (globalThis.document?.body?.getAttribute('data-public-text-generation-ti export const TEXT_GENERATION_TIMEOUT_MS = textGenerationTimeoutMs export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true' + +export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || globalThis.document?.body?.getAttribute('data-public-github-access-token') || '' diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 60d53f1e98..622077ee91 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -31,8 +31,10 @@ import ModelLoadBalancingModal from '@/app/components/header/account-setting/mod import OpeningSettingModal from '@/app/components/base/features/new-feature-panel/conversation-opener/modal' 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' -export interface ModalState<T> { +export type ModalState<T> = { payload: T onCancelCallback?: () => void onSaveCallback?: (newPayload: T) => void @@ -43,7 +45,7 @@ export interface ModalState<T> { datasetBindings?: { id: string; name: string }[] } -export interface ModelModalType { +export type ModelModalType = { currentProvider: ModelProvider currentConfigurationMethod: ConfigurationMethodEnum currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields @@ -52,7 +54,8 @@ export type LoadBalancingEntryModalType = ModelModalType & { entry?: ModelLoadBalancingConfigEntry index?: number } -export interface ModalContextState { + +export type ModalContextState = { setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<string> | null>> setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>> setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>> @@ -68,6 +71,7 @@ export interface ModalContextState { workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void }> | null>> + setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>> } const ModalContext = createContext<ModalContextState>({ setShowAccountSettingModal: () => { }, @@ -81,6 +85,7 @@ const ModalContext = createContext<ModalContextState>({ setShowModelLoadBalancingModal: () => { }, setShowModelLoadBalancingEntryModal: () => { }, setShowOpeningModal: () => { }, + setShowUpdatePluginModal: () => { }, }) export const useModalContext = () => useContext(ModalContext) @@ -90,7 +95,7 @@ export const useModalContext = () => useContext(ModalContext) export const useModalContextSelector = <T,>(selector: (state: ModalContextState) => T): T => useContextSelector(ModalContext, selector) -interface ModalContextProviderProps { +type ModalContextProviderProps = { children: React.ReactNode } export const ModalContextProvider = ({ @@ -109,6 +114,8 @@ export const ModalContextProvider = ({ workflowVariables?: InputVar[] onAutoAddPromptVariable?: (variable: PromptVariable[]) => void }> | null>(null) + const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null) + const searchParams = useSearchParams() const router = useRouter() const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1') @@ -228,6 +235,7 @@ export const ModalContextProvider = ({ setShowModelLoadBalancingModal, setShowModelLoadBalancingEntryModal, setShowOpeningModal, + setShowUpdatePluginModal, }}> <> {children} @@ -338,6 +346,22 @@ export const ModalContextProvider = ({ onAutoAddPromptVariable={showOpeningModal.payload.onAutoAddPromptVariable} /> )} + + { + !!showUpdatePluginModal && ( + <UpdatePlugin + {...showUpdatePluginModal.payload} + onCancel={() => { + setShowUpdatePluginModal(null) + showUpdatePluginModal.onCancelCallback?.() + }} + onSave={() => { + setShowUpdatePluginModal(null) + showUpdatePluginModal.onSaveCallback?.({} as any) + }} + /> + ) + } </> </ModalContext.Provider> ) diff --git a/web/context/query-client.tsx b/web/context/query-client.tsx new file mode 100644 index 0000000000..1adb8af653 --- /dev/null +++ b/web/context/query-client.tsx @@ -0,0 +1,15 @@ +'use client' + +import type { FC, PropsWithChildren } from 'react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const client = new QueryClient() + +export const TanstackQueryIniter: FC<PropsWithChildren> = (props) => { + const { children } = props + return <QueryClientProvider client={client}> + {children} + <ReactQueryDevtools initialIsOpen={false} /> + </QueryClientProvider> +} diff --git a/web/i18n/de-DE/plugin-tags.ts b/web/i18n/de-DE/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/de-DE/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/en-US/plugin-tags.ts b/web/i18n/en-US/plugin-tags.ts new file mode 100644 index 0000000000..e96f415053 --- /dev/null +++ b/web/i18n/en-US/plugin-tags.ts @@ -0,0 +1,24 @@ +const translation = { + allTags: 'All Tags', + searchTags: 'Search Tags', + tags: { + search: 'Search', + image: 'Image', + videos: 'Videos', + weather: 'Weather', + finance: 'Finance', + design: 'Design', + travel: 'Travel', + social: 'Social', + news: 'News', + medical: 'Medical', + productivity: 'Productivity', + education: 'Education', + business: 'Business', + entertainment: 'Entertainment', + utilities: 'Utilities', + other: 'Other', + }, +} + +export default translation diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 0801ee21b6..6d6e0d57c0 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -1,9 +1,17 @@ const translation = { + category: { + models: 'models', + tools: 'tools', + extensions: 'extensions', + bundles: 'bundles', + }, + searchPlugins: 'Search plugins', from: 'From', findMoreInMarketplace: 'Find more in Marketplace', searchInMarketplace: 'Search in Marketplace', fromMarketplace: 'From Marketplace', endpointsEnabled: '{{num}} sets of endpoints enabled', + searchTools: 'Search tools...', detailPanel: { categoryTip: { marketplace: 'Installed from Marketplace', @@ -85,6 +93,10 @@ const translation = { }, installFromGitHub: { installPlugin: 'Install plugin from GitHub', + updatePlugin: 'Update plugin from GitHub', + installedSuccessfully: 'Installation successful', + installFailed: 'Installation failed', + uploadFailed: 'Upload failed', gitHubRepo: 'GitHub repository', selectVersion: 'Select version', selectVersionPlaceholder: 'Please select a version', diff --git a/web/i18n/es-ES/plugin-tags.ts b/web/i18n/es-ES/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/es-ES/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/fa-IR/plugin-tags.ts b/web/i18n/fa-IR/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/fa-IR/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/fr-FR/plugin-tags.ts b/web/i18n/fr-FR/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/fr-FR/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/hi-IN/plugin-tags.ts b/web/i18n/hi-IN/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/hi-IN/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/i18next-config.ts b/web/i18n/i18next-config.ts index be8b4c46e1..bbba4c7c35 100644 --- a/web/i18n/i18next-config.ts +++ b/web/i18n/i18next-config.ts @@ -29,6 +29,7 @@ const loadLangResources = (lang: string) => ({ workflow: require(`./${lang}/workflow`).default, runLog: require(`./${lang}/run-log`).default, plugin: require(`./${lang}/plugin`).default, + pluginTags: require(`./${lang}/plugin-tags`).default, }, }) diff --git a/web/i18n/it-IT/plugin-tags.ts b/web/i18n/it-IT/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/it-IT/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/ja-JP/plugin-tags.ts b/web/i18n/ja-JP/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/ja-JP/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/ko-KR/plugin-tags.ts b/web/i18n/ko-KR/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/ko-KR/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/pl-PL/plugin-tags.ts b/web/i18n/pl-PL/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/pl-PL/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/pt-BR/plugin-tags.ts b/web/i18n/pt-BR/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/pt-BR/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/ro-RO/plugin-tags.ts b/web/i18n/ro-RO/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/ro-RO/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/ru-RU/plugin-tags.ts b/web/i18n/ru-RU/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/ru-RU/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/tr-TR/plugin-tags.ts b/web/i18n/tr-TR/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/tr-TR/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/uk-UA/plugin-tags.ts b/web/i18n/uk-UA/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/uk-UA/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/vi-VN/plugin-tags.ts b/web/i18n/vi-VN/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/vi-VN/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/i18n/zh-Hans/plugin-tags.ts b/web/i18n/zh-Hans/plugin-tags.ts new file mode 100644 index 0000000000..4c9b2c6370 --- /dev/null +++ b/web/i18n/zh-Hans/plugin-tags.ts @@ -0,0 +1,24 @@ +const translation = { + allTags: '所有标签', + searchTags: '搜索标签', + tags: { + search: '搜索', + image: '图片', + videos: '视频', + weather: '天气', + finance: '金融', + design: '设计', + travel: '旅行', + social: '社交', + news: '新闻', + medical: '医疗', + productivity: '生产力', + education: '教育', + business: '商业', + entertainment: '娱乐', + utilities: '工具', + other: '其他', + }, +} + +export default translation diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index a8b8b2dafa..8b13e63aee 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -1,9 +1,17 @@ const translation = { + category: { + models: '模型', + tools: '工具', + extensions: '扩展', + bundles: '捆绑包', + }, + searchPlugins: '搜索插件', from: '来自', findMoreInMarketplace: '在 Marketplace 中查找更多', searchInMarketplace: '在 Marketplace 中搜索', fromMarketplace: '来自市场', endpointsEnabled: '{{num}} 组端点已启用', + searchTools: '搜索工具...', detailPanel: { categoryTip: { marketplace: '从 Marketplace 安装', @@ -85,6 +93,10 @@ const translation = { }, installFromGitHub: { installPlugin: '从 GitHub 安装插件', + updatePlugin: '更新来自 GitHub 的插件', + installedSuccessfully: '安装成功', + installFailed: '安装失败', + uploadFailed: '上传失败', gitHubRepo: 'GitHub 仓库', selectVersion: '选择版本', selectVersionPlaceholder: '请选择一个版本', diff --git a/web/i18n/zh-Hant/plugin-tags.ts b/web/i18n/zh-Hant/plugin-tags.ts new file mode 100644 index 0000000000..928649474b --- /dev/null +++ b/web/i18n/zh-Hant/plugin-tags.ts @@ -0,0 +1,4 @@ +const translation = { +} + +export default translation diff --git a/web/package.json b/web/package.json index bec9504fe6..8d69bbc209 100644 --- a/web/package.json +++ b/web/package.json @@ -27,22 +27,25 @@ "@babel/runtime": "^7.22.3", "@dagrejs/dagre": "^1.1.4", "@emoji-mart/data": "^1.2.1", - "@floating-ui/react": "^0.25.2", + "@floating-ui/react": "^0.26.25", "@formatjs/intl-localematcher": "^0.5.6", "@headlessui/react": "^1.7.13", "@heroicons/react": "^2.0.16", - "@hookform/resolvers": "^3.3.4", + "@hookform/resolvers": "^3.9.0", "@lexical/react": "^0.18.0", - "@mdx-js/loader": "^2.3.0", - "@mdx-js/react": "^2.3.0", + "@mdx-js/loader": "^3.1.0", + "@mdx-js/react": "^3.1.0", "@monaco-editor/react": "^4.6.0", "@next/mdx": "^14.0.4", + "@octokit/core": "^6.1.2", "@remixicon/react": "^4.3.0", "@sentry/react": "^7.54.0", "@sentry/utils": "^7.54.0", "@svgdotjs/svg.js": "^3.2.4", - "@tailwindcss/line-clamp": "^0.4.4", - "@tailwindcss/typography": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", + "@tanstack/react-query": "^5.59.20", + "@tanstack/react-query-devtools": "^5.59.20", + "@types/hast": "^3.0.4", "ahooks": "^3.8.1", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", @@ -60,7 +63,7 @@ "js-audio-recorder": "^1.0.7", "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", - "katex": "^0.16.10", + "katex": "^0.16.11", "lamejs": "^1.2.1", "lexical": "^0.18.0", "lodash-es": "^4.17.21", @@ -78,12 +81,12 @@ "react-easy-crop": "^5.1.0", "react-error-boundary": "^4.1.2", "react-headless-pagination": "^1.1.6", - "react-hook-form": "^7.51.4", + "react-hook-form": "^7.53.1", "react-i18next": "^15.1.0", "react-infinite-scroll-component": "^6.1.0", - "react-markdown": "^8.0.6", "react-multi-email": "^1.0.25", "react-papaparse": "^4.4.0", + "react-markdown": "^9.0.1", "react-slider": "^2.0.6", "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^15.6.1", @@ -92,20 +95,21 @@ "react-window-infinite-loader": "^1.0.9", "reactflow": "^11.11.3", "recordrtc": "^5.6.2", - "rehype-katex": "^6.0.2", + "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", - "remark-breaks": "^3.0.2", - "remark-gfm": "^3.0.1", - "remark-math": "^5.1.1", + "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", "scheduler": "^0.23.0", + "semver": "^7.6.3", "server-only": "^0.0.1", "sharp": "^0.33.5", "sortablejs": "^1.15.3", "swr": "^2.1.0", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.4", "use-context-selector": "^2.0.0", "uuid": "^10.0.0", - "zod": "^3.23.6", + "zod": "^3.23.8", "zundo": "^2.1.0", "zustand": "^4.5.2" }, @@ -144,26 +148,27 @@ "@types/react-window": "^1.8.8", "@types/react-window-infinite-loader": "^1.0.9", "@types/recordrtc": "^5.6.14", + "@types/semver": "^7.5.8", "@types/sortablejs": "^1.15.1", "@types/uuid": "^10.0.0", - "autoprefixer": "^10.4.14", + "autoprefixer": "^10.4.20", "bing-translate-api": "^4.0.2", "code-inspector-plugin": "^0.17.4", "cross-env": "^7.0.3", "eslint": "^9.13.0", "eslint-config-next": "^15.0.0", "eslint-plugin-react-hooks": "^5.0.0", - "husky": "^9.1.6", "eslint-plugin-react-refresh": "^0.4.13", "eslint-plugin-storybook": "^0.10.1", + "husky": "^9.1.6", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.10", "magicast": "^0.3.4", - "postcss": "^8.4.31", + "postcss": "^8.4.47", "sass": "^1.80.3", "storybook": "^8.3.6", - "tailwindcss": "^3.4.4", + "tailwindcss": "^3.4.14", "ts-node": "^10.9.2", "typescript": "4.9.5", "uglify-js": "^3.19.3" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index b76da7ca7c..1a98266fdc 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 '@floating-ui/react': - specifier: ^0.25.2 - version: 0.25.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^0.26.25 + version: 0.26.27(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@formatjs/intl-localematcher': specifier: ^0.5.6 version: 0.5.6 @@ -35,23 +35,26 @@ importers: specifier: ^2.0.16 version: 2.1.5(react@18.2.0) '@hookform/resolvers': - specifier: ^3.3.4 + specifier: ^3.9.0 version: 3.9.0(react-hook-form@7.53.1(react@18.2.0)) '@lexical/react': specifier: ^0.18.0 version: 0.18.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(yjs@13.6.20) '@mdx-js/loader': - specifier: ^2.3.0 - version: 2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)) + specifier: ^3.1.0 + version: 3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)) '@mdx-js/react': - specifier: ^2.3.0 - version: 2.3.0(react@18.2.0) + specifier: ^3.1.0 + version: 3.1.0(@types/react@18.2.79)(react@18.2.0) '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.52.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@next/mdx': specifier: ^14.0.4 - version: 14.2.15(@mdx-js/loader@2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)))(@mdx-js/react@2.3.0(react@18.2.0)) + version: 14.2.15(@mdx-js/loader@3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@18.2.79)(react@18.2.0)) + '@octokit/core': + specifier: ^6.1.2 + version: 6.1.2 '@remixicon/react': specifier: ^4.3.0 version: 4.3.0(react@18.2.0) @@ -64,12 +67,18 @@ importers: '@svgdotjs/svg.js': specifier: ^3.2.4 version: 3.2.4 - '@tailwindcss/line-clamp': - specifier: ^0.4.4 - version: 0.4.4(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))) '@tailwindcss/typography': - specifier: ^0.5.9 + specifier: ^0.5.15 version: 0.5.15(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5))) + '@tanstack/react-query': + specifier: ^5.59.20 + version: 5.59.20(react@18.2.0) + '@tanstack/react-query-devtools': + specifier: ^5.59.20 + version: 5.59.20(@tanstack/react-query@5.59.20(react@18.2.0))(react@18.2.0) + '@types/hast': + specifier: ^3.0.4 + version: 3.0.4 ahooks: specifier: ^3.8.1 version: 3.8.1(react@18.2.0) @@ -122,7 +131,7 @@ importers: specifier: ^4.0.0 version: 4.0.0 katex: - specifier: ^0.16.10 + specifier: ^0.16.11 version: 0.16.11 lamejs: specifier: ^1.2.1 @@ -176,7 +185,7 @@ importers: specifier: ^1.1.6 version: 1.1.6(react@18.2.0) react-hook-form: - specifier: ^7.51.4 + specifier: ^7.53.1 version: 7.53.1(react@18.2.0) react-i18next: specifier: ^15.1.0 @@ -185,8 +194,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(react@18.2.0) react-markdown: - specifier: ^8.0.6 - version: 8.0.7(@types/react@18.2.79)(react@18.2.0) + specifier: ^9.0.1 + version: 9.0.1(@types/react@18.2.79)(react@18.2.0) react-multi-email: specifier: ^1.0.25 version: 1.0.25(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -218,23 +227,26 @@ importers: specifier: ^5.6.2 version: 5.6.2 rehype-katex: - specifier: ^6.0.2 - version: 6.0.3 + specifier: ^7.0.1 + version: 7.0.1 rehype-raw: specifier: ^7.0.0 version: 7.0.0 remark-breaks: - specifier: ^3.0.2 - version: 3.0.3 + specifier: ^4.0.0 + version: 4.0.0 remark-gfm: - specifier: ^3.0.1 - version: 3.0.1 + specifier: ^4.0.0 + version: 4.0.0 remark-math: - specifier: ^5.1.1 - version: 5.1.1 + specifier: ^6.0.0 + version: 6.0.0 scheduler: specifier: ^0.23.0 version: 0.23.2 + semver: + specifier: ^7.6.3 + version: 7.6.3 server-only: specifier: ^0.0.1 version: 0.0.1 @@ -248,7 +260,7 @@ importers: specifier: ^2.1.0 version: 2.2.5(react@18.2.0) tailwind-merge: - specifier: ^2.4.0 + specifier: ^2.5.4 version: 2.5.4 use-context-selector: specifier: ^2.0.0 @@ -257,7 +269,7 @@ importers: specifier: ^10.0.0 version: 10.0.0 zod: - specifier: ^3.23.6 + specifier: ^3.23.8 version: 3.23.8 zundo: specifier: ^2.1.0 @@ -368,6 +380,9 @@ importers: '@types/recordrtc': specifier: ^5.6.14 version: 5.6.14 + '@types/semver': + specifier: ^7.5.8 + version: 7.5.8 '@types/sortablejs': specifier: ^1.15.1 version: 1.15.8 @@ -375,7 +390,7 @@ importers: specifier: ^10.0.0 version: 10.0.0 autoprefixer: - specifier: ^10.4.14 + specifier: ^10.4.20 version: 10.4.20(postcss@8.4.47) bing-translate-api: specifier: ^4.0.2 @@ -417,7 +432,7 @@ importers: specifier: ^0.3.4 version: 0.3.5 postcss: - specifier: ^8.4.31 + specifier: ^8.4.47 version: 8.4.47 sass: specifier: ^1.80.3 @@ -426,7 +441,7 @@ importers: specifier: ^8.3.6 version: 8.3.6 tailwindcss: - specifier: ^3.4.4 + specifier: ^3.4.14 version: 3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) ts-node: specifier: ^10.9.2 @@ -1427,15 +1442,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.25.4': - resolution: {integrity: sha512-lWRQ/UiTvSIBxohn0/2HFHEmnmOVRjl7j6XcRJuLH0ls6f/9AyHMWVzkAJFuwx0n9gaEeCmg9VccCSCJzbEJig==} + '@floating-ui/react@0.26.27': + resolution: {integrity: sha512-jLP72x0Kr2CgY6eTYi/ra3VA9LOkTo4C+DUTrbFgFOExKy3omYVmwMjNKqxAHdsnyLS96BIDLcO2SlnsNf8KUQ==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/utils@0.1.6': - resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} - '@floating-ui/utils@0.2.8': resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} @@ -1753,18 +1765,16 @@ packages: peerDependencies: yjs: '>=13.5.22' - '@mdx-js/loader@2.3.0': - resolution: {integrity: sha512-IqsscXh7Q3Rzb+f5DXYk0HU71PK+WuFsEhf+mSV3fOhpLcEpgsHvTQ2h0T6TlZ5gHOaBeFjkXwB52by7ypMyNg==} + '@mdx-js/loader@3.1.0': + resolution: {integrity: sha512-xU/lwKdOyfXtQGqn3VnJjlDrmKXEvMi1mgYxVmukEUtVycIz1nh7oQ40bKTd4cA7rLStqu0740pnhGYxGoqsCg==} peerDependencies: - webpack: '>=4' + webpack: '>=5' + peerDependenciesMeta: + webpack: + optional: true - '@mdx-js/mdx@2.3.0': - resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==} - - '@mdx-js/react@2.3.0': - resolution: {integrity: sha512-zQH//gdOmuu7nt2oJR29vFhDv88oGPmVw6BggmrHeMI+xgEkp1B2dX9/bMBSYtK0dyLX/aOmesKS09g222K1/g==} - peerDependencies: - react: '>=16' + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} '@mdx-js/react@3.1.0': resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} @@ -1871,6 +1881,36 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/request-error@6.1.5': + resolution: {integrity: sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/types@13.6.1': + resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} + '@parcel/watcher-android-arm64@2.4.1': resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} engines: {node: '>= 10.0.0'} @@ -2328,16 +2368,28 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tailwindcss/line-clamp@0.4.4': - resolution: {integrity: sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==} - peerDependencies: - tailwindcss: '>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1' - '@tailwindcss/typography@0.5.15': resolution: {integrity: sha512-AqhlCXl+8grUz8uqExv5OTtgpjuVIwFTSXTrh8y9/pw6q2ek7fJ+Y8ZEVw7EB2DCcuCOtEjf9w3+J3rzts01uA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20' + '@tanstack/query-core@5.59.20': + resolution: {integrity: sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==} + + '@tanstack/query-devtools@5.59.20': + resolution: {integrity: sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==} + + '@tanstack/react-query-devtools@5.59.20': + resolution: {integrity: sha512-AL/eQS1NFZhwwzq2Bq9Gd8wTTH+XhPNOJlDFpzPMu9NC5CQVgA0J8lWrte/sXpdWNo5KA4hgHnEdImZsF4h6Lw==} + peerDependencies: + '@tanstack/react-query': ^5.59.20 + react: ^18 || ^19 + + '@tanstack/react-query@5.59.20': + resolution: {integrity: sha512-Zly0egsK0tFdfSbh5/mapSa+Zfc3Et0Zkar7Wo5sQkFzWyB3p3uZWOHR2wrlAEEV2L953eLuDBtbgFvMYiLvUw==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-virtual@3.10.8': resolution: {integrity: sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==} peerDependencies: @@ -2591,9 +2643,6 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/katex@0.14.0': - resolution: {integrity: sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==} - '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -3158,6 +3207,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -3444,6 +3496,9 @@ packages: code-inspector-plugin@0.17.4: resolution: {integrity: sha512-aIM8wcO0eNoY+tlXXU+xwcTnUN96jmfglWFi1A1Vmqs5gew8k54709a95dJ6wa+gOHD5I3cw+Qh3xtoikHi9KA==} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} @@ -4103,6 +4158,12 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild-code-inspector-plugin@0.17.4: resolution: {integrity: sha512-gqgcEPgtcJyjBVId9av8QaTGlMnX75/aV8iLn4bjRPpOWX9hqSS5jUhHlIJHisptSuWPYeCyvduHEblAcKsHzA==} @@ -4475,20 +4536,23 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} - estree-util-attach-comments@2.1.1: - resolution: {integrity: sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w==} + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} - estree-util-build-jsx@2.2.2: - resolution: {integrity: sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg==} + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} - estree-util-is-identifier-name@2.1.0: - resolution: {integrity: sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ==} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - estree-util-to-js@1.2.0: - resolution: {integrity: sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA==} + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} - estree-util-visit@1.2.1: - resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -4825,17 +4889,14 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-from-dom@4.2.0: - resolution: {integrity: sha512-t1RJW/OpJbCAJQeKi3Qrj1cAOLA0+av/iPFori112+0X7R3wng+jxLA+kXec8K4szqPRGI8vPxbbpEYvvpwaeQ==} + hast-util-from-dom@5.0.0: + resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} - hast-util-from-html-isomorphic@1.0.0: - resolution: {integrity: sha512-Yu480AKeOEN/+l5LA674a+7BmIvtDj24GvOt7MtQWuhzUwlaaRWdEPXAh3Qm5vhuthpAipFb2vTetKXWOjmTvw==} + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - hast-util-from-html@1.0.2: - resolution: {integrity: sha512-LhrTA2gfCbLOGJq2u/asp4kwuG0y6NhWTXiPKP+n0qNukKy7hc10whqqCFfyvIA1Q5U5d0sp9HhNim9gglEH4A==} - - hast-util-from-parse5@7.1.2: - resolution: {integrity: sha512-Nz7FfPBuljzsN3tCQ4kCBKqdNhQE2l0Tn+X1ubgKBPRoiDIu1mL08Cfw4k7q71+Duyaw7DXDN+VTAp4Vh3oCOw==} + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} hast-util-from-parse5@8.0.1: resolution: {integrity: sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==} @@ -4843,26 +4904,23 @@ packages: hast-util-heading-rank@3.0.0: resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} - hast-util-is-element@2.1.3: - resolution: {integrity: sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA==} - hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} hast-util-parse-selector@2.2.5: resolution: {integrity: sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==} - hast-util-parse-selector@3.1.1: - resolution: {integrity: sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} hast-util-raw@9.0.4: resolution: {integrity: sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==} - hast-util-to-estree@2.3.3: - resolution: {integrity: sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ==} + hast-util-to-estree@3.1.0: + resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + + hast-util-to-jsx-runtime@2.3.2: + resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} @@ -4870,18 +4928,15 @@ packages: hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} - hast-util-to-text@3.1.2: - resolution: {integrity: sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==} + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - hast-util-whitespace@2.0.1: - resolution: {integrity: sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==} + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} - hastscript@7.2.0: - resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - hastscript@8.0.0: resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} @@ -4926,6 +4981,9 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -5047,6 +5105,9 @@ packages: inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + internal-slot@1.0.7: resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} engines: {node: '>= 0.4'} @@ -5110,10 +5171,6 @@ packages: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} engines: {node: '>= 0.4'} - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -5222,9 +5279,6 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -5716,9 +5770,9 @@ packages: map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} - markdown-extensions@1.1.1: - resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} - engines: {node: '>=0.10.0'} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} @@ -5732,12 +5786,6 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} - mdast-util-definitions@5.1.2: - resolution: {integrity: sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==} - - mdast-util-find-and-replace@2.2.2: - resolution: {integrity: sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==} - mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} @@ -5747,75 +5795,48 @@ packages: mdast-util-from-markdown@2.0.1: resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} - mdast-util-gfm-autolink-literal@1.0.3: - resolution: {integrity: sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==} - mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - mdast-util-gfm-footnote@1.0.2: - resolution: {integrity: sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==} - mdast-util-gfm-footnote@2.0.0: resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} - mdast-util-gfm-strikethrough@1.0.3: - resolution: {integrity: sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==} - mdast-util-gfm-strikethrough@2.0.0: resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - mdast-util-gfm-table@1.0.7: - resolution: {integrity: sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==} - mdast-util-gfm-table@2.0.0: resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - mdast-util-gfm-task-list-item@1.0.2: - resolution: {integrity: sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==} - mdast-util-gfm-task-list-item@2.0.0: resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - mdast-util-gfm@2.0.2: - resolution: {integrity: sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==} - mdast-util-gfm@3.0.0: resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} - mdast-util-math@2.0.2: - resolution: {integrity: sha512-8gmkKVp9v6+Tgjtq6SYx9kGPpTf6FVYRa53/DLh479aldR9AyP48qeVOgNZ5X7QUK7nOy4yw7vg6mbiGcs9jWQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} - mdast-util-mdx-expression@1.3.2: - resolution: {integrity: sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA==} + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} - mdast-util-mdx-jsx@2.1.4: - resolution: {integrity: sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA==} + mdast-util-mdx-jsx@3.1.3: + resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} - mdast-util-mdx@2.0.1: - resolution: {integrity: sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw==} + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} - mdast-util-mdxjs-esm@1.3.1: - resolution: {integrity: sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w==} + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} - mdast-util-newline-to-break@1.0.0: - resolution: {integrity: sha512-491LcYv3gbGhhCrLoeALncQmega2xPh+m3gbsIhVsOX4sw85+ShLFPvPyibxc1Swx/6GtzxgVodq+cGa/47ULg==} - - mdast-util-phrasing@3.0.1: - resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@12.3.0: - resolution: {integrity: sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==} - mdast-util-to-hast@13.2.0: resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} - mdast-util-to-markdown@1.5.0: - resolution: {integrity: sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==} - mdast-util-to-markdown@2.1.0: resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} @@ -5862,65 +5883,44 @@ packages: micromark-core-commonmark@2.0.1: resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} - micromark-extension-gfm-autolink-literal@1.0.5: - resolution: {integrity: sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==} - micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - micromark-extension-gfm-footnote@1.1.2: - resolution: {integrity: sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==} - micromark-extension-gfm-footnote@2.1.0: resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - micromark-extension-gfm-strikethrough@1.0.7: - resolution: {integrity: sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==} - micromark-extension-gfm-strikethrough@2.1.0: resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - micromark-extension-gfm-table@1.0.7: - resolution: {integrity: sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==} - micromark-extension-gfm-table@2.1.0: resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} - micromark-extension-gfm-tagfilter@1.0.2: - resolution: {integrity: sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==} - micromark-extension-gfm-tagfilter@2.0.0: resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - micromark-extension-gfm-task-list-item@1.0.5: - resolution: {integrity: sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==} - micromark-extension-gfm-task-list-item@2.1.0: resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - micromark-extension-gfm@2.0.3: - resolution: {integrity: sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==} - micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - micromark-extension-math@2.1.2: - resolution: {integrity: sha512-es0CcOV89VNS9wFmyn+wyFTKweXGW4CEvdaAca6SWRWPyYCbBisnjaHLjWO4Nszuiud84jCpkHsqAJoa768Pvg==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} - micromark-extension-mdx-expression@1.0.8: - resolution: {integrity: sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw==} + micromark-extension-mdx-expression@3.0.0: + resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} - micromark-extension-mdx-jsx@1.0.5: - resolution: {integrity: sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA==} + micromark-extension-mdx-jsx@3.0.1: + resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} - micromark-extension-mdx-md@1.0.1: - resolution: {integrity: sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA==} + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} - micromark-extension-mdxjs-esm@1.0.5: - resolution: {integrity: sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w==} + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} - micromark-extension-mdxjs@1.0.1: - resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} micromark-factory-destination@1.1.0: resolution: {integrity: sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==} @@ -5934,8 +5934,8 @@ packages: micromark-factory-label@2.0.0: resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} - micromark-factory-mdx-expression@1.0.9: - resolution: {integrity: sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA==} + micromark-factory-mdx-expression@2.0.2: + resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==} micromark-factory-space@1.1.0: resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} @@ -5997,8 +5997,8 @@ packages: micromark-util-encode@2.0.0: resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - micromark-util-events-to-acorn@1.2.3: - resolution: {integrity: sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w==} + micromark-util-events-to-acorn@2.0.2: + resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} micromark-util-html-tag-name@1.2.0: resolution: {integrity: sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==} @@ -6445,9 +6445,6 @@ packages: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} engines: {node: '>=0.12'} - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -6821,11 +6818,11 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-markdown@8.0.7: - resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} + react-markdown@9.0.1: + resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: '@types/react': ~18.2.0 - react: '>=16' + react: '>=18' react-multi-email@1.0.25: resolution: {integrity: sha512-Wmv28FvIk4nWgdpHzlIPonY4iSs7bPV35+fAiWYzSBhTo+vhXfglEhjY1WnjHQINW/Pibu2xlb/q1heVuytQHQ==} @@ -6923,6 +6920,18 @@ packages: resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} engines: {node: '>= 4'} + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + recordrtc@5.6.2: resolution: {integrity: sha512-1QNKKNtl7+KcwD1lyOgP3ZlbiJ1d0HtXnypUy7yq49xEERxk31PHvE9RCciDrulPCY7WJ+oz0R9hpNxgsIurGQ==} @@ -6987,12 +6996,15 @@ packages: rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} - rehype-katex@6.0.3: - resolution: {integrity: sha512-ByZlRwRUcWegNbF70CVRm2h/7xy7jQ3R9LaY4VVSvjnoVWwWVhNL60DiZsBpC5tSzYQOCvDbzncIpIjPZWodZA==} + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} @@ -7000,23 +7012,26 @@ packages: resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} engines: {node: '>= 0.10'} - remark-breaks@3.0.3: - resolution: {integrity: sha512-C7VkvcUp1TPUc2eAYzsPdaUh8Xj4FSbQnYA5A9f80diApLZscTDeG7efiWP65W8hV2sEy3JuGVU0i6qr5D8Hug==} + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} - remark-gfm@3.0.1: - resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} + remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} - remark-math@5.1.1: - resolution: {integrity: sha512-cE5T2R/xLVtfFI4cCePtiRn+e6jKMtFDR3P8V3qpv8wpKjwvHoBA4eJzvX+nVrnlNy0911bdGmuspCSwetfYHw==} + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} - remark-mdx@2.3.0: - resolution: {integrity: sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g==} + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} - remark-parse@10.0.2: - resolution: {integrity: sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==} + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-rehype@10.1.0: - resolution: {integrity: sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==} + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} @@ -7431,6 +7446,9 @@ packages: style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -7761,32 +7779,23 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} - unified@10.1.2: - resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-find-after@4.0.1: - resolution: {integrity: sha512-QO/PuPMm2ERxC6vFXEPtmAutOopy5PknD+Oq64gGwxKtk4xwo9Z97t9Av1obPmGU0IyTa6EKYUfTrK2QJS3Ozw==} - - unist-util-generated@2.0.1: - resolution: {integrity: sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==} - - unist-util-is@5.2.1: - resolution: {integrity: sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==} + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} - unist-util-position-from-estree@1.1.2: - resolution: {integrity: sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww==} - - unist-util-position@4.0.4: - resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-remove-position@4.0.2: - resolution: {integrity: sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ==} + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} unist-util-stringify-position@3.0.3: resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} @@ -7794,18 +7803,15 @@ packages: unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@5.1.3: - resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} - unist-util-visit-parents@6.0.1: resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} - unist-util-visit@4.1.2: - resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} - unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -7897,21 +7903,12 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} - vfile-location@4.1.0: - resolution: {integrity: sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw==} - vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@3.1.4: - resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} - vfile@5.3.7: - resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} - vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -9345,16 +9342,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@floating-ui/react@0.25.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@floating-ui/react@0.26.27(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) - '@floating-ui/utils': 0.1.6 + '@floating-ui/utils': 0.2.8 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) tabbable: 6.2.0 - '@floating-ui/utils@0.1.6': {} - '@floating-ui/utils@0.2.8': {} '@formatjs/intl-localematcher@0.5.6': @@ -9817,42 +9812,46 @@ snapshots: lexical: 0.18.0 yjs: 13.6.20 - '@mdx-js/loader@2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3))': + '@mdx-js/loader@3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3))': dependencies: - '@mdx-js/mdx': 2.3.0 + '@mdx-js/mdx': 3.1.0(acorn@8.13.0) source-map: 0.7.4 + optionalDependencies: webpack: 5.95.0(esbuild@0.23.1)(uglify-js@3.19.3) transitivePeerDependencies: + - acorn - supports-color - '@mdx-js/mdx@2.3.0': + '@mdx-js/mdx@3.1.0(acorn@8.13.0)': dependencies: + '@types/estree': 1.0.6 '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 '@types/mdx': 2.0.13 - estree-util-build-jsx: 2.2.2 - estree-util-is-identifier-name: 2.1.0 - estree-util-to-js: 1.2.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-estree: 2.3.3 - markdown-extensions: 1.1.1 - periscopic: 3.1.0 - remark-mdx: 2.3.0 - remark-parse: 10.0.2 - remark-rehype: 10.1.0 - unified: 10.1.2 - unist-util-position-from-estree: 1.1.2 - unist-util-stringify-position: 3.0.3 - unist-util-visit: 4.1.2 - vfile: 5.3.7 + hast-util-to-jsx-runtime: 2.3.2 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.13.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 transitivePeerDependencies: + - acorn - supports-color - '@mdx-js/react@2.3.0(react@18.2.0)': - dependencies: - '@types/mdx': 2.0.13 - '@types/react': 18.2.79 - react: 18.2.0 - '@mdx-js/react@3.1.0(@types/react@18.2.79)(react@18.2.0)': dependencies: '@types/mdx': 2.0.13 @@ -9877,12 +9876,12 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/mdx@14.2.15(@mdx-js/loader@2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)))(@mdx-js/react@2.3.0(react@18.2.0))': + '@next/mdx@14.2.15(@mdx-js/loader@3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)))(@mdx-js/react@3.1.0(@types/react@18.2.79)(react@18.2.0))': dependencies: source-map: 0.7.4 optionalDependencies: - '@mdx-js/loader': 2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)) - '@mdx-js/react': 2.3.0(react@18.2.0) + '@mdx-js/loader': 3.1.0(acorn@8.13.0)(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)) + '@mdx-js/react': 3.1.0(@types/react@18.2.79)(react@18.2.0) '@next/swc-darwin-arm64@14.2.15': optional: true @@ -9925,6 +9924,46 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/request-error@6.1.5': + dependencies: + '@octokit/types': 13.6.1 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/types@13.6.1': + dependencies: + '@octokit/openapi-types': 22.2.0 + '@parcel/watcher-android-arm64@2.4.1': optional: true @@ -10602,10 +10641,6 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/line-clamp@0.4.4(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)))': - dependencies: - tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) - '@tailwindcss/typography@0.5.15(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)))': dependencies: lodash.castarray: 4.4.0 @@ -10614,6 +10649,21 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@18.15.0)(typescript@4.9.5)) + '@tanstack/query-core@5.59.20': {} + + '@tanstack/query-devtools@5.59.20': {} + + '@tanstack/react-query-devtools@5.59.20(@tanstack/react-query@5.59.20(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/query-devtools': 5.59.20 + '@tanstack/react-query': 5.59.20(react@18.2.0) + react: 18.2.0 + + '@tanstack/react-query@5.59.20(react@18.2.0)': + dependencies: + '@tanstack/query-core': 5.59.20 + react: 18.2.0 + '@tanstack/react-virtual@3.10.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/virtual-core': 3.10.8 @@ -10918,8 +10968,6 @@ snapshots: '@types/json5@0.0.29': {} - '@types/katex@0.14.0': {} - '@types/katex@0.16.7': {} '@types/keyv@3.1.4': @@ -11618,6 +11666,8 @@ snapshots: base64-js@1.5.1: {} + before-after-hook@3.0.2: {} + better-opn@3.0.2: dependencies: open: 8.4.2 @@ -11929,6 +11979,8 @@ snapshots: transitivePeerDependencies: - supports-color + collapse-white-space@2.1.0: {} + collect-v8-coverage@1.0.2: {} color-convert@1.9.3: @@ -12648,6 +12700,20 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.13.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + esbuild-code-inspector-plugin@0.17.4: dependencies: code-inspector-core: 0.17.4 @@ -13248,28 +13314,34 @@ snapshots: estraverse@5.3.0: {} - estree-util-attach-comments@2.1.1: + estree-util-attach-comments@3.0.0: dependencies: '@types/estree': 1.0.6 - estree-util-build-jsx@2.2.2: + estree-util-build-jsx@3.0.1: dependencies: '@types/estree-jsx': 1.0.5 - estree-util-is-identifier-name: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 estree-walker: 3.0.3 - estree-util-is-identifier-name@2.1.0: {} + estree-util-is-identifier-name@3.0.0: {} - estree-util-to-js@1.2.0: + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 astring: 1.9.0 source-map: 0.7.4 - estree-util-visit@1.2.1: + estree-util-visit@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 - '@types/unist': 2.0.11 + '@types/unist': 3.0.3 estree-walker@2.0.2: {} @@ -13676,35 +13748,27 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-from-dom@4.2.0: + hast-util-from-dom@5.0.0: dependencies: - hastscript: 7.2.0 + '@types/hast': 3.0.4 + hastscript: 8.0.0 web-namespaces: 2.0.1 - hast-util-from-html-isomorphic@1.0.0: + hast-util-from-html-isomorphic@2.0.0: dependencies: - '@types/hast': 2.3.10 - hast-util-from-dom: 4.2.0 - hast-util-from-html: 1.0.2 - unist-util-remove-position: 4.0.2 + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.0 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 - hast-util-from-html@1.0.2: + hast-util-from-html@2.0.3: dependencies: - '@types/hast': 2.3.10 - hast-util-from-parse5: 7.1.2 + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.1 parse5: 7.2.0 - vfile: 5.3.7 - vfile-message: 3.1.4 - - hast-util-from-parse5@7.1.2: - dependencies: - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 - hastscript: 7.2.0 - property-information: 6.5.0 - vfile: 5.3.7 - vfile-location: 4.1.0 - web-namespaces: 2.0.1 + vfile: 6.0.3 + vfile-message: 4.0.2 hast-util-from-parse5@8.0.1: dependencies: @@ -13721,21 +13785,12 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-is-element@2.1.3: - dependencies: - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 - hast-util-is-element@3.0.0: dependencies: '@types/hast': 3.0.4 hast-util-parse-selector@2.2.5: {} - hast-util-parse-selector@3.1.1: - dependencies: - '@types/hast': 2.3.10 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -13756,26 +13811,47 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-estree@2.3.3: + hast-util-to-estree@3.1.0: dependencies: '@types/estree': 1.0.6 '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 + '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 - estree-util-attach-comments: 2.1.1 - estree-util-is-identifier-name: 2.1.0 - hast-util-whitespace: 2.0.1 - mdast-util-mdx-expression: 1.3.2 - mdast-util-mdxjs-esm: 1.3.1 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 property-information: 6.5.0 space-separated-tokens: 2.0.2 style-to-object: 0.4.4 - unist-util-position: 4.0.4 + unist-util-position: 5.0.0 zwitch: 2.0.4 transitivePeerDependencies: - supports-color + hast-util-to-jsx-runtime@2.3.2: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -13790,14 +13866,16 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-to-text@3.1.2: + hast-util-to-text@4.0.2: dependencies: - '@types/hast': 2.3.10 - '@types/unist': 2.0.11 - hast-util-is-element: 2.1.3 - unist-util-find-after: 4.0.1 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 - hast-util-whitespace@2.0.1: {} + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 hastscript@6.0.0: dependencies: @@ -13807,14 +13885,6 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 - hastscript@7.2.0: - dependencies: - '@types/hast': 2.3.10 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 3.1.1 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - hastscript@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -13865,6 +13935,8 @@ snapshots: html-tags@3.3.1: {} + html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} html-webpack-plugin@5.6.2(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)): @@ -13979,6 +14051,8 @@ snapshots: inline-style-parser@0.1.1: {} + inline-style-parser@0.2.4: {} + internal-slot@1.0.7: dependencies: es-errors: 1.3.0 @@ -14040,8 +14114,6 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.2 - is-buffer@2.0.5: {} - is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 @@ -14129,10 +14201,6 @@ snapshots: is-potential-custom-element-name@1.0.1: {} - is-reference@3.0.2: - dependencies: - '@types/estree': 1.0.6 - is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -14839,7 +14907,7 @@ snapshots: map-or-similar@1.5.0: {} - markdown-extensions@1.1.1: {} + markdown-extensions@2.0.0: {} markdown-table@3.0.3: {} @@ -14853,19 +14921,6 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 - mdast-util-definitions@5.1.2: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - unist-util-visit: 4.1.2 - - mdast-util-find-and-replace@2.2.2: - dependencies: - '@types/mdast': 3.0.15 - escape-string-regexp: 5.0.0 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - mdast-util-find-and-replace@3.0.1: dependencies: '@types/mdast': 4.0.4 @@ -14907,13 +14962,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-autolink-literal@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - ccount: 2.0.1 - mdast-util-find-and-replace: 2.2.2 - micromark-util-character: 1.2.0 - mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -14922,12 +14970,6 @@ snapshots: mdast-util-find-and-replace: 3.0.1 micromark-util-character: 2.1.0 - mdast-util-gfm-footnote@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - micromark-util-normalize-identifier: 1.1.0 - mdast-util-gfm-footnote@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -14938,11 +14980,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@1.0.3: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -14951,15 +14988,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-table@1.0.7: - dependencies: - '@types/mdast': 3.0.15 - markdown-table: 3.0.3 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - mdast-util-gfm-table@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -14970,11 +14998,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@1.0.2: - dependencies: - '@types/mdast': 3.0.15 - mdast-util-to-markdown: 1.5.0 - mdast-util-gfm-task-list-item@2.0.0: dependencies: '@types/mdast': 4.0.4 @@ -14984,18 +15007,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm@2.0.2: - dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-gfm-autolink-literal: 1.0.3 - mdast-util-gfm-footnote: 1.0.2 - mdast-util-gfm-strikethrough: 1.0.3 - mdast-util-gfm-table: 1.0.7 - mdast-util-gfm-task-list-item: 1.0.2 - mdast-util-to-markdown: 1.5.0 - transitivePeerDependencies: - - supports-color - mdast-util-gfm@3.0.0: dependencies: mdast-util-from-markdown: 2.0.1 @@ -15008,85 +15019,77 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-math@2.0.2: + mdast-util-math@3.0.0: dependencies: - '@types/mdast': 3.0.15 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 longest-streak: 3.1.0 - mdast-util-to-markdown: 1.5.0 - - mdast-util-mdx-expression@1.3.2: - dependencies: - '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + unist-util-remove-position: 5.0.0 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@2.1.4: + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 ccount: 2.0.1 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 parse-entities: 4.0.1 stringify-entities: 4.0.4 - unist-util-remove-position: 4.0.2 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 transitivePeerDependencies: - supports-color - mdast-util-mdx@2.0.1: + mdast-util-mdx@3.0.0: dependencies: - mdast-util-from-markdown: 1.3.1 - mdast-util-mdx-expression: 1.3.2 - mdast-util-mdx-jsx: 2.1.4 - mdast-util-mdxjs-esm: 1.3.1 - mdast-util-to-markdown: 1.5.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.0 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@1.3.1: + mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - mdast-util-to-markdown: 1.5.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.1 + mdast-util-to-markdown: 2.1.0 transitivePeerDependencies: - supports-color - mdast-util-newline-to-break@1.0.0: + mdast-util-newline-to-break@2.0.0: dependencies: - '@types/mdast': 3.0.15 - mdast-util-find-and-replace: 2.2.2 - - mdast-util-phrasing@3.0.1: - dependencies: - '@types/mdast': 3.0.15 - unist-util-is: 5.2.1 + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.1 mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.0 - mdast-util-to-hast@12.3.0: - dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-definitions: 5.1.2 - micromark-util-sanitize-uri: 1.2.0 - trim-lines: 3.0.1 - unist-util-generated: 2.0.1 - unist-util-position: 4.0.4 - unist-util-visit: 4.1.2 - mdast-util-to-hast@13.2.0: dependencies: '@types/hast': 3.0.4 @@ -15099,17 +15102,6 @@ snapshots: unist-util-visit: 5.0.0 vfile: 6.0.3 - mdast-util-to-markdown@1.5.0: - dependencies: - '@types/mdast': 3.0.15 - '@types/unist': 2.0.11 - longest-streak: 3.1.0 - mdast-util-phrasing: 3.0.1 - mdast-util-to-string: 3.2.0 - micromark-util-decode-string: 1.1.0 - unist-util-visit: 4.1.2 - zwitch: 2.0.4 - mdast-util-to-markdown@2.1.0: dependencies: '@types/mdast': 4.0.4 @@ -15212,13 +15204,6 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm-autolink-literal@1.0.5: - dependencies: - micromark-util-character: 1.2.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.0 @@ -15226,17 +15211,6 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm-footnote@1.1.2: - dependencies: - micromark-core-commonmark: 1.1.0 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-normalize-identifier: 1.1.0 - micromark-util-sanitize-uri: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-footnote@2.1.0: dependencies: devlop: 1.1.0 @@ -15248,15 +15222,6 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm-strikethrough@1.0.7: - dependencies: - micromark-util-chunked: 1.1.0 - micromark-util-classify-character: 1.1.0 - micromark-util-resolve-all: 1.1.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-strikethrough@2.1.0: dependencies: devlop: 1.1.0 @@ -15266,14 +15231,6 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm-table@1.0.7: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-table@2.1.0: dependencies: devlop: 1.1.0 @@ -15282,22 +15239,10 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm-tagfilter@1.0.2: - dependencies: - micromark-util-types: 1.1.0 - micromark-extension-gfm-tagfilter@2.0.0: dependencies: micromark-util-types: 2.0.0 - micromark-extension-gfm-task-list-item@1.0.5: - dependencies: - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - micromark-extension-gfm-task-list-item@2.1.0: dependencies: devlop: 1.1.0 @@ -15306,17 +15251,6 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-gfm@2.0.3: - dependencies: - micromark-extension-gfm-autolink-literal: 1.0.5 - micromark-extension-gfm-footnote: 1.1.2 - micromark-extension-gfm-strikethrough: 1.0.7 - micromark-extension-gfm-table: 1.0.7 - micromark-extension-gfm-tagfilter: 1.0.2 - micromark-extension-gfm-task-list-item: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 - micromark-extension-gfm@3.0.0: dependencies: micromark-extension-gfm-autolink-literal: 2.1.0 @@ -15328,66 +15262,67 @@ snapshots: micromark-util-combine-extensions: 2.0.0 micromark-util-types: 2.0.0 - micromark-extension-math@2.1.2: + micromark-extension-math@3.1.0: dependencies: '@types/katex': 0.16.7 + devlop: 1.1.0 katex: 0.16.11 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 - micromark-extension-mdx-expression@1.0.8: + micromark-extension-mdx-expression@3.0.0: dependencies: '@types/estree': 1.0.6 - micromark-factory-mdx-expression: 1.0.9 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 - micromark-extension-mdx-jsx@1.0.5: + micromark-extension-mdx-jsx@3.0.1: dependencies: '@types/acorn': 4.0.6 '@types/estree': 1.0.6 - estree-util-is-identifier-name: 2.1.0 - micromark-factory-mdx-expression: 1.0.9 - micromark-factory-space: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - vfile-message: 3.1.4 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 - micromark-extension-mdx-md@1.0.1: + micromark-extension-mdx-md@2.0.0: dependencies: - micromark-util-types: 1.1.0 + micromark-util-types: 2.0.0 - micromark-extension-mdxjs-esm@1.0.5: + micromark-extension-mdxjs-esm@3.0.0: dependencies: '@types/estree': 1.0.6 - micromark-core-commonmark: 1.1.0 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-position-from-estree: 1.1.2 - uvu: 0.5.6 - vfile-message: 3.1.4 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.1 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 - micromark-extension-mdxjs@1.0.1: + micromark-extension-mdxjs@3.0.0: dependencies: acorn: 8.13.0 acorn-jsx: 5.3.2(acorn@8.13.0) - micromark-extension-mdx-expression: 1.0.8 - micromark-extension-mdx-jsx: 1.0.5 - micromark-extension-mdx-md: 1.0.1 - micromark-extension-mdxjs-esm: 1.0.5 - micromark-util-combine-extensions: 1.1.0 - micromark-util-types: 1.1.0 + micromark-extension-mdx-expression: 3.0.0 + micromark-extension-mdx-jsx: 3.0.1 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-types: 2.0.0 micromark-factory-destination@1.1.0: dependencies: @@ -15415,16 +15350,17 @@ snapshots: micromark-util-symbol: 2.0.0 micromark-util-types: 2.0.0 - micromark-factory-mdx-expression@1.0.9: + micromark-factory-mdx-expression@2.0.2: dependencies: '@types/estree': 1.0.6 - micromark-util-character: 1.2.0 - micromark-util-events-to-acorn: 1.2.3 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - unist-util-position-from-estree: 1.1.2 - uvu: 0.5.6 - vfile-message: 3.1.4 + devlop: 1.1.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.1.0 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 micromark-factory-space@1.1.0: dependencies: @@ -15530,16 +15466,16 @@ snapshots: micromark-util-encode@2.0.0: {} - micromark-util-events-to-acorn@1.2.3: + micromark-util-events-to-acorn@2.0.2: dependencies: '@types/acorn': 4.0.6 '@types/estree': 1.0.6 - '@types/unist': 2.0.11 - estree-util-visit: 1.2.1 - micromark-util-symbol: 1.1.0 - micromark-util-types: 1.1.0 - uvu: 0.5.6 - vfile-message: 3.1.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + vfile-message: 4.0.2 micromark-util-html-tag-name@1.2.0: {} @@ -16045,12 +15981,6 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.6 - estree-walker: 3.0.3 - is-reference: 3.0.2 - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -16423,25 +16353,20 @@ snapshots: react-is@18.3.1: {} - react-markdown@8.0.7(@types/react@18.2.79)(react@18.2.0): + react-markdown@9.0.1(@types/react@18.2.79)(react@18.2.0): dependencies: - '@types/hast': 2.3.10 - '@types/prop-types': 15.7.13 + '@types/hast': 3.0.4 '@types/react': 18.2.79 - '@types/unist': 2.0.11 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 2.0.1 - prop-types: 15.8.1 - property-information: 6.5.0 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.2 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.0 react: 18.2.0 - react-is: 18.3.1 - remark-parse: 10.0.2 - remark-rehype: 10.1.0 - space-separated-tokens: 2.0.2 - style-to-object: 0.4.4 - unified: 10.1.2 - unist-util-visit: 4.1.2 - vfile: 5.3.7 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -16573,6 +16498,36 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.0 + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.13.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.13.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.6 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + recordrtc@5.6.2: {} redent@3.0.0: @@ -16656,14 +16611,15 @@ snapshots: space-separated-tokens: 2.0.2 unist-util-visit: 5.0.0 - rehype-katex@6.0.3: + rehype-katex@7.0.1: dependencies: - '@types/hast': 2.3.10 - '@types/katex': 0.14.0 - hast-util-from-html-isomorphic: 1.0.0 - hast-util-to-text: 3.1.2 + '@types/hast': 3.0.4 + '@types/katex': 0.16.7 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 katex: 0.16.11 - unist-util-visit: 4.1.2 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 rehype-raw@7.0.0: dependencies: @@ -16671,6 +16627,14 @@ snapshots: hast-util-raw: 9.0.4 vfile: 6.0.3 + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.0 + transitivePeerDependencies: + - supports-color + rehype-slug@6.0.0: dependencies: '@types/hast': 3.0.4 @@ -16681,49 +16645,61 @@ snapshots: relateurl@0.2.7: {} - remark-breaks@3.0.3: + remark-breaks@4.0.0: dependencies: - '@types/mdast': 3.0.15 - mdast-util-newline-to-break: 1.0.0 - unified: 10.1.2 + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 - remark-gfm@3.0.1: + remark-gfm@4.0.0: dependencies: - '@types/mdast': 3.0.15 - mdast-util-gfm: 2.0.2 - micromark-extension-gfm: 2.0.3 - unified: 10.1.2 + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-math@5.1.1: + remark-math@6.0.0: dependencies: - '@types/mdast': 3.0.15 - mdast-util-math: 2.0.2 - micromark-extension-math: 2.1.2 - unified: 10.1.2 - - remark-mdx@2.3.0: - dependencies: - mdast-util-mdx: 2.0.1 - micromark-extension-mdxjs: 1.0.1 + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-parse@10.0.2: + remark-mdx@3.1.0: dependencies: - '@types/mdast': 3.0.15 - mdast-util-from-markdown: 1.3.1 - unified: 10.1.2 + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-rehype@10.1.0: + remark-parse@11.0.0: dependencies: - '@types/hast': 2.3.10 - '@types/mdast': 3.0.15 - mdast-util-to-hast: 12.3.0 - unified: 10.1.2 + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.1 + micromark-util-types: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.0 + unified: 11.0.5 renderkid@3.0.0: dependencies: @@ -17186,6 +17162,10 @@ snapshots: dependencies: inline-style-parser: 0.1.1 + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + styled-jsx@5.1.1(@babel/core@7.25.8)(react@18.2.0): dependencies: client-only: 0.0.1 @@ -17505,47 +17485,37 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} - unified@10.1.2: + unified@11.0.5: dependencies: - '@types/unist': 2.0.11 + '@types/unist': 3.0.3 bail: 2.0.2 + devlop: 1.1.0 extend: 3.0.2 - is-buffer: 2.0.5 is-plain-obj: 4.1.0 trough: 2.2.0 - vfile: 5.3.7 + vfile: 6.0.3 - unist-util-find-after@4.0.1: + unist-util-find-after@5.0.0: dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - - unist-util-generated@2.0.1: {} - - unist-util-is@5.2.1: - dependencies: - '@types/unist': 2.0.11 + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-position-from-estree@1.1.2: + unist-util-position-from-estree@2.0.0: dependencies: - '@types/unist': 2.0.11 - - unist-util-position@4.0.4: - dependencies: - '@types/unist': 2.0.11 + '@types/unist': 3.0.3 unist-util-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-remove-position@4.0.2: + unist-util-remove-position@5.0.0: dependencies: - '@types/unist': 2.0.11 - unist-util-visit: 4.1.2 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 unist-util-stringify-position@3.0.3: dependencies: @@ -17555,28 +17525,19 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@5.1.3: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 - unist-util-visit@4.1.2: - dependencies: - '@types/unist': 2.0.11 - unist-util-is: 5.2.1 - unist-util-visit-parents: 5.1.3 - unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.2: {} + universalify@0.2.0: {} universalify@2.0.1: {} @@ -17661,33 +17622,16 @@ snapshots: vary@1.1.2: {} - vfile-location@4.1.0: - dependencies: - '@types/unist': 2.0.11 - vfile: 5.3.7 - vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@3.1.4: - dependencies: - '@types/unist': 2.0.11 - unist-util-stringify-position: 3.0.3 - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vfile@5.3.7: - dependencies: - '@types/unist': 2.0.11 - is-buffer: 2.0.5 - unist-util-stringify-position: 3.0.3 - vfile-message: 3.1.4 - vfile@6.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/web/service/base.ts b/web/service/base.ts index 6e7d90fd3a..e1a04217c7 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -369,42 +369,8 @@ const baseFetch = <T>( if (!/^(2|3)\d{2}$/.test(String(res.status))) { const bodyJson = res.json() switch (res.status) { - case 401: { - if (isMarketplaceAPI) - return - - if (isPublicAPI) { - return bodyJson.then((data: ResponseError) => { - if (data.code === 'web_sso_auth_required') - requiredWebSSOLogin() - - if (data.code === 'unauthorized') { - removeAccessToken() - globalThis.location.reload() - } - - return Promise.reject(data) - }) - } - const loginUrl = `${globalThis.location.origin}/signin` - bodyJson.then((data: ResponseError) => { - if (data.code === 'init_validate_failed' && IS_CE_EDITION && !silent) - Toast.notify({ type: 'error', message: data.message, duration: 4000 }) - else if (data.code === 'not_init_validated' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/init` - else if (data.code === 'not_setup' && IS_CE_EDITION) - globalThis.location.href = `${globalThis.location.origin}/install` - else if (location.pathname !== '/signin' || !IS_CE_EDITION) - globalThis.location.href = loginUrl - else if (!silent) - Toast.notify({ type: 'error', message: data.message }) - }).catch(() => { - // Handle any other errors - globalThis.location.href = loginUrl - }) - - break - } + case 401: + return Promise.reject(resClone) case 403: bodyJson.then((data: ResponseError) => { if (!silent) diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 43a57a24b4..e9de724256 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -6,7 +6,6 @@ import type { EndpointsRequest, EndpointsResponse, InstallPackageResponse, - InstalledPluginListResponse, Permissions, PluginDeclaration, PluginManifestInMarket, @@ -14,6 +13,7 @@ import type { TaskStatusResponse, UninstallPluginResponse, UpdateEndpointRequest, + uploadGitHubResponse, } from '@/app/components/plugins/types' import type { DebugInfo as DebugInfoTypes } from '@/app/components/plugins/types' import type { @@ -51,12 +51,6 @@ export const disableEndpoint: Fetcher<EndpointOperationResponse, { url: string; return post<EndpointOperationResponse>(url, { body: { endpoint_id: endpointID } }) } -export const installPackageFromGitHub: Fetcher<InstallPackageResponse, { repo: string; version: string; package: string }> = ({ repo, version, package: packageName }) => { - return post<InstallPackageResponse>('/workspaces/current/plugin/upload/github', { - body: { repo, version, package: packageName }, - }) -} - export const fetchDebugKey = async () => { return get<DebugInfoTypes>('/workspaces/current/plugin/debugging-key') } @@ -76,6 +70,33 @@ export const installPackageFromLocal = async (uniqueIdentifier: string) => { }) } +export const updateFromMarketPlace = async (body: Record<string, string>) => { + return post<InstallPackageResponse>('/workspaces/current/plugin/upgrade/marketplace', { + body, + }) +} + +export const uploadGitHub = async (repoUrl: string, selectedVersion: string, selectedPackage: string) => { + return post<uploadGitHubResponse>('/workspaces/current/plugin/upload/github', { + body: { + repo: repoUrl, + version: selectedVersion, + package: selectedPackage, + }, + }) +} + +export const installPackageFromGitHub = async (repoUrl: string, selectedVersion: string, selectedPackage: string, uniqueIdentifier: string) => { + return post<InstallPackageResponse>('/workspaces/current/plugin/install/github', { + body: { + repo: repoUrl, + version: selectedVersion, + package: selectedPackage, + plugin_unique_identifier: uniqueIdentifier, + }, + }) +} + export const fetchIcon = (tenantId: string, fileName: string) => { return get(`workspaces/current/plugin/icon?tenant_id=${tenantId}&filename=${fileName}`) } @@ -118,10 +139,6 @@ export const updatePermission = async (permissions: Permissions) => { return post('/workspaces/current/plugin/permission/change', { body: permissions }) } -export const fetchInstalledPluginList: Fetcher<InstalledPluginListResponse, { url: string }> = ({ url }) => { - return get<InstalledPluginListResponse>(url) -} - export const uninstallPlugin = async (pluginId: string) => { return post<UninstallPluginResponse>('/workspaces/current/plugin/uninstall', { body: { plugin_installation_id: pluginId } }) } diff --git a/web/service/tools.ts b/web/service/tools.ts index 90221edec2..d6b2f2034b 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -15,6 +15,10 @@ export const fetchCollectionList = () => { return get<Collection[]>('/workspaces/current/tool-providers') } +export const fetchCollectionDetail = (collectionName: string) => { + return get<Collection>(`/workspaces/current/tool-provider/${collectionName}/info`) +} + export const fetchBuiltInToolList = (collectionName: string) => { return get<Tool[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`) } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts new file mode 100644 index 0000000000..69e79c5bab --- /dev/null +++ b/web/service/use-plugins.ts @@ -0,0 +1,29 @@ +import type { InstalledPluginListResponse } from '@/app/components/plugins/types' +import { get } from './base' +import { + useQueryClient, +} from '@tanstack/react-query' + +import { + useQuery, +} from '@tanstack/react-query' + +const NAME_SPACE = 'plugins' + +const useInstalledPluginListKey = [NAME_SPACE, 'installedPluginList'] +export const useInstalledPluginList = () => { + return useQuery<InstalledPluginListResponse>({ + queryKey: useInstalledPluginListKey, + queryFn: () => get<InstalledPluginListResponse>('/workspaces/current/plugin/list'), + }) +} + +export const useInvalidateInstalledPluginList = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries( + { + queryKey: useInstalledPluginListKey, + }) + } +} diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts new file mode 100644 index 0000000000..fb01888e7f --- /dev/null +++ b/web/service/use-tools.ts @@ -0,0 +1,53 @@ +import { get } from './base' +import type { + Tool, +} from '@/app/components/tools/types' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import { + useQueryClient, +} from '@tanstack/react-query' + +import { + useQuery, +} from '@tanstack/react-query' + +const NAME_SPACE = 'tools' + +export const useAllBuiltInTools = () => { + return useQuery<ToolWithProvider[]>({ + queryKey: [NAME_SPACE, 'builtIn'], + queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/builtin'), + }) +} + +const useAllCustomToolsKey = [NAME_SPACE, 'customTools'] +export const useAllCustomTools = () => { + return useQuery<ToolWithProvider[]>({ + queryKey: useAllCustomToolsKey, + queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/api'), + }) +} + +export const useInvalidateAllCustomTools = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries( + { + queryKey: useAllCustomToolsKey, + }) + } +} + +export const useAllWorkflowTools = () => { + return useQuery<ToolWithProvider[]>({ + queryKey: [NAME_SPACE, 'workflowTools'], + queryFn: () => get<ToolWithProvider[]>('/workspaces/current/tools/workflow'), + }) +} + +export const useBuiltInTools = (collectionName: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'builtIn', collectionName], + queryFn: () => get<Tool[]>(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`), + }) +} diff --git a/web/utils/index.ts b/web/utils/index.ts index 7aa6fef0a8..b8b499ae32 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -57,3 +57,13 @@ export async function fetchWithRetry<T = any>(fn: Promise<T>, retries = 3): Prom return [null, res] } } + +export const correctProvider = (provider: string) => { + if (!provider) + return '' + + if (provider.includes('/')) + return provider + + return `langgenius/${provider}/${provider}` +} diff --git a/web/utils/semver.ts b/web/utils/semver.ts new file mode 100644 index 0000000000..f1b9eb8d7e --- /dev/null +++ b/web/utils/semver.ts @@ -0,0 +1,9 @@ +import semver from 'semver' + +export const getLatestVersion = (versionList: string[]) => { + return semver.rsort(versionList)[0] +} + +export const compareVersion = (v1: string, v2: string) => { + return semver.compare(v1, v2) +}