mirror of
https://github.com/langgenius/dify.git
synced 2026-04-14 16:08:01 +08:00
merge feat/plugins
This commit is contained in:
commit
5d7c527702
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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=
|
||||
|
||||
@ -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<HTMLDivElement>(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('')
|
||||
|
||||
@ -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 }) => {
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
{children}
|
||||
<TanstackQueryIniter>
|
||||
{children}
|
||||
</TanstackQueryIniter>
|
||||
</ModalContextProvider>
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
|
||||
67
web/app/(commonLayout)/plugins/test/update/page.tsx
Normal file
67
web/app/(commonLayout)/plugins/test/update/page.tsx
Normal file
@ -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 (
|
||||
<div>
|
||||
<div>更新组件</div>
|
||||
<div className='flex space-x-1'>
|
||||
<div className='underline cursor-pointer' onClick={handleUpdateFromMarketPlace}>从 Marketplace</div>
|
||||
<div className='underline cursor-pointer' onClick={handleUpdateFromGithub}>从 GitHub</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(UpdatePlugin)
|
||||
@ -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<number>(-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 (
|
||||
<>
|
||||
<Panel
|
||||
@ -107,7 +124,14 @@ const AgentTools: FC = () => {
|
||||
{tools.length < MAX_TOOLS_NUM && (
|
||||
<>
|
||||
<div className='ml-3 mr-1 h-3.5 w-px bg-gray-200'></div>
|
||||
<OperationBtn type="add" onClick={() => setIsShowChooseTool(true)} />
|
||||
<ToolPicker
|
||||
trigger={<OperationBtn type="add" />}
|
||||
isShow={isShowChooseTool}
|
||||
onShowChange={setIsShowChooseTool}
|
||||
disabled={false}
|
||||
supportAddCustomTool
|
||||
onSelect={handleSelectTool}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -125,8 +149,8 @@ const AgentTools: FC = () => {
|
||||
{item.isDeleted && <DefaultToolIcon className='w-5 h-5' />}
|
||||
{!item.isDeleted && (
|
||||
<div className={cn((item.notAuthor || !item.enabled) && 'opacity-50')}>
|
||||
{typeof item.icon === 'string' && <div className='w-5 h-5 bg-cover bg-center rounded-md' style={{ backgroundImage: `url(${item.icon})` }}/>}
|
||||
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background}/>}
|
||||
{typeof item.icon === 'string' && <div className='w-5 h-5 bg-cover bg-center rounded-md' style={{ backgroundImage: `url(${item.icon})` }} />}
|
||||
{typeof item.icon !== 'string' && <AppIcon className='rounded-md' size='xs' icon={item.icon?.content} background={item.icon?.background} />}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@ -245,9 +269,6 @@ const AgentTools: FC = () => {
|
||||
))}
|
||||
</div >
|
||||
</Panel >
|
||||
{isShowChooseTool && (
|
||||
<AddToolModal onHide={() => setIsShowChooseTool(false)} />
|
||||
)}
|
||||
{isShowSettingTool && (
|
||||
<SettingBuiltInTool
|
||||
toolName={currentTool?.tool_name as string}
|
||||
|
||||
@ -71,8 +71,9 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
import { correctProvider } from '@/utils'
|
||||
|
||||
interface PublishConfig {
|
||||
type PublishConfig = {
|
||||
modelConfig: ModelConfig
|
||||
completionParams: FormValue
|
||||
}
|
||||
@ -156,6 +157,7 @@ const Configuration: FC = () => {
|
||||
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<ModelConfig>({
|
||||
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<Collection[]>([])
|
||||
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)
|
||||
})
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -60,7 +60,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
'/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
|
||||
|
||||
@ -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<string, string> = {
|
||||
@ -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<Element>): 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 <PreContext.Provider value={{
|
||||
inline: false,
|
||||
}}>
|
||||
<pre {...rest} />
|
||||
</PreContext.Provider>
|
||||
}
|
||||
|
||||
// **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<Element>): 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 }
|
||||
</div>
|
||||
)
|
||||
})
|
||||
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 <VideoGallery key={srcs.join()} srcs={srcs} />
|
||||
})
|
||||
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 <AudioGallery key={srcs.join()} srcs={srcs} />
|
||||
})
|
||||
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 (
|
||||
<>
|
||||
<ImageGallery srcs={[children_node[0].properties.src]} />
|
||||
<p>{paragraph.children.slice(1)}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <p>{paragraph.children}</p>
|
||||
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 <ImageGallery srcs={[children_node?.[0]?.properties?.src as string]} />
|
||||
return <p>{children}</p>
|
||||
}
|
||||
|
||||
const Img = ({ src }: any) => {
|
||||
return (<ImageGallery srcs={[src]} />)
|
||||
const Img: Components['img'] = ({ src }) => {
|
||||
return (<ImageGallery srcs={[src!]} />)
|
||||
}
|
||||
|
||||
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 <abbr className="underline decoration-dashed !decoration-primary-700 cursor-pointer" onClick={() => onSend?.(hidden_text)} title={node.children[0]?.value}>{node.children[0]?.value}</abbr>
|
||||
const hidden_text = decodeURIComponent(node!.properties.href.toString().split('abbr:')[1])
|
||||
const title = (node!.children[0] as ElementContentMap['text'])?.value
|
||||
return <abbr className="underline decoration-dashed !decoration-primary-700 cursor-pointer" onClick={() => onSend?.(hidden_text)} title={title}>{title}</abbr>
|
||||
}
|
||||
else {
|
||||
return <a {...props} target="_blank" className="underline decoration-dashed !decoration-primary-700 cursor-pointer">{node.children[0] ? node.children[0]?.value : 'Download'}</a>
|
||||
const firstChild = node?.children?.[0] as ElementContentMap['text'] | undefined
|
||||
return <a {...props} target="_blank" className="underline decoration-dashed !decoration-primary-700 cursor-pointer">{
|
||||
firstChild
|
||||
? firstChild.value
|
||||
: 'Download'
|
||||
}</a>
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -18,7 +18,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
|
||||
}
|
||||
|
||||
static clone(node: WorkflowVariableBlockNode): WorkflowVariableBlockNode {
|
||||
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap)
|
||||
return new WorkflowVariableBlockNode(node.__variables, node.__workflowNodesMap, node.__key)
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
|
||||
@ -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<PortalSelectProps> = ({
|
||||
onSelect,
|
||||
items,
|
||||
placeholder,
|
||||
installedValue,
|
||||
renderTrigger,
|
||||
triggerClassName,
|
||||
triggerClassNameFn,
|
||||
@ -313,7 +316,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
|
||||
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 (
|
||||
<PortalToFollowElem
|
||||
@ -328,19 +331,20 @@ const PortalSelect: FC<PortalSelectProps> = ({
|
||||
: (
|
||||
<div
|
||||
className={classNames(`
|
||||
flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-gray-100 text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
group flex items-center justify-between px-2.5 h-9 rounded-lg border-0 bg-components-input-bg-normal hover:bg-state-base-hover-alt text-sm ${readonly ? 'cursor-not-allowed' : 'cursor-pointer'}
|
||||
`, triggerClassName, triggerClassNameFn?.(open))}
|
||||
title={selectedItem?.name}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
grow truncate
|
||||
${!selectedItem?.name && 'text-gray-400'}
|
||||
${!selectedItem?.name && 'text-components-input-text-placeholder'}
|
||||
`}
|
||||
>
|
||||
{selectedItem?.name ?? localPlaceholder}
|
||||
</span>
|
||||
<ChevronDownIcon className='shrink-0 h-4 w-4 text-gray-400' />
|
||||
<div className='mx-0.5'>{installedValue && selectedItem && selectedItem.value !== installedValue && <Badge>{installedValue} {'->'} {selectedItem.value} </Badge>}</div>
|
||||
<ChevronDownIcon className='shrink-0 h-4 w-4 text-text-quaternary group-hover:text-text-secondary' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -353,8 +357,8 @@ const PortalSelect: FC<PortalSelectProps> = ({
|
||||
<div
|
||||
key={item.value}
|
||||
className={`
|
||||
flex items-center justify-between px-2.5 h-9 cursor-pointer rounded-lg hover:bg-gray-100 text-gray-700
|
||||
${item.value === value && 'bg-gray-100'}
|
||||
flex items-center justify-between px-2.5 h-9 cursor-pointer rounded-lg hover:bg-state-base-hover text-text-secondary
|
||||
${item.value === value && 'bg-state-base-hover'}
|
||||
`}
|
||||
title={item.name}
|
||||
onClick={() => {
|
||||
@ -366,7 +370,10 @@ const PortalSelect: FC<PortalSelectProps> = ({
|
||||
className='w-0 grow truncate'
|
||||
title={item.name}
|
||||
>
|
||||
{item.name}
|
||||
<span className='truncate'>{item.name}</span>
|
||||
{item.value === installedValue && (
|
||||
<Badge uppercase={true} className='shrink-0 ml-1'>INSTALLED</Badge>
|
||||
)}
|
||||
</span>
|
||||
{!hideChecked && item.value === value && (
|
||||
<CheckIcon className='shrink-0 h-4 w-4 text-text-accent' />
|
||||
|
||||
@ -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<TabSliderProps> = ({
|
||||
}) => {
|
||||
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<TabSliderProps> = ({
|
||||
const newIndex = options.findIndex(option => option.value === value)
|
||||
setActiveIndex(newIndex)
|
||||
updateSliderStyle(newIndex)
|
||||
}, [value, options])
|
||||
}, [value, options, pluginList])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'inline-flex p-0.5 rounded-[10px] bg-components-segmented-control-bg-normal relative items-center justify-center')}>
|
||||
@ -67,13 +67,15 @@ const TabSlider: FC<TabSliderProps> = ({
|
||||
}}
|
||||
>
|
||||
{option.text}
|
||||
{/* if no plugin installed, the badge won't show */}
|
||||
{option.value === 'plugins'
|
||||
&& (pluginList?.plugins.length ?? 0) > 0
|
||||
&& <Badge
|
||||
size='s'
|
||||
uppercase={true}
|
||||
state={BadgeState.Default}
|
||||
>
|
||||
{pluginList.length}
|
||||
{pluginList?.plugins.length}
|
||||
</Badge>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -74,7 +74,7 @@ const UpgradeBtn: FC<Props> = ({
|
||||
onClick={onClick}
|
||||
>
|
||||
<GoldCoin className='mr-1 w-3.5 h-3.5' />
|
||||
<div className='text-xs font-normal'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
|
||||
<div className='text-xs font-normal text-nowrap'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
|
||||
<Sparkles
|
||||
className='absolute -right-1 -top-2 w-4 h-5 bg-cover'
|
||||
/>
|
||||
|
||||
@ -55,7 +55,7 @@ export const Heading = function H2({
|
||||
|
||||
export function Row({ children }: IChildrenProps) {
|
||||
return (
|
||||
<div className="grid items-start grid-cols-1 gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
|
||||
<div className="grid items-start grid-cols-1 gap-x-16 gap-y-10 xl:!max-w-none xl:grid-cols-2">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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 => (
|
||||
<div className='flex py-1 pl-3 pr-2 items-center gap-2 self-stretch hover:bg-state-base-hover rounded-lg' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
|
||||
<div className='flex items-center justify-center w-7 h-7 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
|
||||
<div className='line-clamp-1 flex-grow overflow-hidden text-text-secondary text-ellipsis system-md-regular'>{workspace.name}</div>
|
||||
{enableBilling && (
|
||||
<div className='select-none'>
|
||||
<HeaderBillingBtn isDisplayOnly={true} />
|
||||
</div>
|
||||
)}
|
||||
<div className='line-clamp-1 flex-grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
|
||||
const [collapse, setCollapse] = useState(false)
|
||||
|
||||
const {
|
||||
plugins,
|
||||
plugins = [],
|
||||
queryPlugins,
|
||||
queryPluginsWithDebounced,
|
||||
isLoading: isPluginsLoading,
|
||||
|
||||
@ -17,7 +17,7 @@ const PluginsNav = ({
|
||||
<Link href="/plugins" className={classNames(
|
||||
className, 'group',
|
||||
)}>
|
||||
<div className='flex flex-row p-1.5 gap-0.5 items-center justify-center rounded-xl system-xs-medium-uppercase hover:bg-state-base-hover text-text-tertiary hover:text-text-secondary'>
|
||||
<div className='flex flex-row h-8 p-1.5 gap-0.5 items-center justify-center rounded-xl system-sm-medium-uppercase hover:bg-state-base-hover text-text-tertiary hover:text-text-secondary'>
|
||||
<div className='flex w-4 h-4 justify-center items-center'>
|
||||
<Group />
|
||||
</div>
|
||||
|
||||
@ -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<Props> = ({
|
||||
label,
|
||||
labelWidthClassName = 'w-10',
|
||||
value,
|
||||
valueMaxWidthClassName = 'max-w-[162px]',
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
@ -46,7 +48,7 @@ const KeyValueItem: FC<Props> = ({
|
||||
<div className='flex items-center gap-1'>
|
||||
<span className={cn('flex flex-col justify-center items-start text-text-tertiary system-xs-medium', labelWidthClassName)}>{label}</span>
|
||||
<div className='flex justify-center items-center gap-0.5'>
|
||||
<span className='max-w-[300px] truncate system-xs-medium text-text-secondary'>
|
||||
<span className={cn(valueMaxWidthClassName, ' truncate system-xs-medium text-text-secondary')}>
|
||||
{value}
|
||||
</span>
|
||||
<Tooltip popupContent={t(`common.operation.${isCopied ? 'copied' : 'copy'}`)} position='top'>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
89
web/app/components/plugins/hooks.ts
Normal file
89
web/app/components/plugins/hooks.ts
Normal file
@ -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<string, Tag>)
|
||||
|
||||
return {
|
||||
tags,
|
||||
tagsMap,
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
69
web/app/components/plugins/install-plugin/hooks.ts
Normal file
69
web/app/components/plugins/install-plugin/hooks.ts
Normal file
@ -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 }
|
||||
}
|
||||
@ -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<InstallFromGitHubProps> = ({ onClose }) => {
|
||||
const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, onClose, onSuccess }) => {
|
||||
const { t } = useTranslation()
|
||||
const { getIconUrl } = useGetIcon()
|
||||
const { fetchReleases } = useGitHubReleases()
|
||||
const [state, setState] = useState<InstallState>({
|
||||
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<string | null>(null)
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
|
||||
const versions: Item[] = state.releases.map(release => ({
|
||||
value: release.tag_name,
|
||||
@ -36,152 +51,151 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ 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 (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={onClose}
|
||||
className='flex min-w-[480px] p-0 flex-col items-start rounded-2xl border-[0.5px]
|
||||
className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px]
|
||||
border-components-panel-border bg-components-panel-bg shadows-shadow-xl'
|
||||
closable
|
||||
>
|
||||
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
|
||||
<div className='flex flex-col items-start gap-1 flex-grow'>
|
||||
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
|
||||
{t('plugin.installFromGitHub.installPlugin')}
|
||||
{getTitle()}
|
||||
</div>
|
||||
<div className='self-stretch text-text-tertiary system-xs-regular'>
|
||||
{state.step !== InstallStepFromGitHub.installed && t('plugin.installFromGitHub.installNote')}
|
||||
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('plugin.installFromGitHub.installNote')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex px-6 py-3 flex-col justify-center items-start self-stretch ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
|
||||
{state.step === InstallStepFromGitHub.setUrl && (
|
||||
<SetURL
|
||||
repoUrl={state.repoUrl}
|
||||
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
|
||||
onNext={handleNext}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.setVersion && (
|
||||
<SetVersion
|
||||
selectedVersion={state.selectedVersion}
|
||||
versions={versions}
|
||||
onSelect={item => setState(prevState => ({ ...prevState, selectedVersion: item.value as string }))}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.setPackage && (
|
||||
<SetPackage
|
||||
selectedPackage={state.selectedPackage}
|
||||
packages={packages}
|
||||
onSelect={item => setState(prevState => ({ ...prevState, selectedPackage: item.value as string }))}
|
||||
onInstall={handleInstall}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.installed && (
|
||||
<Installed
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
|
||||
? <Installed
|
||||
payload={manifest}
|
||||
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
|
||||
errMsg={errorMsg}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
: <div className={`flex px-6 py-3 flex-col justify-center items-start self-stretch ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
|
||||
{state.step === InstallStepFromGitHub.setUrl && (
|
||||
<SetURL
|
||||
repoUrl={state.repoUrl}
|
||||
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
|
||||
onNext={handleUrlSubmit}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
)}
|
||||
{state.step === InstallStepFromGitHub.selectPackage && (
|
||||
<SelectPackage
|
||||
updatePayload={updatePayload!}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
versions={versions}
|
||||
onSelectVersion={item => 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 && (
|
||||
<Loaded
|
||||
updatePayload={updatePayload!}
|
||||
uniqueIdentifier={uniqueIdentifier!}
|
||||
payload={manifest as any}
|
||||
repoUrl={state.repoUrl}
|
||||
selectedVersion={state.selectedVersion}
|
||||
selectedPackage={state.selectedPackage}
|
||||
onBack={handleBack}
|
||||
onInstalled={handleInstalled}
|
||||
onFailed={handleFailed}
|
||||
/>
|
||||
)}
|
||||
</div>}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 }) => (
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className='flex-shrink-0 w-[72px] items-center gap-2'>
|
||||
<div className='text-text-tertiary system-sm-medium truncate'>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-grow overflow-hidden'>
|
||||
<div className='text-text-secondary text-ellipsis system-sm-medium'>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const Installed: React.FC<InstalledProps> = ({ repoUrl, selectedVersion, selectedPackage, onClose }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className='text-text-secondary system-md-regular'>The plugin has been installed successfully.</div>
|
||||
<div className='flex w-full p-4 flex-col justify-center items-start gap-2 rounded-2xl bg-background-section-burn'>
|
||||
{[
|
||||
{ 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 }) => (
|
||||
<InfoRow key={label} label={label} value={value} />
|
||||
))}
|
||||
</div>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('plugin.installModal.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Installed
|
||||
@ -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<LoadedProps> = ({
|
||||
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 (
|
||||
<>
|
||||
<div className='text-text-secondary system-md-regular'>
|
||||
<p>{t(`${i18nPrefix}.readyToInstall`)}</p>
|
||||
</div>
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestToCardPluginProps(payload)}
|
||||
titleLeft={<Badge className='mx-1' size="s" state={BadgeState.Default}>{payload.version}</Badge>}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isInstalling && (
|
||||
<Button variant='secondary' className='min-w-[72px]' onClick={onBack}>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px] flex space-x-0.5'
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling}
|
||||
>
|
||||
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
|
||||
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loaded
|
||||
@ -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<SelectPackageProps> = ({
|
||||
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 (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectVersion`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelectVersion}
|
||||
items={versions}
|
||||
installedValue={updatePayload?.originalPackageInfo.version}
|
||||
placeholder={t(`${i18nPrefix}.selectVersionPlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t(`${i18nPrefix}.selectPackage`)}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelectPackage}
|
||||
items={packages}
|
||||
readonly={!selectedVersion}
|
||||
placeholder={t(`${i18nPrefix}.selectPackagePlaceholder`) || ''}
|
||||
popupClassName='w-[512px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
{!isEdit
|
||||
&& <Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
disabled={isUploading}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={handleUploadPackage}
|
||||
disabled={!selectedVersion || !selectedPackage || isUploading}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectPackage
|
||||
@ -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<SetPackageProps> = ({ selectedPackage, packages, onSelect, onInstall, onBack }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='package'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.selectPackage')}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedPackage}
|
||||
onSelect={onSelect}
|
||||
items={packages}
|
||||
placeholder={t('plugin.installFromGitHub.selectPackagePlaceholder') || ''}
|
||||
popupClassName='w-[432px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onInstall}
|
||||
disabled={!selectedPackage}
|
||||
>
|
||||
{t('plugin.installModal.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetPackage
|
||||
@ -1,3 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -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<SetVersionProps> = ({ selectedVersion, versions, onSelect, onNext, onBack }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
htmlFor='version'
|
||||
className='flex flex-col justify-center items-start self-stretch text-text-secondary'
|
||||
>
|
||||
<span className='system-sm-semibold'>{t('plugin.installFromGitHub.selectVersion')}</span>
|
||||
</label>
|
||||
<PortalSelect
|
||||
value={selectedVersion}
|
||||
onSelect={onSelect}
|
||||
items={versions}
|
||||
placeholder={t('plugin.installFromGitHub.selectVersionPlaceholder') || ''}
|
||||
popupClassName='w-[432px] z-[1001]'
|
||||
/>
|
||||
<div className='flex justify-end items-center gap-2 self-stretch mt-4'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('plugin.installModal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='min-w-[72px]'
|
||||
onClick={onNext}
|
||||
disabled={!selectedVersion}
|
||||
>
|
||||
{t('plugin.installModal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetVersion
|
||||
@ -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<InstallFromLocalPackageProps> = ({
|
||||
const [uniqueIdentifier, setUniqueIdentifier] = useState<string | null>(null)
|
||||
const [manifest, setManifest] = useState<PluginDeclaration | null>(null)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(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<InstallFromLocalPackageProps> = ({
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
}, [step])
|
||||
}, [step, t])
|
||||
|
||||
const { getIconUrl } = useGetIcon()
|
||||
|
||||
@ -59,7 +59,7 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
icon,
|
||||
})
|
||||
setStep(InstallStep.readyToInstall)
|
||||
}, [])
|
||||
}, [getIconUrl])
|
||||
|
||||
const handleUploadFail = useCallback((errorMsg: string) => {
|
||||
setErrorMsg(errorMsg)
|
||||
@ -67,9 +67,9 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
|
||||
}, [])
|
||||
|
||||
const handleInstalled = useCallback(() => {
|
||||
mutateInstalledPluginList()
|
||||
invalidateInstalledPluginList()
|
||||
setStep(InstallStep.installed)
|
||||
}, [mutateInstalledPluginList])
|
||||
}, [invalidateInstalledPluginList])
|
||||
|
||||
const handleFailed = useCallback((errorMsg?: string) => {
|
||||
setStep(InstallStep.installFailed)
|
||||
|
||||
@ -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<Props> = ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
@ -40,9 +43,12 @@ const Installed: FC<Props> = ({
|
||||
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<Props> = ({
|
||||
onInstalled()
|
||||
return
|
||||
}
|
||||
setPluginTasksWithPolling()
|
||||
await check({
|
||||
taskId,
|
||||
pluginUniqueIdentifier: uniqueIdentifier,
|
||||
|
||||
@ -30,7 +30,7 @@ const Uploading: FC<Props> = ({
|
||||
const handleUpload = async () => {
|
||||
try {
|
||||
const res = await uploadPackageFile(file)
|
||||
// onUploaded(res)
|
||||
onUploaded(res)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.response?.message) {
|
||||
|
||||
@ -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<InstallFromMarketplaceProps> = ({
|
||||
if (step === InstallStep.installFailed)
|
||||
return t(`${i18nPrefix}.installFailed`)
|
||||
return t(`${i18nPrefix}.installPlugin`)
|
||||
}, [step])
|
||||
}, [step, t])
|
||||
|
||||
const handleInstalled = useCallback(() => {
|
||||
setStep(InstallStep.installed)
|
||||
|
||||
@ -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<Props> = ({
|
||||
uniqueIdentifier,
|
||||
payload,
|
||||
onCancel,
|
||||
onStartToInstall,
|
||||
onInstalled,
|
||||
onFailed,
|
||||
}) => {
|
||||
@ -43,6 +45,7 @@ const Installed: FC<Props> = ({
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isInstalling) return
|
||||
onStartToInstall?.()
|
||||
setIsInstalling(true)
|
||||
|
||||
try {
|
||||
@ -90,7 +93,7 @@ const Installed: FC<Props> = ({
|
||||
</>
|
||||
)
|
||||
}</>)
|
||||
}, [payload])
|
||||
}, [payload.latest_version, supportCheckInstalled])
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -101,7 +104,7 @@ const Installed: FC<Props> = ({
|
||||
<div className='flex p-2 items-start content-start gap-1 self-stretch flex-wrap rounded-2xl bg-background-section-burn'>
|
||||
<Card
|
||||
className='w-full'
|
||||
payload={pluginManifestInMarketToPluginProps(payload)}
|
||||
payload={pluginManifestInMarketToPluginProps(payload as PluginManifestInMarket)}
|
||||
titleLeft={versionInfo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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}` : ''
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<h1 className='mb-2 text-center title-4xl-semi-bold text-text-primary'>
|
||||
<h1 className='shrink-0 mb-2 text-center title-4xl-semi-bold text-text-primary'>
|
||||
Empower your AI development
|
||||
</h1>
|
||||
<h2 className='flex justify-center items-center text-center body-md-regular text-text-tertiary'>
|
||||
<h2 className='shrink-0 flex justify-center 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('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('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('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('category.bundles')}
|
||||
</span>
|
||||
in Dify Marketplace
|
||||
</h2>
|
||||
|
||||
@ -4,7 +4,7 @@ import Line from './line'
|
||||
const Empty = () => {
|
||||
return (
|
||||
<div
|
||||
className='relative grid grid-cols-4 grid-rows-4 gap-3 p-2'
|
||||
className='grow relative h-0 grid grid-cols-4 grid-rows-4 gap-3 p-2 overflow-hidden'
|
||||
>
|
||||
{
|
||||
Array.from({ length: 16 }).map((_, index) => (
|
||||
|
||||
@ -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<Plugin[]>([])
|
||||
const [plugins, setPlugins] = useState<Plugin[]>()
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<MarketplaceContextProvider>
|
||||
<Description />
|
||||
<Description locale={locale} />
|
||||
<IntersectionLine />
|
||||
<SearchBoxWrapper />
|
||||
<PluginTypeSwitch />
|
||||
<SearchBoxWrapper locale={locale} />
|
||||
<PluginTypeSwitch locale={locale} />
|
||||
<ListWrapper
|
||||
locale={locale}
|
||||
marketplaceCollections={marketplaceCollections}
|
||||
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
|
||||
showInstallButton={showInstallButton}
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import { useEffect } from 'react'
|
||||
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
|
||||
import { useMarketplaceContext } from '@/app/components/plugins/marketplace/context'
|
||||
|
||||
export const useScrollIntersection = (
|
||||
anchorRef: React.RefObject<HTMLDivElement>,
|
||||
) => {
|
||||
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])
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className='group relative rounded-xl cursor-pointer'>
|
||||
<div
|
||||
className='group relative rounded-xl cursor-pointer'
|
||||
onClick={() => {
|
||||
if (!showInstallButton)
|
||||
window.open(`${MARKETPLACE_URL_PREFIX}/plugin/${plugin.org}/${plugin.name}`)
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
key={plugin.name}
|
||||
payload={plugin}
|
||||
locale={locale}
|
||||
footer={
|
||||
<CardMoreInfo
|
||||
downloadCount={plugin.install_count}
|
||||
@ -34,6 +50,7 @@ const CardWrapper = ({
|
||||
<Button
|
||||
variant='primary'
|
||||
className='flex-1'
|
||||
onClick={showInstallFromMarketplace}
|
||||
>
|
||||
{t('plugin.detailPanel.operation.install')}
|
||||
</Button>
|
||||
@ -48,6 +65,16 @@ const CardWrapper = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
isShowInstallFromMarketplace && (
|
||||
<InstallFromMarketplace
|
||||
manifest={plugin as any}
|
||||
uniqueIdentifier={plugin.latest_package_identifier}
|
||||
onClose={hideInstallFromMarketplace}
|
||||
onSuccess={hideInstallFromMarketplace}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -10,12 +10,14 @@ type ListProps = {
|
||||
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
||||
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}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
@ -7,11 +7,13 @@ type ListWithCollectionProps = {
|
||||
marketplaceCollections: MarketplaceCollection[]
|
||||
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
||||
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}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
@ -9,18 +9,20 @@ type ListWrapperProps = {
|
||||
marketplaceCollections: MarketplaceCollection[]
|
||||
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
|
||||
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 (
|
||||
<div className='px-12 py-2 bg-background-default-subtle'>
|
||||
<div className='flex flex-col grow px-12 py-2 bg-background-default-subtle'>
|
||||
{
|
||||
plugins && (
|
||||
<div className='flex items-center mb-4 pt-3'>
|
||||
@ -35,6 +37,7 @@ const ListWrapper = ({
|
||||
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap}
|
||||
plugins={plugins}
|
||||
showInstallButton={showInstallButton}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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: <RiBrain2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.tool,
|
||||
text: 'Tools',
|
||||
icon: <RiHammerLine className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.extension,
|
||||
text: 'Extensions',
|
||||
icon: <RiPuzzle2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.bundle,
|
||||
text: 'Bundles',
|
||||
icon: <RiArchive2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
]
|
||||
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: <RiBrain2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.tool,
|
||||
text: t('plugin.category.tools'),
|
||||
icon: <RiHammerLine className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.extension,
|
||||
text: t('plugin.category.extensions'),
|
||||
icon: <RiPuzzle2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
{
|
||||
value: PLUGIN_TYPE_SEARCH_MAP.bundle,
|
||||
text: t('plugin.category.bundles'),
|
||||
icon: <RiArchive2Line className='mr-1.5 w-4 h-4' />,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'sticky top-[60px] flex items-center justify-center py-3 bg-background-body space-x-2 z-10',
|
||||
'sticky top-[60px] shrink-0 flex items-center justify-center py-3 bg-background-body space-x-2 z-10',
|
||||
)}>
|
||||
{
|
||||
options.map(option => (
|
||||
|
||||
@ -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 (
|
||||
<div
|
||||
@ -35,10 +37,11 @@ const SearchBox = ({
|
||||
tags={tags}
|
||||
onTagsChange={onTagsChange}
|
||||
size={size}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className='mx-1 w-[1px] h-3.5 bg-divider-regular'></div>
|
||||
<div className='grow flex items-center p-1 pl-2'>
|
||||
<div className='flex items-center mr-2 py-0.5 w-full'>
|
||||
<div className='flex items-center mr-2 w-full'>
|
||||
<input
|
||||
className={cn(
|
||||
'grow block outline-none appearance-none body-md-medium text-text-secondary bg-transparent',
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
'use client'
|
||||
import { useMarketplaceContext } from '../context'
|
||||
import { useMixedTranslation } from '../hooks'
|
||||
import SearchBox from './index'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const SearchBoxWrapper = () => {
|
||||
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 (
|
||||
<SearchBox
|
||||
inputClassName={cn(
|
||||
'sticky top-3 mx-auto w-[640px]',
|
||||
'sticky top-3 mx-auto w-[640px] shrink-0',
|
||||
!intersected && 'w-[508px] transition-[width] duration-300',
|
||||
)}
|
||||
search={searchPluginText}
|
||||
@ -21,6 +28,8 @@ const SearchBoxWrapper = () => {
|
||||
tags={filterPluginTags}
|
||||
onTagsChange={handleFilterPluginTagsChange}
|
||||
size='large'
|
||||
locale={locale}
|
||||
placeholder={t('plugin.searchPlugins')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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') || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className='p-1 max-h-[448px] overflow-y-auto'>
|
||||
{
|
||||
filteredOptions.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
key={option.name}
|
||||
className='flex items-center px-2 py-1.5 h-7 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||
onClick={() => handleCheck(option.value)}
|
||||
onClick={() => handleCheck(option.name)}
|
||||
>
|
||||
<Checkbox
|
||||
className='mr-1'
|
||||
checked={tags.includes(option.value)}
|
||||
checked={tags.includes(option.name)}
|
||||
/>
|
||||
<div className='px-1 system-sm-medium text-text-secondary'>
|
||||
{option.text}
|
||||
{option.label}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@ -14,13 +14,13 @@ export const getMarketplaceCollectionsAndPlugins = async (query?: CollectionsAnd
|
||||
let marketplaceCollections = [] as MarketplaceCollection[]
|
||||
let marketplaceCollectionPluginsMap = {} as Record<string, Plugin[]>
|
||||
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) => {
|
||||
|
||||
@ -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 (
|
||||
<div className='px-4 py-3 bg-components-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs cursor-pointer hover:bg-components-panel-on-panel-item-bg-hover'>
|
||||
<div className='pb-0.5 text-text-secondary system-md-semibold'>Notion Page Search</div>
|
||||
<div className='text-text-tertiary system-xs-regular line-clamp-2'>A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query.</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<div className='px-4 pt-2 pb-4'>
|
||||
<div className='mb-1 py-1'>
|
||||
<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'>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
{t('plugin.detailPanel.actionNum', { num: data.length })}
|
||||
{provider.is_team_authorization && provider.allow_delete && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button variant='primary' className='w-full'>{t('tools.auth.unauthorized')}</Button>
|
||||
{!provider.is_team_authorization && provider.allow_delete && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>{t('tools.auth.unauthorized')}</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<ActionCard />
|
||||
<ActionCard />
|
||||
<ActionCard />
|
||||
{data.map(tool => (
|
||||
<ToolItem
|
||||
key={`${currentPluginDetail.plugin_id}${tool.name}`}
|
||||
disabled={false}
|
||||
collection={provider}
|
||||
tool={tool}
|
||||
isBuiltIn={true}
|
||||
isModel={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={provider}
|
||||
onCancel={() => 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)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
<div className={cn('shrink-0 p-4 pb-3 border-b border-divider-subtle bg-components-panel-bg')}>
|
||||
<div className="flex">
|
||||
<Icon src={detail.declaration.icon} />
|
||||
<div className='overflow-hidden border-components-panel-border-subtle border rounded-xl'>
|
||||
<Icon src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} />
|
||||
</div>
|
||||
<div className="ml-3 w-0 grow">
|
||||
<div className="flex items-center h-5">
|
||||
<Title title={detail.declaration.label[locale]} />
|
||||
{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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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' />
|
||||
|
||||
@ -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}
|
||||
|
||||
118
web/app/components/plugins/plugin-page/empty/index.tsx
Normal file
118
web/app/components/plugins/plugin-page/empty/index.tsx
Normal file
@ -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)
|
||||
@ -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([])
|
||||
}
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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')}
|
||||
|
||||
86
web/app/components/plugins/plugin-page/install-info.tsx
Normal file
86
web/app/components/plugins/plugin-page/install-info.tsx
Normal file
@ -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
|
||||
@ -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={() => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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()}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
40
web/app/components/plugins/plugin-page/store.tsx
Normal file
40
web/app/components/plugins/plugin-page/store.tsx
Normal file
@ -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()
|
||||
},
|
||||
}))
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
26
web/app/components/plugins/update-plugin/from-github.tsx
Normal file
26
web/app/components/plugins/update-plugin/from-github.tsx
Normal file
@ -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)
|
||||
153
web/app/components/plugins/update-plugin/from-market-place.tsx
Normal file
153
web/app/components/plugins/update-plugin/from-market-place.tsx
Normal file
@ -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)
|
||||
@ -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)
|
||||
|
||||
361
web/app/components/tools/edit-custom-collection-modal/modal.tsx
Normal file
361
web/app/components/tools/edit-custom-collection-modal/modal.tsx
Normal file
@ -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)
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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 })),
|
||||
}))
|
||||
@ -37,7 +37,7 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
|
||||
}
|
||||
else {
|
||||
queryMarketplaceCollectionsAndPlugins()
|
||||
setPlugins([])
|
||||
setPlugins(undefined)
|
||||
}
|
||||
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, setPlugins])
|
||||
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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: {},
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@ -25,4 +25,5 @@ export type ToolDefaultValue = {
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
title: string
|
||||
params: Record<string, any>
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user