Compare commits

..

31 Commits

Author SHA1 Message Date
3107ec878a chore: edit metadata batch i18n 2025-02-20 16:06:10 +08:00
7d5fcfef4c feat: document dataset i18n 2025-02-20 15:22:13 +08:00
e862ab0def feat: document and tech params 2025-02-20 14:54:02 +08:00
15f80a72b8 feat: add popup 2025-02-20 11:45:36 +08:00
1a7de23864 feat: doc metadata can show edit and cancel 2025-02-20 11:00:38 +08:00
10fccd2b3f feat: metadata panel 2025-02-19 18:38:29 +08:00
b568947e00 feat: no data 2025-02-17 17:33:44 +08:00
7dcbb75839 chore: rename 2025-02-17 17:00:05 +08:00
ffdfcdd4a4 feat: apply to all selected 2025-02-17 16:57:59 +08:00
f4604bf6d0 feat: new meta data 2025-02-17 15:12:38 +08:00
3a72b76c32 file 2025-02-17 14:30:36 +08:00
49dd77e219 feat: edit beacon 2025-02-17 14:30:23 +08:00
b9f223d9d4 feat: modal row 2025-02-17 12:53:34 +08:00
3c4da03575 feat: eidt row item 2025-02-17 11:38:12 +08:00
7692476097 feat: edit modal struct 2025-02-14 17:58:19 +08:00
428438eeca feat: i18n 2025-02-14 16:39:31 +08:00
b7c546f2ad feat: dataset metadata collection 2025-02-14 16:24:49 +08:00
0ed892a747 feat: dataset metadata 2025-02-14 15:28:18 +08:00
5e2bd407a8 chore: select i18n and other 2025-02-14 14:01:03 +08:00
a4668e0ffc fix: manage btn hover 2025-02-14 11:21:48 +08:00
1ca79ea729 feat: select metadata 2025-02-14 11:18:40 +08:00
ebb6de5f52 feat: split main conten 2025-02-13 16:48:02 +08:00
2adc704463 feat: add back button 2025-02-13 16:19:30 +08:00
b74f1b3c07 chore: i18n 2025-02-13 15:50:18 +08:00
f60e650400 feat: create metadata modal 2025-02-13 15:34:09 +08:00
83d0142641 fix: refresh after install plugin (#13593) 2025-02-12 15:51:55 +08:00
56c7f49625 fix: add langgenius to list tool api (#13578) 2025-02-12 15:37:10 +08:00
7c1d842cfe (1.0) fix: invalid default model provider (#13572) 2025-02-12 14:21:58 +08:00
2ea3b64a45 Feat: tool setting support variable (#13465)
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
2025-02-12 12:54:10 +08:00
824f8d8994 chore: add debug doc link (#13537) 2025-02-11 18:32:01 +08:00
31c17e6378 fix: installed plugin not show upgrade (#13523) 2025-02-11 14:08:43 +08:00
93 changed files with 1646 additions and 543 deletions

View File

@ -6,7 +6,6 @@ on:
- "main"
- "deploy/dev"
- "plugins/beta"
- "dev/plugin-deploy"
release:
types: [published]
@ -141,16 +140,3 @@ jobs:
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env[matrix.image_name_env] }}:${{ steps.meta.outputs.version }}
- name: print context var
uses: actions/checkout@v4
- name: deploy pod in plugin env
if: github.ref == 'refs/heads/dev/plugin-deploy'
env:
IMAGEHASH: ${{ github.sha }}
APICMD: "${{ secrets.PLUGIN_CD_API_CURL }}"
WEBCMD: "${{ secrets.PLUGIN_CD_WEB_CURL }}"
run: |
bash -c "${APICMD/yourNewVersion/$IMAGEHASH}"
bash -c "${WEBCMD/yourNewVersion/$IMAGEHASH}"

View File

@ -1,23 +0,0 @@
name: Deploy Plugin Dev
on:
workflow_run:
workflows: ["Build and Push API & Web"]
branches:
- "dev/plugin-deploy"
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: "echo 123"

View File

@ -384,6 +384,7 @@ class AdvancedChatAppGenerateTaskPipeline:
task_id=self._application_generate_entity.task_id,
workflow_node_execution=workflow_node_execution,
)
session.commit()
if node_finish_resp:
yield node_finish_resp

View File

@ -387,7 +387,6 @@ class WorkflowBasedAppRunner(AppRunner):
status=event.status,
data=event.data,
metadata=event.metadata,
node_id=event.node_id,
)
)
elif isinstance(event, ParallelBranchRunStartedEvent):

View File

@ -331,7 +331,6 @@ class QueueAgentLogEvent(AppQueueEvent):
status: str
data: Mapping[str, Any]
metadata: Optional[Mapping[str, Any]] = None
node_id: str
class QueueNodeRetryEvent(QueueNodeStartedEvent):

View File

@ -719,7 +719,6 @@ class AgentLogStreamResponse(StreamResponse):
status: str
data: Mapping[str, Any]
metadata: Optional[Mapping[str, Any]] = None
node_id: str
event: StreamEvent = StreamEvent.AGENT_LOG
data: Data

View File

@ -864,6 +864,5 @@ class WorkflowCycleManage:
status=event.status,
data=event.data,
metadata=event.metadata,
node_id=event.node_id,
),
)

View File

@ -65,8 +65,7 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):
retries += 1
if retries <= max_retries:
time.sleep(BACKOFF_FACTOR * (2 ** (retries - 1)))
raise MaxRetriesExceededError(
f"Reached maximum retries ({max_retries}) for URL {url}")
raise MaxRetriesExceededError(f"Reached maximum retries ({max_retries}) for URL {url}")
def get(url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs):

View File

@ -20,6 +20,7 @@ from core.model_runtime.model_providers.__base.text_embedding_model import TextE
from core.model_runtime.model_providers.__base.tts_model import TTSModel
from core.model_runtime.schema_validators.model_credential_schema_validator import ModelCredentialSchemaValidator
from core.model_runtime.schema_validators.provider_credential_schema_validator import ProviderCredentialSchemaValidator
from core.plugin.entities.plugin import ModelProviderID
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
from core.plugin.manager.asset import PluginAssetManager
from core.plugin.manager.model import PluginModelManager
@ -112,6 +113,9 @@ class ModelProviderFactory:
:param provider: provider name
:return: provider schema
"""
if "/" not in provider:
provider = str(ModelProviderID(provider))
# fetch plugin model providers
plugin_model_provider_entities = self.get_plugin_model_providers()
@ -363,4 +367,4 @@ class ModelProviderFactory:
plugin_id = "/".join(provider.split("/")[:-1])
provider_name = provider.split("/")[-1]
return plugin_id, provider_name
return str(plugin_id), provider_name

View File

@ -169,6 +169,21 @@ class GenericProviderID:
return f"{self.organization}/{self.plugin_name}"
class ModelProviderID(GenericProviderID):
def __init__(self, value: str, is_hardcoded: bool = False) -> None:
super().__init__(value, is_hardcoded)
if self.organization == "langgenius" and self.provider_name == "google":
self.provider_name = "gemini"
class ToolProviderID(GenericProviderID):
def __init__(self, value: str, is_hardcoded: bool = False) -> None:
super().__init__(value, is_hardcoded)
if self.organization == "langgenius":
if self.provider_name in ["jina", "siliconflow"]:
self.provider_name = f"{self.provider_name}_tool"
class PluginDependency(BaseModel):
class Type(enum.StrEnum):
Github = PluginInstallationSource.Github.value
@ -197,9 +212,3 @@ class PluginDependency(BaseModel):
type: Type
value: Github | Marketplace | Package
current_identifier: Optional[str] = None
class MissingPluginDependency(BaseModel):
plugin_unique_identifier: str
current_identifier: Optional[str] = None

View File

@ -3,7 +3,6 @@ from collections.abc import Sequence
from core.plugin.entities.bundle import PluginBundleDependency
from core.plugin.entities.plugin import (
GenericProviderID,
MissingPluginDependency,
PluginDeclaration,
PluginEntity,
PluginInstallation,
@ -176,16 +175,14 @@ class PluginInstallationManager(BasePluginManager):
headers={"Content-Type": "application/json"},
)
def fetch_missing_dependencies(
self, tenant_id: str, plugin_unique_identifiers: list[str]
) -> list[MissingPluginDependency]:
def fetch_missing_dependencies(self, tenant_id: str, plugin_unique_identifiers: list[str]) -> list[str]:
"""
Fetch missing dependencies
"""
return self._request_with_plugin_daemon_response(
"POST",
f"plugin/{tenant_id}/management/installation/missing",
list[MissingPluginDependency],
list[str],
data={"plugin_unique_identifiers": plugin_unique_identifiers},
headers={"Content-Type": "application/json"},
)

View File

@ -160,8 +160,8 @@ class ToolManager:
"""
get the tool runtime
:param provider_type: the type of the provider
:param provider_name: the name of the provider
:param provider_type: the type of the provider
:param provider_name: the name of the provider
:param tool_name: the name of the tool
:return: the tool

View File

@ -207,7 +207,6 @@ class AgentLogEvent(BaseAgentEvent):
status: str = Field(..., description="status")
data: Mapping[str, Any] = Field(..., description="data")
metadata: Optional[Mapping[str, Any]] = Field(default=None, description="metadata")
node_id: str = Field(..., description="agent node id")
InNodeEvent = BaseNodeEvent | BaseParallelBranchEvent | BaseIterationEvent | BaseAgentEvent

View File

@ -18,7 +18,6 @@ from core.workflow.entities.node_entities import AgentNodeStrategyInit, NodeRunM
from core.workflow.entities.variable_pool import VariablePool, VariableValue
from core.workflow.graph_engine.condition_handlers.condition_manager import ConditionManager
from core.workflow.graph_engine.entities.event import (
BaseAgentEvent,
BaseIterationEvent,
GraphEngineEvent,
GraphRunFailedEvent,
@ -502,7 +501,7 @@ class GraphEngine:
break
yield event
if not isinstance(event, BaseAgentEvent) and event.parallel_id == parallel_id:
if event.parallel_id == parallel_id:
if isinstance(event, ParallelBranchRunSucceededEvent):
succeeded_count += 1
if succeeded_count == len(futures):

View File

@ -8,12 +8,12 @@ from core.model_manager import ModelManager
from core.model_runtime.entities.model_entities import ModelType
from core.plugin.manager.exc import PluginDaemonClientSideError
from core.plugin.manager.plugin import PluginInstallationManager
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType
from core.tools.entities.tool_entities import ToolProviderType
from core.tools.tool_manager import ToolManager
from core.workflow.entities.node_entities import NodeRunResult
from core.workflow.entities.variable_pool import VariablePool
from core.workflow.enums import SystemVariableKey
from core.workflow.nodes.agent.entities import AgentNodeData, ParamsAutoGenerated
from core.workflow.nodes.agent.entities import AgentNodeData
from core.workflow.nodes.base.entities import BaseNodeData
from core.workflow.nodes.enums import NodeType
from core.workflow.nodes.event.event import RunCompletedEvent
@ -156,37 +156,16 @@ class AgentNode(ToolNode):
value = cast(list[dict[str, Any]], value)
value = [tool for tool in value if tool.get("enabled", False)]
for tool in value:
if "schemas" in tool:
tool.pop("schemas")
parameters = tool.get("parameters", {})
if all(isinstance(v, dict) for _, v in parameters.items()):
params = {}
for key, param in parameters.items():
if param.get("auto", ParamsAutoGenerated.OPEN.value) == ParamsAutoGenerated.CLOSE.value:
params[key] = param.get("value", {}).get("value", "")
else:
params[key] = None
parameters = params
tool["settings"] = {k: v.get("value", None) for k, v in tool.get("settings", {}).items()}
tool["parameters"] = parameters
if not for_log:
if parameter.type == "array[tools]":
value = cast(list[dict[str, Any]], value)
tool_value = []
for tool in value:
provider_type = ToolProviderType(tool.get("type", ToolProviderType.BUILT_IN.value))
setting_params = tool.get("settings", {})
parameters = tool.get("parameters", {})
manual_input_params = [key for key, value in parameters.items() if value is not None]
parameters = {**parameters, **setting_params}
entity = AgentToolEntity(
provider_id=tool.get("provider_name", ""),
provider_type=provider_type,
provider_type=ToolProviderType.BUILT_IN,
tool_name=tool.get("tool_name", ""),
tool_parameters=parameters,
tool_parameters=tool.get("parameters", {}),
plugin_unique_identifier=tool.get("plugin_unique_identifier", None),
)
@ -199,27 +178,13 @@ class AgentNode(ToolNode):
tool_runtime.entity.description.llm = (
extra.get("descrption", "") or tool_runtime.entity.description.llm
)
for params in tool_runtime.entity.parameters:
params.form = (
ToolParameter.ToolParameterForm.FORM
if params.name in manual_input_params
else params.form
)
if tool_runtime.entity.parameters:
manual_input_value = {
key: value for key, value in parameters.items() if key in manual_input_params
tool_value.append(
{
**tool_runtime.entity.model_dump(mode="json"),
"runtime_parameters": tool_runtime.runtime.runtime_parameters,
}
runtime_parameters = {
**tool_runtime.runtime.runtime_parameters,
**manual_input_value,
}
tool_value.append(
{
**tool_runtime.entity.model_dump(mode="json"),
"runtime_parameters": runtime_parameters,
"provider_type": provider_type.value,
}
)
)
value = tool_value
if parameter.type == "model-selector":
value = cast(dict[str, Any], value)

View File

@ -1,4 +1,3 @@
from enum import Enum
from typing import Any, Literal, Union
from pydantic import BaseModel
@ -17,8 +16,3 @@ class AgentNodeData(BaseNodeData):
type: Literal["mixed", "variable", "constant"]
agent_parameters: dict[str, AgentInput]
class ParamsAutoGenerated(Enum):
CLOSE = 0
OPEN = 1

View File

@ -64,7 +64,7 @@ class EndStreamGeneratorRouter:
node_type = node.get("data", {}).get("type")
if (
variable_selector.value_selector not in value_selectors
and (node_type in (NodeType.LLM.value, NodeType.AGENT.value))
and node_type == NodeType.LLM.value
and variable_selector.value_selector[1] == "text"
):
value_selectors.append(list(variable_selector.value_selector))

View File

@ -44,13 +44,11 @@ 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,
@ -58,8 +56,7 @@ 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 = (
self._fetch_files(
@ -181,15 +178,12 @@ 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,8 +204,7 @@ 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={},
@ -224,15 +217,13 @@ 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:
@ -273,8 +264,7 @@ 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(
@ -315,5 +305,4 @@ class QuestionClassifierNode(LLMNode):
)
else:
raise InvalidModelTypeError(
f"Model mode {model_mode} not support.")
raise InvalidModelTypeError(f"Model mode {model_mode} not support.")

View File

@ -338,7 +338,6 @@ class ToolNode(BaseNode[ToolNodeData]):
data=message.message.data,
label=message.message.label,
metadata=message.message.metadata,
node_id=self.node_id,
)
# check if the agent log is already in the list

View File

@ -198,7 +198,7 @@ app_site_fields = {
"use_icon_as_answer_icon": fields.Boolean,
}
leaked_dependency_fields = {"type": fields.String, "value": fields.Raw, "current_identifier": fields.String}
leaked_dependency_fields = {"type": fields.String, "value": fields.Raw}
app_import_fields = {
"id": fields.String,

View File

@ -748,7 +748,7 @@ class DatasetKeywordTable(db.Model): # type: ignore[name-defined]
if keyword_table_text:
return json.loads(keyword_table_text.decode("utf-8"), cls=SetDecoder)
return None
except Exception:
except Exception as e:
logging.exception(f"Failed to load keyword table from file: {file_key}")
return None

View File

@ -1,27 +0,0 @@
from datetime import datetime
from sqlalchemy.orm import Mapped
from extensions.ext_database import db
from models.base import Base
from .types import StringUUID
class StagingAccountWhitelist(Base):
__tablename__ = "staging_account_whitelists"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="staging_account_whitelist_pkey"),
db.Index("account_email_idx", "email"),
)
id: Mapped[str] = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
email: Mapped[str] = db.Column(db.String(255), nullable=False)
disabled: Mapped[bool] = db.Column(db.Boolean, nullable=False, server_default=db.text("false"))
created_at: Mapped[datetime] = db.Column(
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
)
updated_at: Mapped[datetime] = db.Column(
db.DateTime, nullable=False, server_default=db.text("CURRENT_TIMESTAMP(0)")
)

View File

@ -33,7 +33,6 @@ from models.account import (
TenantStatus,
)
from models.model import DifySetup
from models.staging import StagingAccountWhitelist
from services.billing_service import BillingService
from services.errors.account import (
AccountAlreadyInTenantError,
@ -349,9 +348,6 @@ class AccountService:
@staticmethod
def login(account: Account, *, ip_address: Optional[str] = None) -> TokenPair:
if not AccountService.verify_account_whitelist(account.email):
raise ValueError("Account is not whitelisted")
if ip_address:
AccountService.update_login_info(account=account, ip_address=ip_address)
@ -383,9 +379,6 @@ class AccountService:
if not account:
raise ValueError("Invalid account")
if not AccountService.verify_account_whitelist(account.email):
raise ValueError("Account is not whitelisted")
# Generate new access token and refresh token
new_access_token = AccountService.get_account_jwt_token(account)
new_refresh_token = _generate_refresh_token()
@ -395,16 +388,6 @@ class AccountService:
return TokenPair(access_token=new_access_token, refresh_token=new_refresh_token)
@staticmethod
def verify_account_whitelist(email: str) -> bool:
with Session(db.engine) as session:
return (
session.query(StagingAccountWhitelist)
.filter(StagingAccountWhitelist.email == email, StagingAccountWhitelist.disabled == False)
.first()
is not None
)
@staticmethod
def load_logged_in_account(*, account_id: str):
return AccountService.load_user(account_id)
@ -449,13 +432,9 @@ class AccountService:
def send_email_code_login_email(
cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US"
):
if email:
if not AccountService.verify_account_whitelist(email):
raise ValueError("Account is not whitelisted")
elif account:
if not AccountService.verify_account_whitelist(account.email):
raise ValueError("Account is not whitelisted")
email = account.email if account else email
if email is None:
raise ValueError("Email must be provided.")
if cls.email_code_login_rate_limiter.is_rate_limited(email):
from controllers.console.auth.error import EmailCodeLoginRateLimitExceededError

View File

@ -1,6 +1,5 @@
import logging
import uuid
from collections.abc import Mapping
from enum import StrEnum
from typing import Optional
from urllib.parse import urlparse
@ -257,16 +256,6 @@ class AppDslService:
check_dependencies_pending_data = None
if dependencies:
check_dependencies_pending_data = [PluginDependency.model_validate(d) for d in dependencies]
elif imported_version <= "0.1.5":
if "workflow" in data:
graph = data.get("workflow", {}).get("graph", {})
dependencies_list = self._extract_dependencies_from_workflow_graph(graph)
else:
dependencies_list = self._extract_dependencies_from_model_config(data.get("model_config", {}))
check_dependencies_pending_data = DependenciesAnalysisService.generate_latest_dependencies(
dependencies_list
)
# Create or update app
app = self._create_or_update_app(
@ -569,7 +558,7 @@ class AppDslService:
raise ValueError("Missing app configuration, please check.")
export_data["model_config"] = app_model_config.to_dict()
dependencies = cls._extract_dependencies_from_model_config(app_model_config.to_dict())
dependencies = cls._extract_dependencies_from_model_config(app_model_config)
export_data["dependencies"] = [
jsonable_encoder(d.model_dump())
for d in DependenciesAnalysisService.generate_dependencies(
@ -585,16 +574,6 @@ class AppDslService:
:return: dependencies list format like ["langgenius/google"]
"""
graph = workflow.graph_dict
dependencies = cls._extract_dependencies_from_workflow_graph(graph)
return dependencies
@classmethod
def _extract_dependencies_from_workflow_graph(cls, graph: Mapping) -> list[str]:
"""
Extract dependencies from workflow graph
:param graph: Workflow graph
:return: dependencies list format like ["langgenius/google"]
"""
dependencies = []
for node in graph.get("nodes", []):
try:
@ -668,24 +647,24 @@ class AppDslService:
return dependencies
@classmethod
def _extract_dependencies_from_model_config(cls, model_config: Mapping) -> list[str]:
def _extract_dependencies_from_model_config(cls, model_config: AppModelConfig) -> list[str]:
"""
Extract dependencies from model config
:param model_config: model config dict
:return: dependencies list format like ["langgenius/google"]
:param model_config: AppModelConfig instance
:return: dependencies list format like ["langgenius/google:1.0.0@abcdef1234567890"]
"""
dependencies = []
try:
# completion model
model_dict = model_config.get("model", {})
model_dict = model_config.model_dict
if model_dict:
dependencies.append(
DependenciesAnalysisService.analyze_model_provider_dependency(model_dict.get("provider", ""))
)
# reranking model
dataset_configs = model_config.get("dataset_configs", {})
dataset_configs = model_config.dataset_configs_dict
if dataset_configs:
for dataset_config in dataset_configs.get("datasets", {}).get("datasets", []):
if dataset_config.get("reranking_model"):
@ -698,7 +677,7 @@ class AppDslService:
)
# tools
agent_configs = model_config.get("agent_mode", {})
agent_configs = model_config.agent_mode_dict
if agent_configs:
for agent_config in agent_configs.get("tools", []):
dependencies.append(

View File

@ -1,4 +1,3 @@
from core.helper import marketplace
from core.plugin.entities.plugin import GenericProviderID, PluginDependency, PluginInstallationSource
from core.plugin.manager.plugin import PluginInstallationManager
@ -13,8 +12,6 @@ class DependenciesAnalysisService:
"""
try:
tool_provider_id = GenericProviderID(tool_id)
if tool_id in ["jina", "siliconflow"]:
tool_provider_id.plugin_name = tool_provider_id.plugin_name + "_tool"
return tool_provider_id.plugin_id
except Exception as e:
raise e
@ -28,9 +25,6 @@ class DependenciesAnalysisService:
"""
try:
generic_provider_id = GenericProviderID(model_provider_id)
if model_provider_id == "google":
generic_provider_id.plugin_name = "gemini"
return generic_provider_id.plugin_id
except Exception as e:
raise e
@ -45,22 +39,15 @@ class DependenciesAnalysisService:
required_plugin_unique_identifiers.append(dependency.value.plugin_unique_identifier)
manager = PluginInstallationManager()
# get leaked dependencies
missing_plugins = manager.fetch_missing_dependencies(tenant_id, required_plugin_unique_identifiers)
missing_plugin_unique_identifiers = {plugin.plugin_unique_identifier: plugin for plugin in missing_plugins}
missing_plugin_unique_identifiers = manager.fetch_missing_dependencies(
tenant_id, required_plugin_unique_identifiers
)
leaked_dependencies = []
for dependency in dependencies:
unique_identifier = dependency.value.plugin_unique_identifier
if unique_identifier in missing_plugin_unique_identifiers:
leaked_dependencies.append(
PluginDependency(
type=dependency.type,
value=dependency.value,
current_identifier=missing_plugin_unique_identifiers[unique_identifier].current_identifier,
)
)
leaked_dependencies.append(dependency)
return leaked_dependencies
@ -111,18 +98,3 @@ class DependenciesAnalysisService:
raise ValueError(f"Unknown plugin source: {plugin.source}")
return result
@classmethod
def generate_latest_dependencies(cls, dependencies: list[str]) -> list[PluginDependency]:
"""
Generate the latest version of dependencies
"""
dependencies = list(set(dependencies))
deps = marketplace.batch_fetch_plugin_manifests(dependencies)
return [
PluginDependency(
type=PluginDependency.Type.Marketplace,
value=PluginDependency.Marketplace(marketplace_plugin_unique_identifier=dep.latest_package_identifier),
)
for dep in deps
]

View File

@ -1,7 +1,6 @@
import datetime
import json
import logging
import sys
import time
from collections.abc import Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor
@ -418,8 +417,6 @@ class PluginMigration:
logger.info("Uninstall plugins")
sys.exit(-1)
# get installation
try:
installation = manager.list_plugins(fake_tenant_id)

View File

@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from configs import dify_config
from core.helper.position_helper import is_filtered
from core.model_runtime.utils.encoders import jsonable_encoder
from core.plugin.entities.plugin import GenericProviderID
from core.plugin.entities.plugin import GenericProviderID, ToolProviderID
from core.plugin.manager.exc import PluginDaemonClientSideError
from core.tools.builtin_tool.providers._positions import BuiltinToolProviderSort
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
@ -240,10 +240,7 @@ class BuiltinToolManageService:
# rewrite db_providers
for db_provider in db_providers:
try:
GenericProviderID(db_provider.provider)
except Exception:
db_provider.provider = f"langgenius/{db_provider.provider}/{db_provider.provider}"
db_provider.provider = str(ToolProviderID(db_provider.provider))
# find provider
def find_provider(provider):
@ -258,7 +255,7 @@ class BuiltinToolManageService:
include_set=dify_config.POSITION_TOOL_INCLUDES_SET, # type: ignore
exclude_set=dify_config.POSITION_TOOL_EXCLUDES_SET, # type: ignore
data=provider_controller,
name_func=lambda x: x.entity.identity.name,
name_func=lambda x: x.identity.name,
):
continue

View File

@ -54,7 +54,7 @@ class ToolTransformService:
@staticmethod
def repack_provider(tenant_id: str, provider: Union[dict, ToolProviderApiEntity]):
"""
repack provider
repack provider
:param provider: the provider dict
"""

View File

@ -68,8 +68,7 @@ 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(
@ -124,8 +123,7 @@ 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(

View File

@ -18,14 +18,6 @@ from models.enums import UserFrom
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
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"}
def test_http_request_node_binary_file(monkeypatch):
data = HttpRequestNodeData(
title="test",
@ -191,8 +183,7 @@ 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(

View File

@ -464,8 +464,6 @@ services:
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
APP_API_URL: ${APP_API_URL:-}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-}
MARKETPLACE_URL: ${MARKETPLACE_URL:-}
SENTRY_DSN: ${WEB_SENTRY_DSN:-}
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}

View File

@ -20,7 +20,9 @@ import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
// import DatasetMetadataDrawer from '@/app/components/datasets/metadata/dataset-metadata-drawer'
// import MetaDataDocument from '@/app/components/datasets/metadata/metadata-document'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@ -29,6 +31,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 { DataType } from '@/app/components/datasets/metadata/types'
const Container = () => {
const { t } = useTranslation()
@ -81,8 +84,54 @@ const Container = () => {
return router.replace('/apps')
}, [currentWorkspace, router])
const [isBuiltInEnabled, setIsBuiltInEnabled] = useState(false)
const [userMetadata, setUserMetadata] = useState([
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
])
return (
<div ref={containerRef} className='grow relative flex flex-col bg-background-body overflow-y-auto scroll-container'>
<div className='flex justify-end mt-[300px] mr-[100px]'>
{/* <MetaDataDocument /> */}
{/* <SelectMetadataModal trigger={<Button className='w-[200px]'>select</Button>} onSave={(data) => { console.log(data) }} />
<CreateModal trigger={<Button className='w-[200px]'>add</Button>} hasBack onSave={(data) => { console.log(data) }} />
<Button className='flex w-[200px]' size="medium" onClick={() => setShowExternalApiPanel(true)}>
Metadata
</Button> */}
{/* <DatasetMetadataDrawer
userMetadata={userMetadata}
onChange={setUserMetadata}
builtInMetadata={[
{ id: '1', name: 'name1', type: DataType.string, valueLength: 1 },
{ id: '2', name: 'name2', type: DataType.number, valueLength: 2 },
{ id: '3', name: 'name3', type: DataType.time, valueLength: 3 },
]}
isBuiltInEnabled={isBuiltInEnabled}
onIsBuiltInEnabledChange={setIsBuiltInEnabled}
onClose={() => { }}
/> */}
<EditMetadataBatchModal
documentNum={20}
list={[
{
id: '1', name: 'name1', type: DataType.string, value: 'aaa',
},
{
id: '2', name: 'name2', type: DataType.number, value: 'ccc', isMultipleValue: true, isUpdated: true,
},
{
id: '2.1', name: 'num v', type: DataType.number, value: 10,
},
{
id: '3', name: 'name3', type: DataType.time, value: '', isUpdated: true, // updateType: UpdateType.delete,
},
]}
onHide={() => { }}
onChange={(list, newList, isApplyToAllSelectDocument) => { console.log(list, newList, isApplyToAllSelectDocument) }}
/>
</div>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-background-body z-10 flex-wrap gap-y-2'>
<TabSliderNew
value={activeTab}

View File

@ -8,7 +8,7 @@ import { logout } from '@/service/common'
import { useAppContext } from '@/context/app-context'
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
export type IAppSelector = {
export interface IAppSelector {
isMobile: boolean
}

View File

@ -33,7 +33,7 @@ import { useChatContext } from '@/app/components/base/chat/chat/context'
const MAX_DEPTH = 3
export type IGenerationItemProps = {
export interface IGenerationItemProps {
isWorkflow?: boolean
workflowProcessData?: WorkflowProcess
className?: string

View File

@ -53,15 +53,18 @@ export default function Drawer({
/>
<div className={cn('relative z-50 flex flex-col justify-between bg-components-panel-bg w-full max-w-sm p-6 overflow-hidden text-left align-middle shadow-xl', panelClassname)}>
<>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-text-primary"
>
{title}
</Dialog.Title>}
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
<XMarkIcon className='w-4 h-4 text-text-tertiary' onClick={onClose} />
</Dialog.Title>}
<div className='flex justify-between'>
{title && <Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-text-primary"
>
{title}
</Dialog.Title>}
{showClose && <Dialog.Title className="flex items-center mb-4" as="div">
<XMarkIcon className='w-4 h-4 text-text-tertiary cursor-pointer' onClick={onClose} />
</Dialog.Title>}
</div>
{description && <Dialog.Description className='text-text-tertiary text-xs font-normal mt-2'>{description}</Dialog.Description>}
{children}
</>

View File

@ -12,10 +12,13 @@ export type InputNumberProps = {
max?: number
min?: number
defaultValue?: number
wrapClassName?: string
controlWrapClassName?: string
controlClassName?: string
} & Omit<InputProps, 'value' | 'onChange' | 'size' | 'min' | 'max' | 'defaultValue'>
export const InputNumber: FC<InputNumberProps> = (props) => {
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, ...rest } = props
const { unit, className, onChange, amount = 1, value, size = 'md', max, min, defaultValue, wrapClassName, controlWrapClassName, controlClassName, ...rest } = props
const isValidValue = (v: number) => {
if (max && v > max)
@ -46,7 +49,7 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
onChange(newValue)
}
return <div className='flex'>
return <div className={classNames('flex', wrapClassName)}>
<Input {...rest}
// disable default controller
type='text'
@ -68,16 +71,18 @@ export const InputNumber: FC<InputNumberProps> = (props) => {
}}
unit={unit}
/>
<div className='flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs'>
<div className={classNames('flex flex-col bg-components-input-bg-normal rounded-r-md border-l border-divider-subtle text-text-tertiary focus:shadow-xs', controlWrapClassName)}>
<button onClick={inc} className={classNames(
size === 'sm' ? 'pt-1' : 'pt-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
controlClassName,
)}>
<RiArrowUpSLine className='size-3' />
</button>
<button onClick={dec} className={classNames(
size === 'sm' ? 'pb-1' : 'pb-1.5',
'px-1.5 hover:bg-components-input-bg-hover',
controlClassName,
)}>
<RiArrowDownSLine className='size-3' />
</button>

View File

@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import Button from '../button'
import { RiCloseLine } from '@remixicon/react'
type Props = {
title: string
className?: string
beforeHeader?: React.ReactNode
onClose: () => void
hideCloseBtn?: boolean
onConfirm: () => void
children: React.ReactNode
}
const ModalLikeWrap: FC<Props> = ({
title,
className,
beforeHeader,
children,
onClose,
hideCloseBtn,
onConfirm,
}) => {
const { t } = useTranslation()
return (
<div className={cn('w-[320px] px-3 pt-3.5 pb-4 bg-components-panel-bg shadow-xl rounded-2xl border-[0.5px] border-components-panel-border', className)}>
{beforeHeader || null}
<div className='mb-1 flex h-6 items-center justify-between'>
<div className='system-xl-semibold text-text-primary'>{title}</div>
{!hideCloseBtn && (
<div
className='p-1.5 text-text-tertiary cursor-pointer'
onClick={onClose}
>
<RiCloseLine className='size-4' />
</div>
)}
</div>
<div className='mt-2'>{children}</div>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button
onClick={onConfirm}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
)
}
export default React.memo(ModalLikeWrap)

View File

@ -12,7 +12,7 @@ type Props = {
}
const maxTopK = (() => {
const configValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10)
const configValue = parseInt(globalThis.document?.body?.getAttribute('data-public-top-k-max-value') || '', 10)
if (configValue && !isNaN(configValue))
return configValue
return 10
@ -33,7 +33,7 @@ const TopKItem: FC<Props> = ({
}) => {
const { t } = useTranslation()
const handleParamChange = (key: string, value: number) => {
let notOutRangeValue = Number.parseFloat(value.toFixed(2))
let notOutRangeValue = parseFloat(value.toFixed(2))
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)

View File

@ -51,11 +51,12 @@ const Toast = ({
'top-0',
'right-0',
)}>
<div className={`absolute inset-0 opacity-40 ${(type === 'success' && 'bg-toast-success-bg')
<div className={`absolute inset-0 opacity-40 ${
(type === 'success' && 'bg-toast-success-bg')
|| (type === 'warning' && 'bg-toast-warning-bg')
|| (type === 'error' && 'bg-toast-error-bg')
|| (type === 'info' && 'bg-toast-info-bg')
}`}
}`}
/>
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
<div className={`flex justify-center items-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
@ -64,11 +65,8 @@ const Toast = ({
{type === 'warning' && <RiAlertFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-warning-secondary`} aria-hidden="true" />}
{type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 grow z-10`}>
<div className='flex items-center gap-1'>
<div className='text-text-primary system-sm-semibold'>{message}</div>
{customComponent}
</div>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 grow`}>
<div className='text-text-primary system-sm-semibold'>{message}</div>
{children && <div className='text-text-secondary system-xs-regular'>
{children}
</div>
@ -80,7 +78,7 @@ const Toast = ({
</ActionButton>)
}
</div>
</div >
</div>
}
export const ToastProvider = ({
@ -132,7 +130,7 @@ Toast.notify = ({
root.render(
<ToastContext.Provider value={{
notify: () => { },
notify: () => {},
close: () => {
if (holder) {
root.unmount()

View File

@ -39,7 +39,7 @@ export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) =>
}
export const MaxLengthInput: FC<InputNumberProps> = (props) => {
const maxValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
const maxValue = parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
const { t } = useTranslation()
return <FormField label={<div className='system-sm-semibold mb-1'>

View File

@ -80,7 +80,7 @@ export const useSegmentListContext = (selector: (value: SegmentListContextValue)
return useContextSelector(SegmentListContext, selector)
}
type ICompletedProps = {
interface ICompletedProps {
embeddingAvailable: boolean
showNewSegmentModal: boolean
onNewSegmentModalChange: (state: boolean) => void

View File

@ -106,8 +106,8 @@ const ModifyRetrievalModal: FC<Props> = ({
borderColor: 'rgba(0, 0, 0, 0.05)',
}}
>
<Button className='mr-2 shrink-0' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
<Button className='mr-2 flex-shrink-0' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='flex-shrink-0' onClick={handleSave} >{t('common.operation.save')}</Button>
</div>
</div>
)

View File

@ -15,7 +15,7 @@ import { asyncRunSafe } from '@/utils'
import { RETRIEVE_METHOD, type RetrievalConfig } from '@/types/app'
import promptS from '@/app/components/app/configuration/config-prompt/style.module.css'
type TextAreaWithButtonIProps = {
interface TextAreaWithButtonIProps {
datasetId: string
onUpdateList: () => void
setHitResult: (res: HitTestingResponse) => void

View File

@ -0,0 +1,31 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Button from '../../base/button'
import { RiAddLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
onClick?: () => void
}
const AddedMetadataButton: FC<Props> = ({
className,
onClick,
}) => {
const { t } = useTranslation()
return (
<Button
className={cn('w-full flex items-center', className)}
size='small'
variant='tertiary'
onClick={onClick}
>
<RiAddLine className='mr-1 size-3.5' />
<div>{t('dataset.metadata.addMetadata')}</div>
</Button>
)
}
export default React.memo(AddedMetadataButton)

View File

@ -0,0 +1,87 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { DataType } from './types'
import ModalLikeWrap from '../../base/modal-like-wrap'
import Field from './field'
import OptionCard from '../../workflow/nodes/_base/components/option-card'
import Input from '@/app/components/base/input'
import { RiArrowLeftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'dataset.metadata.createMetadata'
export type Props = {
onSave: (data: any) => void
hasBack?: boolean
onBack?: () => void
}
const CreateContent: FC<Props> = ({
hasBack,
onBack,
onSave,
}) => {
const { t } = useTranslation()
const [type, setType] = useState(DataType.string)
const handleTypeChange = useCallback((newType: DataType) => {
return () => setType(newType)
}, [setType])
const [name, setName] = useState('')
const handleNameChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value)
}, [setName])
const handleSave = useCallback(() => {
onSave({
type,
name,
})
}, [onSave, type, name])
return (
<ModalLikeWrap
title={t(`${i18nPrefix}.title`)}
onClose={() => { }}
onConfirm={handleSave}
hideCloseBtn={hasBack}
beforeHeader={hasBack && (
<div className='relative left-[-4px] mb-1 flex items-center py-1 space-x-1 text-text-accent cursor-pointer' onClick={onBack}>
<RiArrowLeftLine className='size-4' />
<div className='system-xs-semibold-uppercase'>{t(`${i18nPrefix}.back`)}</div>
</div>
)}
>
<div className='space-y-3'>
<Field label={t(`${i18nPrefix}.type`)}>
<div className='grid grid-cols-3 gap-2'>
<OptionCard
title='String'
selected={type === DataType.string}
onSelect={handleTypeChange(DataType.string)}
/>
<OptionCard
title='Number'
selected={type === DataType.number}
onSelect={handleTypeChange(DataType.number)}
/>
<OptionCard
title='Time'
selected={type === DataType.time}
onSelect={handleTypeChange(DataType.time)}
/>
</div>
</Field>
<Field label={t(`${i18nPrefix}.name`)}>
<Input
value={name}
onChange={handleNameChange}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
</div>
</ModalLikeWrap>
)
}
export default React.memo(CreateContent)

View File

@ -0,0 +1,43 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
type Props = {
onSave: (data: any) => void
trigger: React.ReactNode
popupLeft?: number
} & CreateContentProps
const CreateMetadataModal: FC<Props> = ({
trigger,
popupLeft = 20,
...createContentProps
}) => {
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='left-start'
offset={{
mainAxis: popupLeft,
crossAxis: -38,
}}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<CreateContent {...createContentProps} />
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(CreateMetadataModal)

View File

@ -0,0 +1,228 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import type { MetadataItemWithValueLength } from './types'
import Drawer from '../../base/drawer'
import Button from '@/app/components/base/button'
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { getIcon } from './utils/get-icon'
import cn from '@/utils/classnames'
import Modal from '../../base/modal'
import Field from './field'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import Switch from '../../base/switch'
import Tooltip from '../../base/tooltip'
import CreateModal from '@/app/components/datasets/metadata/create-metadata-modal'
import { useBoolean, useHover } from 'ahooks'
import Confirm from '@/app/components/base/confirm'
const i18nPrefix = 'dataset.metadata.datasetMetadata'
type Props = {
userMetadata: MetadataItemWithValueLength[]
builtInMetadata: MetadataItemWithValueLength[]
isBuiltInEnabled: boolean
onIsBuiltInEnabledChange: (value: boolean) => void
onClose: () => void
onChange: (data: MetadataItemWithValueLength[]) => void
}
type ItemProps = {
readonly?: boolean
disabled?: boolean
payload: MetadataItemWithValueLength
onRename?: () => void
onDelete?: () => void
}
const Item: FC<ItemProps> = ({
readonly,
disabled,
payload,
onRename,
onDelete,
}) => {
const { t } = useTranslation()
const Icon = getIcon(payload.type)
const handleRename = useCallback(() => {
onRename?.()
}, [onRename])
const deleteBtnRef = useRef<HTMLDivElement>(null)
const isDeleteHovering = useHover(deleteBtnRef)
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = useCallback(() => {
hideDeleteConfirm()
onDelete?.()
}, [hideDeleteConfirm, onDelete])
return (
<div
key={payload.id}
className={cn(
!readonly && !disabled && 'group/item hover:shadow-xs cursor-pointer',
'border border-components-panel-border-subtle rounded-md bg-components-panel-on-panel-item-bg',
isDeleteHovering && 'border border-state-destructive-border bg-state-destructive-hover',
)}
>
<div
className={cn(
'flex items-center h-8 px-2 justify-between',
disabled && 'opacity-30', // not include border and bg
)}
>
<div className='flex items-center h-full text-text-tertiary space-x-1'>
<Icon className='shrink-0 size-4' />
<div className='max-w-[250px] truncate system-sm-medium text-text-primary'>{payload.name}</div>
<div className='shrink-0 system-xs-regular'>{payload.type}</div>
</div>
<div className='group-hover/item:hidden ml-2 shrink-0 system-xs-regular text-text-tertiary'>
{disabled ? t(`${i18nPrefix}.disabled`) : t(`${i18nPrefix}.values`, { num: payload.valueLength || 0 })}
</div>
<div className='group-hover/item:flex hidden ml-2 items-center text-text-tertiary space-x-1'>
<RiEditLine className='size-4 cursor-pointer' onClick={handleRename} />
<div ref={deleteBtnRef} className='hover:text-text-destructive'>
<RiDeleteBinLine className='size-4 cursor-pointer' onClick={showDeleteConfirm} />
</div>
</div>
{isShowDeleteConfirm && (
<Confirm
isShow
type='warning'
title={'Confirm to delete'}
content={`Are you sure you want to delete the metadata "${payload.name}"?`}
onConfirm={handleDelete}
onCancel={hideDeleteConfirm}
/>
)}
</div>
</div>
)
}
const DatasetMetadataDrawer: FC<Props> = ({
userMetadata,
builtInMetadata,
isBuiltInEnabled,
onIsBuiltInEnabledChange,
onClose,
onChange,
}) => {
const { t } = useTranslation()
const [isShowRenameModal, setIsShowRenameModal] = useState(false)
const [currPayload, setCurrPayload] = useState<MetadataItemWithValueLength | null>(null)
const [templeName, setTempleName] = useState('')
const handleRename = useCallback((payload: MetadataItemWithValueLength) => {
return () => {
setCurrPayload(payload)
setTempleName(payload.name)
setIsShowRenameModal(true)
}
}, [setCurrPayload, setIsShowRenameModal])
const handleAdd = useCallback((data: MetadataItemWithValueLength) => {
const nextUserMetadata = produce(userMetadata, (draft) => {
draft.push(data)
})
onChange(nextUserMetadata)
}, [userMetadata, onChange])
const handleRenamed = useCallback(() => {
const nextUserMetadata = produce(userMetadata, (draft) => {
const index = draft.findIndex(p => p.id === currPayload?.id)
if (index !== -1)
draft[index].name = templeName!
})
onChange(nextUserMetadata)
setIsShowRenameModal(false)
}, [currPayload, templeName, userMetadata, onChange])
const handleDelete = useCallback((payload: MetadataItemWithValueLength) => {
return () => {
const nextUserMetadata = userMetadata.filter(p => p.id !== payload.id)
onChange(nextUserMetadata)
}
}, [userMetadata, onChange])
return (
<Drawer
isOpen={true}
onClose={onClose}
showClose
title={t('dataset.metadata.metadata')}
footer={null}
panelClassname='px-4 block !max-w-[420px] my-2 rounded-l-2xl'
>
<div className='system-sm-regular text-text-tertiary'>{t(`${i18nPrefix}.description`)}</div>
<CreateModal trigger={<Button variant='primary' className='mt-3'>
<RiAddLine className='mr-1' />
{t(`${i18nPrefix}.addMetaData`)}
</Button>} hasBack onSave={handleAdd} />
<div className='mt-3 space-y-1'>
{userMetadata.map(payload => (
<Item
key={payload.id}
payload={payload}
onRename={handleRename(payload)}
onDelete={handleDelete(payload)}
/>
))}
</div>
<div className='mt-3 flex h-6 items-center'>
<Switch
defaultValue={isBuiltInEnabled}
onChange={onIsBuiltInEnabledChange}
/>
<div className='ml-2 mr-0.5 system-sm-semibold text-text-secondary'>{t(`${i18nPrefix}.builtIn`)}</div>
<Tooltip popupContent={<div className='max-w-[100px]'>{t(`${i18nPrefix}.builtInDescription`)}</div>} />
</div>
<div className='mt-1 space-y-1'>
{builtInMetadata.map(payload => (
<Item
key={payload.id}
readonly
disabled={!isBuiltInEnabled}
payload={payload}
/>
))}
</div>
{isShowRenameModal && (
<Modal isShow title={t(`${i18nPrefix}.rename`)} onClose={() => setIsShowRenameModal(false)}>
<Field label={t(`${i18nPrefix}.name`)}>
<Input
value={templeName}
onChange={e => setTempleName(e.target.value)}
placeholder={t(`${i18nPrefix}.namePlaceholder`)}
/>
</Field>
<div className='mt-4 flex justify-end'>
<Button
className='mr-2'
onClick={() => {
setIsShowRenameModal(false)
setTempleName(currPayload!.name)
}}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleRenamed}
variant='primary'
disabled={!templeName}
>{t('common.operation.save')}</Button>
</div>
</Modal>
)}
</Drawer>
)
}
export default React.memo(DatasetMetadataDrawer)

View File

@ -0,0 +1,45 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { MetadataItemWithEdit } from '../types'
import cn from '@/utils/classnames'
import Label from './label'
import InputCombined from './input-combined'
import { RiIndeterminateCircleLine } from '@remixicon/react'
type Props = {
className?: string
payload: MetadataItemWithEdit
onChange: (value: any) => void
onRemove: () => void
}
const AddRow: FC<Props> = ({
className,
payload,
onChange,
onRemove,
}) => {
return (
<div className={cn('flex h-6 items-center space-x-0.5', className)}>
<Label text={payload.name} />
<InputCombined
type={payload.type}
value={payload.value}
onChange={onChange}
/>
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
)
}
onClick={onRemove}
>
<RiIndeterminateCircleLine className='size-4' />
</div>
</div>
)
}
export default React.memo(AddRow)

View File

@ -0,0 +1,50 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { type MetadataItemWithEdit, UpdateType } from '../types'
import Label from './label'
import { RiDeleteBinLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import InputHasSetMultipleValue from './input-has-set-multiple-value'
import InputCombined from './input-combined'
import EditedBeacon from './edited-beacon'
type Props = {
payload: MetadataItemWithEdit
onChange: (payload: MetadataItemWithEdit) => void
onRemove: (id: string) => void
}
const EditMetadatabatchItem: FC<Props> = ({
payload,
onChange,
onRemove,
}) => {
const isUpdated = payload.isUpdated
const isDeleted = payload.updateType === UpdateType.delete
return (
<div className='flex h-6 items-center space-x-0.5'>
{isUpdated ? <EditedBeacon onReset={() => { }} /> : <div className='shrink-0 size-4' />}
<Label text={payload.name} isDeleted={isDeleted} />
{payload.isMultipleValue
? <InputHasSetMultipleValue onClear={() => onChange({ ...payload, isMultipleValue: false })} />
: <InputCombined
type={payload.type}
value={payload.value}
onChange={v => onChange({ ...payload, value: v as string })
} />}
<div
className={
cn(
'p-1 rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive cursor-pointer',
isDeleted && 'cursor-default bg-state-destructive-hover text-text-destructive')
}
onClick={() => onRemove(payload.id)}
>
<RiDeleteBinLine className='size-4' />
</div>
</div>
)
}
export default React.memo(EditMetadatabatchItem)

View File

@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React, { useRef } from 'react'
import { useHover } from 'ahooks'
import { RiResetLeftLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
type Props = {
onReset: () => void
}
const EditedBeacon: FC<Props> = ({
onReset,
}) => {
const { t } = useTranslation()
const ref = useRef(null)
const isHovering = useHover(ref)
return (
<div ref={ref} className='size-4 cursor-pointer'>
{isHovering ? (
<Tooltip popupContent={t('common.operation.reset')}>
<div className='flex justify-center items-center size-4 bg-text-accent-secondary rounded-full' onClick={onReset}>
<RiResetLeftLine className='size-[10px] text-text-primary-on-surface' />
</div>
</Tooltip>
) : (
<div className='flex items-center justify-center size-4'>
<div className='size-1 rounded-full bg-text-accent-secondary'></div>
</div>
)}
</div>
)
}
export default React.memo(EditedBeacon)

View File

@ -0,0 +1,51 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { DataType } from '../types'
import Input from '@/app/components/base/input'
import { InputNumber } from '@/app/components/base/input-number'
import cn from '@/utils/classnames'
type Props = {
className?: string
type: DataType
value: any
onChange: (value: any) => void
}
const InputCombined: FC<Props> = ({
className: configClassName,
type,
value,
onChange,
}) => {
// TODO: configClassName...
const className = cn('grow p-0.5 h-6 text-xs')
if (type === DataType.time)
return <div className='grow text-xs'>Datepicker placeholder</div>
if (type === DataType.number) {
return (
<div className='grow text-[0]'>
<InputNumber
className={cn(className, 'rounded-l-md')}
value={value}
onChange={onChange}
size='sm'
controlWrapClassName='overflow-hidden'
controlClassName='pt-0 pb-0'
/>
</div>
)
}
return (
<Input
wrapperClassName={configClassName}
className={cn(className, 'rounded-md')}
value={value}
onChange={e => onChange(e.target.value)}
>
</Input>
)
}
export default React.memo(InputCombined)

View File

@ -0,0 +1,29 @@
'use client'
import { RiCloseLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
onClear: () => void
}
const InputHasSetMultipleValue: FC<Props> = ({
onClear,
}) => {
const { t } = useTranslation()
return (
<div className='grow h-6 p-0.5 rounded-md bg-components-input-bg-normal text-[0]'>
<div className='inline-flex rounded-[5px] items-center h-5 pl-1.5 pr-0.5 bg-components-badge-white-to-dark border-[0.5px] border-components-panel-border shadow-xs space-x-0.5'>
<div className='system-xs-regular text-text-secondary'>{t('dataset.metadata.batchEditMetadata.multipleValue')}</div>
<div className='p-0.5rounded-sm text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary cursor-pointer'>
<RiCloseLine
className='size-3.5 '
onClick={onClear}
/>
</div>
</div>
</div>
)
}
export default React.memo(InputHasSetMultipleValue)

View File

@ -0,0 +1,27 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
type Props = {
isDeleted?: boolean,
className?: string,
text: string
}
const Label: FC<Props> = ({
isDeleted,
className,
text,
}) => {
return (
<div className={cn(
'shrink-0 w-[136px] system-xs-medium text-text-tertiary truncate',
isDeleted && 'line-through text-text-quaternary',
className,
)}>
{text}
</div>
)
}
export default React.memo(Label)

View File

@ -0,0 +1,139 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import Modal from '../../../base/modal'
import { DataType, type MetadataItemWithEdit } from '../types'
import EditMetadataBatchItem from './edit-row'
import AddedMetadataItem from './add-row'
import Button from '../../../base/button'
import { useTranslation } from 'react-i18next'
import Checkbox from '../../../base/checkbox'
import Tooltip from '../../../base/tooltip'
import SelectMetadataModal from '../select-metadata-modal'
import { RiQuestionLine } from '@remixicon/react'
import Divider from '@/app/components/base/divider'
import AddMetadataButton from '../add-metadata-button'
const i18nPrefix = 'dataset.metadata.batchEditMetadata'
type Props = {
documentNum: number
list: MetadataItemWithEdit[]
onChange: (list: MetadataItemWithEdit[], addedList: MetadataItemWithEdit[], isApplyToAllSelectDocument: boolean) => void
onHide: () => void
}
const EditMetadataBatchModal: FC<Props> = ({
documentNum,
list,
onChange,
onHide,
}) => {
const { t } = useTranslation()
const [templeList, setTempleList] = useState<MetadataItemWithEdit[]>(list)
const handleTemplesChange = useCallback((payload: MetadataItemWithEdit) => {
const newTempleList = templeList.map(i => i.id === payload.id ? payload : i)
setTempleList(newTempleList)
}, [templeList])
const handleTempleItemRemove = useCallback((id: string) => {
const newTempleList = templeList.filter(i => i.id !== id)
setTempleList(newTempleList)
}, [templeList])
const testAddedList: MetadataItemWithEdit[] = [
{
id: '1', name: 'name1', type: DataType.string, value: 'aaa',
},
{
id: '2.1', name: 'num v', type: DataType.number, value: 10,
},
]
const [addedList, setAddedList] = useState<MetadataItemWithEdit[]>(testAddedList)
const handleAddedListChange = useCallback((payload: MetadataItemWithEdit) => {
const newAddedList = addedList.map(i => i.id === payload.id ? payload : i)
setAddedList(newAddedList)
}, [addedList])
const handleAddedItemRemove = useCallback((removeIndex: number) => {
return () => {
const newAddedList = addedList.filter((i, index) => index !== removeIndex)
setAddedList(newAddedList)
}
}, [addedList])
const [isApplyToAllSelectDocument, setIsApplyToAllSelectDocument] = useState(false)
const handleSave = useCallback(() => {
onChange(templeList, addedList, isApplyToAllSelectDocument)
}, [templeList, addedList, isApplyToAllSelectDocument, onChange])
return (
<Modal
title={t(`${i18nPrefix}.editMetadata`)}
isShow
closable
onClose={onHide}
className='!max-w-[640px]'
>
<div className='system-xs-medium text-text-accent'>{t(`${i18nPrefix}.editDocumentsNum`, { num: documentNum })}</div>
{/* TODO handle list scroll. There is two list. */}
<div className='mt-4 space-y-2'>
{templeList.map(item => (
<EditMetadataBatchItem
key={item.id}
payload={item}
onChange={handleTemplesChange}
onRemove={handleTempleItemRemove}
/>
))}
</div>
<div className='mt-4 pl-[18px]'>
<div className='flex items-center'>
<div className='mr-2 shrink-0 system-xs-medium-uppercase text-text-tertiary'>{t('dataset.metadata.createMetadata.title')}</div>
<Divider bgStyle='gradient' />
</div>
<div className='mt-2 space-y-2'>
{addedList.map((item, i) => (
<AddedMetadataItem
key={i}
payload={item}
onChange={handleAddedListChange}
onRemove={handleAddedItemRemove(i)}
/>
))}
</div>
<div className='mt-3'>
<SelectMetadataModal
popupPlacement='top-start'
popupOffset={{ mainAxis: 4, crossAxis: 0 }}
trigger={
<AddMetadataButton />
}
onSave={data => setAddedList([...addedList, data])}
/>
</div>
</div>
<div className='mt-4 flex items-center justify-between'>
<div className='flex items-center select-none'>
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} />
<div className='ml-2 mr-1 system-xs-medium text-text-secondary'>{t(`${i18nPrefix}.applyToAllSelectDocument`)}</div>
<Tooltip popupContent={
<div className='max-w-[240px]'>{t(`${i18nPrefix}.applyToAllSelectDocumentTip`)}</div>
} >
<div className='p-px cursor-pointer'>
<RiQuestionLine className='size-3.5 text-text-tertiary' />
</div>
</Tooltip>
</div>
<div className='flex items-center space-x-2'>
<Button
onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button
onClick={handleSave}
variant='primary'
>{t('common.operation.save')}</Button>
</div>
</div>
</Modal>
)
}
export default React.memo(EditMetadataBatchModal)

View File

@ -0,0 +1,23 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
className?: string
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
className,
label,
children,
}) => {
return (
<div className={className}>
<div className='py-1 system-sm-semibold text-text-secondary'>{label}</div>
<div className='mt-1'>{children}</div>
</div>
)
}
export default React.memo(Field)

View File

@ -0,0 +1,26 @@
'use client'
import type { FC } from 'react'
import React from 'react'
type Props = {
label: string
children: React.ReactNode
}
const Field: FC<Props> = ({
label,
children,
}) => {
return (
<div className='flex items-start space-x-2'>
<div className='shrink-0 w-[128px] truncate py-1 items-center text-text-tertiary system-xs-medium'>
{label}
</div>
<div className='shrink-0 w-[244px]'>
{children}
</div>
</div>
)
}
export default React.memo(Field)

View File

@ -0,0 +1,127 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { DataType, type MetadataItemWithValue } from '../types'
import InfoGroup from './info-group'
import NoData from './no-data'
import Button from '@/app/components/base/button'
import { RiEditLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
const i18nPrefix = 'dataset.metadata.documentMetadata'
const MetadataDocument: FC = () => {
const { t } = useTranslation()
const [isEdit, setIsEdit] = useState(true)
const [list, setList] = useState<MetadataItemWithValue[]>([
{
id: '1',
name: 'Doc type',
value: 'PDF',
type: DataType.string,
},
{
id: '2',
name: 'Title',
value: 'PDF',
type: DataType.string,
},
])
const [tempList, setTempList] = useState<MetadataItemWithValue[]>(list)
const builtInEnabled = true
const builtList = [
{
id: '1',
name: 'OriginalfileNmae',
value: 'Steve Jobs The Man Who Thought Different.pdf',
type: DataType.string,
},
{
id: '2',
name: 'Title',
value: 'PDF',
type: DataType.string,
},
]
const hasData = list.length > 0
const documentInfoList = builtList
const technicalParams = builtList
return (
<div className='w-[388px] space-y-4'>
{!hasData ? (
<div className='pl-2'>
<InfoGroup
title={t('dataset.metadata.metadata')}
uppercaseTitle={false}
titleTooltip={t(`${i18nPrefix}.metadataToolTip`)}
list={isEdit ? tempList : list}
headerRight={isEdit ? (
<div className='flex space-x-1'>
<Button variant='ghost' size='small' onClick={() => {
setTempList(list)
setIsEdit(false)
}}>
<div>{t('common.operation.cancel')}</div>
</Button>
<Button variant='primary' size='small' onClick={() => {
setIsEdit(false)
setList(tempList)
}}>
<div>{t('common.operation.save')}</div>
</Button>
</div>
) : (
<Button variant='ghost' size='small' onClick={() => {
setTempList(list)
setIsEdit(true)
}}>
<RiEditLine className='mr-1 size-3.5 text-text-tertiary cursor-pointer' />
<div>{t('common.operation.edit')}</div>
</Button>
)}
isEdit={isEdit}
contentClassName='mt-5'
onChange={(item) => {
const newList = tempList.map(i => (i.name === item.name ? item : i))
setList(newList)
}}
onDelete={(item) => {
const newList = tempList.filter(i => i.name !== item.name)
setList(newList)
}}
onAdd={() => {
}}
/>
{builtInEnabled && (
<>
<Divider className='my-3' bgStyle='gradient' />
<InfoGroup
noHeader
titleTooltip='Built-in metadata is system-generated metadata that is automatically added to the document. You can enable or disable built-in metadata here.'
list={builtList}
/>
</>
)}
</div>
) : (
<NoData onStart={() => { }} />
)}
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.documentInformation`)}
list={documentInfoList}
/>
<InfoGroup
className='pl-2'
title={t(`${i18nPrefix}.technicalParameters`)}
list={technicalParams}
/>
</div>
)
}
export default React.memo(MetadataDocument)

View File

@ -0,0 +1,94 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { MetadataItemWithValue } from '../types'
import Field from './field'
import InputCombined from '../edit-metadata-batch/input-combined'
import { RiDeleteBinLine, RiQuestionLine } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
import SelectMetadataModal from '../select-metadata-modal'
import AddMetadataButton from '../add-metadata-button'
type Props = {
className?: string
noHeader?: boolean
title?: string
uppercaseTitle?: boolean
titleTooltip?: string
headerRight?: React.ReactNode
contentClassName?: string
list: MetadataItemWithValue[]
isEdit?: boolean
onChange?: (item: MetadataItemWithValue) => void
onDelete?: (item: MetadataItemWithValue) => void
onAdd?: (item: MetadataItemWithValue) => void
}
const InfoGroup: FC<Props> = ({
className,
noHeader,
title,
uppercaseTitle = true,
titleTooltip,
headerRight,
contentClassName,
list,
isEdit,
onChange,
onDelete,
onAdd,
}) => {
return (
<div className={cn('bg-white', className)}>
{!noHeader && (
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-1'>
<div className={cn('text-text-secondary', uppercaseTitle ? 'system-xs-semibold-uppercase' : 'system-md-semibold')}>{title}</div>
{titleTooltip && (
<Tooltip popupContent={<div className='max-w-[240px]'>{titleTooltip}</div>}>
<RiQuestionLine className='size-3.5 text-text-tertiary' />
</Tooltip>
)}
</div>
{headerRight}
{/* <div className='flex px-1.5 rounded-md hover:bg-components-button-tertiary-bg-hover items-center h-6 space-x-1 cursor-pointer' onClick={() => setIsEdit(true)}>
</div> */}
</div>
)}
<div className={cn('mt-3 space-y-1', !noHeader && 'mt-0', contentClassName)}>
{isEdit && (
<div>
<SelectMetadataModal
trigger={
<AddMetadataButton />
}
onSave={() => { }}
/>
<Divider className='my-3 ' bgStyle='gradient' />
</div>
)}
{list.map((item, i) => (
<Field key={item.id || `${i}`} label={item.name}>
{isEdit ? (
<div className='flex items-center space-x-0.5'>
<InputCombined
className='h-6'
type={item.type}
value={item.value}
onChange={value => onChange?.({ ...item, value })}
/>
<div className='shrink-0 p-1 rounded-md text-text-tertiary hover:text-text-destructive hover:bg-state-destructive-hover cursor-pointer'>
<RiDeleteBinLine className='size-4' />
</div>
</div>
) : (<div className='py-1 system-xs-regular text-text-secondary'>{item.value}</div>)}
</Field>
))}
</div>
</div>
)
}
export default React.memo(InfoGroup)

View File

@ -0,0 +1,27 @@
'use client'
import Button from '@/app/components/base/button'
import { RiArrowRightLine } from '@remixicon/react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
onStart: () => void
}
const NoData: FC<Props> = ({
onStart,
}) => {
const { t } = useTranslation()
return (
<div className='p-4 pt-3 rounded-xl bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2'>
<div className='text-text-secondary text-xs font-semibold leading-5'>{t('dataset.metadata.metadata')}</div>
<div className='mt-1 system-xs-regular text-text-tertiary'>{t('dataset.metadata.documentMetadata.metadataToolTip')}</div>
<Button variant='primary' className='mt-2' onClick={onStart}>
<div>{t('dataset.metadata.documentMetadata.startLabeling')}</div>
<RiArrowRightLine className='ml-1 size-4' />
</Button>
</div>
)
}
export default React.memo(NoData)

View File

@ -0,0 +1,76 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import type { Props as CreateContentProps } from './create-content'
import CreateContent from './create-content'
import SelectMetadata from './select-metadata'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import type { MetadataItem } from './types'
import type { Placement } from '@floating-ui/react'
import { DataType } from './types'
type Props = {
popupPlacement?: Placement
popupOffset?: { mainAxis: number, crossAxis: number }
onSave: (data: any) => void
trigger: React.ReactNode
} & CreateContentProps
enum Step {
select = 'select',
create = 'create',
}
const testMetadataList: MetadataItem[] = [
{ id: '1', name: 'name1', type: DataType.string },
{ id: '2', name: 'name2', type: DataType.number },
{ id: '3', name: 'name3', type: DataType.time },
]
const SelectMetadataModal: FC<Props> = ({
popupPlacement = 'left-start',
popupOffset = { mainAxis: -38, crossAxis: 4 },
trigger,
onSave,
}) => {
const [open, setOpen] = useState(false)
const [step, setStep] = useState(Step.select)
const handleSave = useCallback((data: MetadataItem) => {
onSave(data)
setOpen(false)
}, [onSave])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={popupPlacement}
offset={popupOffset}
>
<PortalToFollowElemTrigger
onClick={() => setOpen(!open)}
className='block'
>
{trigger}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
{step === Step.select ? (
<SelectMetadata
onSelect={handleSave}
list={testMetadataList}
onNew={() => setStep(Step.create)}
onManage={() => { }}
/>
) : (
<CreateContent
onSave={handleSave}
hasBack
onBack={() => setStep(Step.select)}
/>
)}
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default React.memo(SelectMetadataModal)

View File

@ -0,0 +1,72 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import type { MetadataItem } from './types'
import SearchInput from '../../base/search-input'
import { RiAddLine, RiArrowRightUpLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { getIcon } from './utils/get-icon'
const i18nPrefix = 'dataset.metadata.selectMetadata'
type Props = {
list: MetadataItem[]
onSelect: (data: any) => void
onNew: () => void
onManage: () => void
}
const SelectMetadata: FC<Props> = ({
list,
onSelect,
onNew,
onManage,
}) => {
const { t } = useTranslation()
const [query, setQuery] = useState('')
return (
<div className='w-[320px] pt-2 pb-0 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg backdrop-blur-[5px]'>
<SearchInput
className='mx-2'
value={query}
onChange={setQuery}
placeholder={t(`${i18nPrefix}.search`)}
/>
<div className='mt-2'>
{list.map((item) => {
const Icon = getIcon(item.type)
return (
<div
key={item.id}
className='mx-1 flex items-center h-6 px-3 justify-between rounded-md hover:bg-state-base-hover cursor-pointer'
onClick={() => onSelect(item)}
>
<div className='w-0 grow flex items-center h-full text-text-secondary'>
<Icon className='shrink-0 mr-[5px] size-3.5' />
<div className='w-0 grow truncate system-sm-medium'>{item.name}</div>
</div>
<div className='ml-1 shrink-0 system-xs-regular text-text-tertiary'>
{item.type}
</div>
</div>
)
})}
</div>
<div className='mt-1 flex justify-between p-1 border-t border-divider-subtle'>
<div className='flex items-center h-6 px-3 text-text-secondary rounded-md hover:bg-state-base-hover cursor-pointer space-x-1' onClick={onNew}>
<RiAddLine className='size-3.5' />
<div className='system-sm-medium'>{t(`${i18nPrefix}.newAction`)}</div>
</div>
<div className='flex items-center h-6 text-text-secondary '>
<div className='mr-[3px] w-px h-3 bg-divider-regular'></div>
<div className='flex h-full items-center px-1.5 hover:bg-state-base-hover rounded-md cursor-pointer' onClick={onManage}>
<div className='mr-1 system-sm-medium'>{t(`${i18nPrefix}.manageAction`)}</div>
<RiArrowRightUpLine className='size-3.5' />
</div>
</div>
</div>
</div>
)
}
export default React.memo(SelectMetadata)

View File

@ -0,0 +1,28 @@
export enum DataType {
string = 'string',
number = 'number',
time = 'time',
}
export type MetadataItem = {
id: string
type: DataType
name: string
}
export type MetadataItemWithValue = MetadataItem & {
value: string | number
}
export type MetadataItemWithValueLength = MetadataItem & {
valueLength: number
}
export enum UpdateType {
changeValue = 'changeValue',
delete = 'delete',
}
export type MetadataItemWithEdit = MetadataItemWithValue & {
isMultipleValue?: boolean
isUpdated?: boolean
updateType?: UpdateType
}

View File

@ -0,0 +1,10 @@
import { DataType } from '../types'
import { RiHashtag, RiTextSnippet, RiTimeLine } from '@remixicon/react'
export const getIcon = (type: DataType) => {
return ({
[DataType.string]: RiTextSnippet,
[DataType.number]: RiHashtag,
[DataType.time]: RiTimeLine,
}[type] || RiTextSnippet)
}

View File

@ -16,6 +16,7 @@ const WorkplaceSelector = () => {
const { notify } = useContext(ToastContext)
const { workspaces } = useWorkspacesContext()
const currentWorkspace = workspaces.find(v => v.current)
const isFreePlan = plan.type === 'sandbox'
const handleSwitchWorkspace = async (tenant_id: string) => {
try {
if (currentWorkspace?.id === tenant_id)

View File

@ -1,8 +1,8 @@
import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import { useLanguage } from '../hooks'
import { Openai } from '@/app/components/base/icons/src/vender/other'
import { useAppContext } from '@/context/app-context'
import { Openai } from '@/app/components/base/icons/src/vender/other'
import { AnthropicDark, AnthropicLight } from '@/app/components/base/icons/src/public/llm'
import { renderI18nObject } from '@/hooks/use-i18n'
import { Theme } from '@/types/app'

View File

@ -1,160 +0,0 @@
import type { PluginDeclaration } from '../types'
import { PluginType } from '../types'
export const toolNeko: PluginDeclaration = {
plugin_unique_identifier: 'xxxxxx',
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',
name: 'notion page search',
version: '1.2.0',
latest_version: '1.3.0',
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
brief: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
}
export const toolNotionManifest: PluginDeclaration = {
version: '1.2.0',
author: 'Notion',
icon: 'https://via.placeholder.com/150',
name: 'notion page search',
category: PluginType.tool,
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
description: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
created_at: '2022-01-01',
resource: {},
plugins: {},
verified: true,
endpoint: {
settings: [],
endpoints: [],
},
tool: {
} as any,
model: {},
}
export const extensionDallE = {
type: PluginType.extension,
org: 'OpenAI',
name: 'DALL-E',
version: '1.1.0',
latest_version: '1.2.0',
install_count: 1234,
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'DALL-E',
'zh-Hans': 'DALL-E',
},
brief: {
'en-US': 'Description: A simple plugin to use OpenAI DALL-E model.',
'zh-Hans': '一个使用 OpenAI DALL-E 模型的简单插件。',
},
}
export const modelGPT4 = {
type: PluginType.model,
org: 'OpenAI',
name: 'GPT-4',
version: '1.0.0',
latest_version: '1.0.0',
install_count: 99999,
icon: 'https://via.placeholder.com/150',
label: {
'en-US': 'GPT-4',
'zh-Hans': 'GPT-4',
},
brief: {
'en-US': 'Description: A simple plugin to use OpenAI GPT-4 model.',
'zh-Hans': '一个使用 OpenAI GPT-4 模型的简单插件。',
},
}
export const customTool = {
type: PluginType.tool,
name: 'notion page search',
version: '1.2.0',
latest_version: '1.3.0',
icon: {
content: '🕵️',
background: '#FEF7C3',
},
label: {
'en-US': 'Notion Page Search',
'zh-Hans': 'Notion 页面搜索',
},
brief: {
'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...',
'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...',
},
}

View File

@ -1,4 +1,5 @@
import { useUpdateModelProviders } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useProviderContext } from '@/context/provider-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
@ -8,7 +9,9 @@ import { PluginType } from '../../types'
const useRefreshPluginList = () => {
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const updateModelProviders = useUpdateModelProviders()
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
const { refreshModelProviders } = useProviderContext()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
@ -31,8 +34,10 @@ const useRefreshPluginList = () => {
// model select
if (PluginType.model.includes(manifest.category) || refreshAllType) {
updateModelProviders()
refreshModelProviders()
refetchLLMModelList()
refetchEmbeddingModelList()
refetchRerankModelList()
}
// agent select

View File

@ -87,10 +87,13 @@ const InstallByDSLList: FC<Props> = ({
const failedIndex: number[] = []
const nextPlugins = produce(pluginsRef.current, (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => {
if (payloads[i])
draft[index] = payloads[i]
else
failedIndex.push(index)
if (payloads[i]) {
draft[index] = {
...payloads[i],
version: payloads[i].version || payloads[i].latest_version,
}
}
else { failedIndex.push(index) }
})
})
setPlugins(nextPlugins)
@ -192,8 +195,8 @@ const InstallByDSLList: FC<Props> = ({
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
payload={plugins[index] as Plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version!}
payload={plugin}
version={(d as GitHubItemAndMarketPlaceDependency).value.version! || plugin?.version || ''}
versionInfo={getVersionInfo(`${plugin?.org || plugin?.author}/${plugin?.name}`)}
/>
)

View File

@ -136,9 +136,10 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.uploadFailed }))
}, [])
const handleInstalled = useCallback(() => {
const handleInstalled = useCallback((notRefresh?: boolean) => {
setState(prevState => ({ ...prevState, step: InstallStepFromGitHub.installed }))
refreshPluginList(manifest)
if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false)
onSuccess()
}, [manifest, onSuccess, refreshPluginList, setIsInstalling])

View File

@ -24,7 +24,7 @@ type LoadedProps = {
selectedPackage: string
onBack: () => void
onStartToInstall?: () => void
onInstalled: () => void
onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void
}
@ -55,7 +55,7 @@ const Loaded: React.FC<LoadedProps> = ({
const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
const { handleRefetch } = usePluginTaskList()
const { handleRefetch } = usePluginTaskList(payload.category)
const { check } = checkTaskStatus()
useEffect(() => {
@ -127,7 +127,7 @@ const Loaded: React.FC<LoadedProps> = ({
onFailed(error)
return
}
onInstalled()
onInstalled(true)
}
catch (e) {
if (typeof e === 'string') {

View File

@ -32,9 +32,10 @@ const ReadyToInstall: FC<Props> = ({
}) => {
const { refreshPluginList } = useRefreshPluginList()
const handleInstalled = useCallback(() => {
const handleInstalled = useCallback((notRefresh?: boolean) => {
onStepChange(InstallStep.installed)
refreshPluginList(manifest)
if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false)
}, [manifest, onStepChange, refreshPluginList, setIsInstalling])

View File

@ -20,7 +20,7 @@ type Props = {
payload: PluginDeclaration
onCancel: () => void
onStartToInstall?: () => void
onInstalled: () => void
onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void
}
@ -62,7 +62,7 @@ const Installed: FC<Props> = ({
onCancel()
}
const { handleRefetch } = usePluginTaskList()
const { handleRefetch } = usePluginTaskList(payload.category)
const handleInstall = async () => {
if (isInstalling) return
setIsInstalling(true)
@ -92,7 +92,7 @@ const Installed: FC<Props> = ({
onFailed(error)
return
}
onInstalled()
onInstalled(true)
}
catch (e) {
if (typeof e === 'string') {

View File

@ -54,9 +54,10 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
return t(`${i18nPrefix}.installPlugin`)
}, [isBundle, step, t])
const handleInstalled = useCallback(() => {
const handleInstalled = useCallback((notRefresh?: boolean) => {
setStep(InstallStep.installed)
refreshPluginList(manifest)
if (!notRefresh)
refreshPluginList(manifest)
setIsInstalling(false)
}, [manifest, refreshPluginList, setIsInstalling])

View File

@ -21,7 +21,7 @@ type Props = {
payload: PluginManifestInMarket | Plugin
onCancel: () => void
onStartToInstall?: () => void
onInstalled: () => void
onInstalled: (notRefresh?: boolean) => void
onFailed: (message?: string) => void
}
@ -51,7 +51,7 @@ const Installed: FC<Props> = ({
check,
stop,
} = checkTaskStatus()
const { handleRefetch } = usePluginTaskList()
const { handleRefetch } = usePluginTaskList(payload.category)
useEffect(() => {
if (hasInstalled && uniqueIdentifier === installedInfoPayload.uniqueIdentifier)
@ -106,7 +106,7 @@ const Installed: FC<Props> = ({
onFailed(error)
return
}
onInstalled()
onInstalled(true)
}
catch (e) {
if (typeof e === 'string') {

View File

@ -63,7 +63,7 @@ const Description = async ({
</>
)
}
</h2 >
</h2>
</>
)
}

View File

@ -113,6 +113,7 @@ const DetailHeader = ({
},
payload: {
type: PluginSource.github,
category: detail.declaration.category,
github: {
originalPackageInfo: {
id: detail.plugin_unique_identifier,
@ -287,6 +288,7 @@ const DetailHeader = ({
isShowUpdateModal && (
<UpdateFromMarketplace
payload={{
category: detail.declaration.category,
originalPackageInfo: {
id: detail.plugin_unique_identifier,
payload: detail.declaration,

View File

@ -14,6 +14,7 @@ import { useGitHubReleases } from '../install-plugin/hooks'
import Toast from '@/app/components/base/toast'
import { useModalContext } from '@/context/modal-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import type { PluginType } from '@/app/components/plugins/types'
const i18nPrefix = 'plugin.action'
@ -22,6 +23,7 @@ type Props = {
installationId: string
pluginUniqueIdentifier: string
pluginName: string
category: PluginType
usedInApps: number
isShowFetchNewVersion: boolean
isShowInfo: boolean
@ -34,6 +36,7 @@ const Action: FC<Props> = ({
installationId,
pluginUniqueIdentifier,
pluginName,
category,
isShowFetchNewVersion,
isShowInfo,
isShowDelete,
@ -67,6 +70,7 @@ const Action: FC<Props> = ({
},
payload: {
type: PluginSource.github,
category,
github: {
originalPackageInfo: {
id: pluginUniqueIdentifier,
@ -94,7 +98,7 @@ const Action: FC<Props> = ({
hideDeleteConfirm()
onDelete()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [installationId, onDelete])
return (
<div className='flex space-x-1'>

View File

@ -20,11 +20,9 @@ import Title from '../card/base/title'
import Action from './action'
import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useInvalidateAllBuiltInTools, useInvalidateAllToolProviders } from '@/service/use-tools'
import { useSingleCategories } from '../hooks'
import { useProviderContext } from '@/context/provider-context'
import { useRenderI18nObject } from '@/hooks/use-i18n'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
type Props = {
className?: string
@ -39,10 +37,7 @@ const PluginItem: FC<Props> = ({
const { categoriesMap } = useSingleCategories()
const currentPluginID = usePluginPageContext(v => v.currentPluginID)
const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const { refreshModelProviders } = useProviderContext()
const { refreshPluginList } = useRefreshPluginList()
const {
source,
@ -60,13 +55,7 @@ const PluginItem: FC<Props> = ({
}, [source, author])
const handleDelete = () => {
invalidateInstalledPluginList()
if (PluginType.model.includes(category))
refreshModelProviders()
if (PluginType.tool.includes(category)) {
invalidateAllToolProviders()
invalidateAllBuiltinTools()
}
refreshPluginList({ category } as any)
}
const getValueFromI18nObject = useRenderI18nObject()
const title = getValueFromI18nObject(label)
@ -116,6 +105,7 @@ const PluginItem: FC<Props> = ({
isShowDelete
meta={meta}
onDelete={handleDelete}
category={category}
/>
</div>
</div>

View File

@ -29,8 +29,8 @@ const DebugInfo: FC = () => {
popupContent={
<>
<div className='flex items-center gap-1 self-stretch'>
<span className='flex flex-col justify-center items-start flex-grow flex-shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span>
<a href='' target='_blank' className='flex items-center gap-0.5 text-text-accent-light-mode-only cursor-pointer'>
<span className='flex flex-col justify-center items-start grow shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span>
<a href='https://docs.dify.ai/plugins/quick-start/develop-plugins/debug-plugin' target='_blank' className='flex items-center gap-0.5 text-text-accent-light-mode-only cursor-pointer'>
<span className='system-xs-medium'>{t(`${i18nPrefix}.viewDocs`)}</span>
<RiArrowRightUpLine className='w-3 h-3' />
</a>

View File

@ -151,6 +151,7 @@ export type Permissions = {
}
export type UpdateFromMarketPlacePayload = {
category: PluginType
originalPackageInfo: {
id: string
payload: PluginDeclaration
@ -173,6 +174,7 @@ export type UpdateFromGitHubPayload = {
export type UpdatePluginPayload = {
type: PluginSource
category: PluginType
marketPlace?: UpdateFromMarketPlacePayload
github?: UpdateFromGitHubPayload
}

View File

@ -57,7 +57,7 @@ const UpdatePluginModal: FC<Props> = ({
}
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
const { handleRefetch } = usePluginTaskList()
const { handleRefetch } = usePluginTaskList(payload.category)
const configBtnText = useMemo(() => {
return ({

View File

@ -321,6 +321,7 @@ const TextGeneration: FC<IMainProps> = ({
setControlSend(Date.now())
// clear run once task status
setControlStopResponding(Date.now())
// eslint-disable-next-line ts/no-use-before-define
showResSidebar()
}
@ -400,6 +401,7 @@ const TextGeneration: FC<IMainProps> = ({
transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods,
// legacy of image upload compatible
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
fileUploadConfig: appParams?.system_parameters,
})
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
setPromptConfig({

View File

@ -69,7 +69,7 @@ const ProviderList = () => {
className='relative flex flex-col overflow-y-auto bg-background-body grow'
>
<div className={cn(
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] z-20 flex-wrap gap-y-2',
'sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-background-body z-20 flex-wrap gap-y-2',
currentProvider && 'pr-6',
)}>
<TabSliderNew

View File

@ -27,6 +27,7 @@ import SearchBox from '@/app/components/plugins/marketplace/search-box'
import {
Plus02,
} from '@/app/components/base/icons/src/vender/line/general'
import classNames from '@/utils/classnames'
type NodeSelectorProps = {
open?: boolean

View File

@ -162,7 +162,7 @@ export const useWorkflowRun = () => {
else
ttsUrl = `/apps/${params.appId}/text-to-audio`
}
const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => { })
const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => {})
ssePost(
url,

View File

@ -61,7 +61,7 @@ const BasePanel: FC<BasePanelProps> = ({
showMessageLogModal: state.showMessageLogModal,
})))
const showSingleRunPanel = useStore(s => s.showSingleRunPanel)
const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
const panelWidth = localStorage.getItem('workflow-node-panel-width') ? parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
const {
setPanelWidth,
} = useWorkflow()

View File

@ -168,6 +168,46 @@ const translation = {
preprocessDocument: '{{num}} Preprocess Documents',
allKnowledge: 'All Knowledge',
allKnowledgeDescription: 'Select to display all knowledge in this workspace. Only the Workspace Owner can manage all knowledge.',
metadata: {
metadata: 'Metadata',
addMetadata: 'Add Metadata',
createMetadata: {
title: 'New Metadata',
back: 'Back',
type: 'Type',
name: 'Name',
namePlaceholder: 'Add metadata name',
},
batchEditMetadata: {
editMetadata: 'Edit Metadata',
editDocumentsNum: 'Editing {{num}} documents',
applyToAllSelectDocument: 'Apply to all selected documents',
applyToAllSelectDocumentTip: 'Automatically create all the above edited and new metadata for all selected documents, otherwise editing metadata will only apply to documents with it.',
multipleValue: 'Multiple Value',
},
selectMetadata: {
search: 'Search metadata',
newAction: 'New Metadata',
manageAction: 'Manage',
},
datasetMetadata: {
description: 'You can manage all metadata in this knowledge here. Modifications will be synchronized to every document.',
addMetaData: 'Add Metadata',
values: '{{num}} Values',
disabled: 'Disabled',
rename: 'Rename',
name: 'Name',
namePlaceholder: 'Metadata name',
builtIn: 'Built-in',
builtInDescription: 'Built-in metadata is automatically extracted and generated. It must be enabled before use and cannot be edited.',
},
documentMetadata: {
metadataToolTip: 'Metadata serves as a critical filter that enhances the accuracy and relevance of information retrieval. You can modify and add metadata for this document here.',
startLabeling: 'Start Labeling',
documentInformation: 'Document Information',
technicalParameters: 'Technical Parameters',
},
},
}
export default translation

View File

@ -2,6 +2,9 @@ const translation = {
common: {
welcome: 'आपका स्वागत है',
appUnavailable: 'ऐप उपलब्ध नहीं है',
appUnknownError: 'अज्ञात त्रुटि, कृपया पुनः प्रयास करें',
// @ts-expect-error TODO: fix this
// eslint-disable-next-line no-dupe-keys
appUnknownError: 'ऐप अनुपलब्ध है',
},
chat: {

View File

@ -168,6 +168,46 @@ const translation = {
preprocessDocument: '{{num}} 个预处理文档',
allKnowledge: '所有知识库',
allKnowledgeDescription: '选择以显示该工作区内所有知识库。只有工作区所有者才能管理所有知识库。',
metadata: {
metadata: '元数据',
addMetadata: '添加元数据',
createMetadata: {
title: '新建元数据',
back: '返回',
type: '类型',
name: '名称',
namePlaceholder: '添加元数据名称',
},
batchEditMetadata: {
editMetadata: '编辑元数据',
editDocumentsNum: '编辑 {{num}} 个文档',
applyToAllSelectDocument: '应用于所有选定文档',
applyToAllSelectDocumentTip: '自动为所有选定文档创建上述编辑和新元数据,否则仅对具有元数据的文档应用编辑。',
multipleValue: '多个值',
},
selectMetadata: {
search: '搜索元数据',
newAction: '新建元数据',
manageAction: '管理',
},
datasetMetadata: {
description: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
addMetaData: '添加元数据',
values: '{{num}} 个值',
disabled: '已禁用',
rename: '重命名',
name: '名称',
namePlaceholder: '元数据名称',
builtIn: '内置',
builtInDescription: '内置元数据是系统预定义的元数据,您可以在此处查看和管理内置元数据。',
},
documentMetadata: {
metadataToolTip: '元数据是关于文档的数据,用于描述文档的属性。元数据可以帮助您更好地组织和管理文档。',
startLabeling: '开始标注',
documentInformation: '文档信息',
technicalParameters: '技术参数',
},
},
}
export default translation

View File

@ -1,23 +1,23 @@
import type { I18nText } from '@/i18n/language'
export type CommonResponse = {
export interface CommonResponse {
result: 'success' | 'fail'
}
export type OauthResponse = {
export interface OauthResponse {
redirect_url: string
}
export type SetupStatusResponse = {
export interface SetupStatusResponse {
step: 'finished' | 'not_started'
setup_at?: Date
}
export type InitValidateStatusResponse = {
export interface InitValidateStatusResponse {
status: 'finished' | 'not_started'
}
export type UserProfileResponse = {
export interface UserProfileResponse {
id: string
name: string
email: string
@ -33,13 +33,13 @@ export type UserProfileResponse = {
created_at?: string
}
export type UserProfileOriginResponse = {
export interface UserProfileOriginResponse {
json: () => Promise<UserProfileResponse>
bodyUsed: boolean
headers: any
}
export type LangGeniusVersionResponse = {
export interface LangGeniusVersionResponse {
current_version: string
latest_version: string
version: string
@ -49,7 +49,7 @@ export type LangGeniusVersionResponse = {
current_env: string
}
export type TenantInfoResponse = {
export interface TenantInfoResponse {
name: string
created_at: string
providers: Array<{
@ -80,14 +80,14 @@ export enum ProviderName {
Tongyi = 'tongyi',
ChatGLM = 'chatglm',
}
export type ProviderAzureToken = {
export interface ProviderAzureToken {
openai_api_base?: string
openai_api_key?: string
}
export type ProviderAnthropicToken = {
export interface ProviderAnthropicToken {
anthropic_api_key?: string
}
export type ProviderTokenType = {
export interface ProviderTokenType {
[ProviderName.OPENAI]: string
[ProviderName.AZURE_OPENAI]: ProviderAzureToken
[ProviderName.ANTHROPIC]: ProviderAnthropicToken
@ -110,14 +110,14 @@ export type ProviderHosted = Provider & {
quota_used: number
}
export type AccountIntegrate = {
export interface AccountIntegrate {
provider: 'google' | 'github'
created_at: number
is_bound: boolean
link: string
}
export type IWorkspace = {
export interface IWorkspace {
id: string
name: string
plan: string
@ -137,7 +137,7 @@ export type ICurrentWorkspace = Omit<IWorkspace, 'current'> & {
}
}
export type DataSourceNotionPage = {
export interface DataSourceNotionPage {
page_icon: null | {
type: string | null
url: string | null
@ -156,7 +156,7 @@ export type NotionPage = DataSourceNotionPage & {
export type DataSourceNotionPageMap = Record<string, DataSourceNotionPage & { workspace_id: string }>
export type DataSourceNotionWorkspace = {
export interface DataSourceNotionWorkspace {
workspace_name: string
workspace_id: string
workspace_icon: string | null
@ -166,7 +166,7 @@ export type DataSourceNotionWorkspace = {
export type DataSourceNotionWorkspaceMap = Record<string, DataSourceNotionWorkspace>
export type DataSourceNotion = {
export interface DataSourceNotion {
id: string
provider: string
is_bound: boolean
@ -181,12 +181,12 @@ export enum DataSourceProvider {
jinaReader = 'jinareader',
}
export type FirecrawlConfig = {
export interface FirecrawlConfig {
api_key: string
base_url: string
}
export type DataSourceItem = {
export interface DataSourceItem {
id: string
category: DataSourceCategory
provider: DataSourceProvider
@ -195,15 +195,15 @@ export type DataSourceItem = {
updated_at: number
}
export type DataSources = {
export interface DataSources {
sources: DataSourceItem[]
}
export type GithubRepo = {
export interface GithubRepo {
stargazers_count: number
}
export type PluginProvider = {
export interface PluginProvider {
tool_name: string
is_enabled: boolean
credentials: {
@ -211,7 +211,7 @@ export type PluginProvider = {
} | null
}
export type FileUploadConfigResponse = {
export interface FileUploadConfigResponse {
batch_count_limit: number
image_file_size_limit?: number | string // default is 10MB
file_size_limit: number // default is 15MB
@ -234,14 +234,14 @@ export type InvitationResponse = CommonResponse & {
invitation_results: InvitationResult[]
}
export type ApiBasedExtension = {
export interface ApiBasedExtension {
id?: string
name?: string
api_endpoint?: string
api_key?: string
}
export type CodeBasedExtensionForm = {
export interface CodeBasedExtensionForm {
type: string
label: I18nText
variable: string
@ -252,17 +252,17 @@ export type CodeBasedExtensionForm = {
max_length?: number
}
export type CodeBasedExtensionItem = {
export interface CodeBasedExtensionItem {
name: string
label: any
form_schema: CodeBasedExtensionForm[]
}
export type CodeBasedExtension = {
export interface CodeBasedExtension {
module: string
data: CodeBasedExtensionItem[]
}
export type ExternalDataTool = {
export interface ExternalDataTool {
type?: string
label?: string
icon?: string
@ -274,7 +274,7 @@ export type ExternalDataTool = {
} & Partial<Record<string, any>>
}
export type ModerateResponse = {
export interface ModerateResponse {
flagged: boolean
text: string
}

View File

@ -24,7 +24,7 @@ function waitUntilTokenRefreshed() {
const isRefreshingSignAvailable = function (delta: number) {
const nowTime = new Date().getTime()
const lastTime = globalThis.localStorage.getItem('last_refresh_time') || '0'
return nowTime - Number.parseInt(lastTime) <= delta
return nowTime - parseInt(lastTime) <= delta
}
// only one request can send

View File

@ -11,6 +11,7 @@ import type {
PluginDetail,
PluginInfoFromMarketPlace,
PluginTask,
PluginType,
PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse,
VersionInfo,
@ -31,6 +32,7 @@ import {
import { useInvalidateAllBuiltInTools } from './use-tools'
import usePermission from '@/app/components/plugins/plugin-page/use-permission'
import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
const NAME_SPACE = 'plugins'
@ -367,10 +369,11 @@ export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[])
}
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
export const usePluginTaskList = () => {
export const usePluginTaskList = (category?: PluginType) => {
const {
canManagement,
} = usePermission()
const { refreshPluginList } = useRefreshPluginList()
const {
data,
isFetched,
@ -383,8 +386,12 @@ export const usePluginTaskList = () => {
refetchInterval: (lastQuery) => {
const lastData = lastQuery.state.data
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
if (taskDone)
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone) {
if (lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
return false
}
return 5000
},