Merge branch 'main' into feat/external-knowledge
This commit is contained in:
commit
4fd57929df
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
||||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description='Dify version',
|
||||
default='0.7.0',
|
||||
default='0.7.1',
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
@ -185,7 +185,7 @@ if you are not sure about the structure.
|
||||
stream=stream,
|
||||
user=user
|
||||
)
|
||||
|
||||
|
||||
model_parameters.pop("response_format")
|
||||
stop = stop or []
|
||||
stop.extend(["\n```", "```\n"])
|
||||
@ -249,10 +249,10 @@ if you are not sure about the structure.
|
||||
prompt_messages=prompt_messages,
|
||||
input_generator=new_generator()
|
||||
)
|
||||
|
||||
|
||||
return response
|
||||
|
||||
def _code_block_mode_stream_processor(self, model: str, prompt_messages: list[PromptMessage],
|
||||
def _code_block_mode_stream_processor(self, model: str, prompt_messages: list[PromptMessage],
|
||||
input_generator: Generator[LLMResultChunk, None, None]
|
||||
) -> Generator[LLMResultChunk, None, None]:
|
||||
"""
|
||||
@ -310,7 +310,7 @@ if you are not sure about the structure.
|
||||
)
|
||||
)
|
||||
|
||||
def _code_block_mode_stream_processor_with_backtick(self, model: str, prompt_messages: list,
|
||||
def _code_block_mode_stream_processor_with_backtick(self, model: str, prompt_messages: list,
|
||||
input_generator: Generator[LLMResultChunk, None, None]) \
|
||||
-> Generator[LLMResultChunk, None, None]:
|
||||
"""
|
||||
@ -470,7 +470,7 @@ if you are not sure about the structure.
|
||||
:return: full response or stream response chunk generator result
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_num_tokens(self, model: str, credentials: dict, prompt_messages: list[PromptMessage],
|
||||
tools: Optional[list[PromptMessageTool]] = None) -> int:
|
||||
@ -792,6 +792,13 @@ if you are not sure about the structure.
|
||||
if not isinstance(parameter_value, str):
|
||||
raise ValueError(f"Model Parameter {parameter_name} should be string.")
|
||||
|
||||
# validate options
|
||||
if parameter_rule.options and parameter_value not in parameter_rule.options:
|
||||
raise ValueError(f"Model Parameter {parameter_name} should be one of {parameter_rule.options}.")
|
||||
elif parameter_rule.type == ParameterType.TEXT:
|
||||
if not isinstance(parameter_value, str):
|
||||
raise ValueError(f"Model Parameter {parameter_name} should be text.")
|
||||
|
||||
# validate options
|
||||
if parameter_rule.options and parameter_value not in parameter_rule.options:
|
||||
raise ValueError(f"Model Parameter {parameter_name} should be one of {parameter_rule.options}.")
|
||||
|
@ -70,7 +70,7 @@ class AzureOpenAIText2SpeechModel(_CommonAzureOpenAI, TTSModel):
|
||||
# doc: https://platform.openai.com/docs/guides/text-to-speech
|
||||
credentials_kwargs = self._to_credential_kwargs(credentials)
|
||||
client = AzureOpenAI(**credentials_kwargs)
|
||||
# max font is 4096,there is 3500 limit for each request
|
||||
# max length is 4096 characters, there is 3500 limit for each request
|
||||
max_length = 3500
|
||||
if len(content_text) > max_length:
|
||||
sentences = self._split_text_into_sentences(content_text, max_length=max_length)
|
||||
|
@ -0,0 +1,44 @@
|
||||
model: gpt-4o-2024-08-06
|
||||
label:
|
||||
zh_Hans: gpt-4o-2024-08-06
|
||||
en_US: gpt-4o-2024-08-06
|
||||
model_type: llm
|
||||
features:
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 128000
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: presence_penalty
|
||||
use_template: presence_penalty
|
||||
- name: frequency_penalty
|
||||
use_template: frequency_penalty
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
default: 512
|
||||
min: 1
|
||||
max: 16384
|
||||
- name: response_format
|
||||
label:
|
||||
zh_Hans: 回复格式
|
||||
en_US: response_format
|
||||
type: string
|
||||
help:
|
||||
zh_Hans: 指定模型必须输出的格式
|
||||
en_US: specifying the format that the model must output
|
||||
required: false
|
||||
options:
|
||||
- text
|
||||
- json_object
|
||||
pricing:
|
||||
input: '2.50'
|
||||
output: '10.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
@ -118,6 +118,9 @@ class _CommonWenxin:
|
||||
'ernie-4.0-turbo-8k-preview': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-4.0-turbo-8k-preview',
|
||||
'yi_34b_chat': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/yi_34b_chat',
|
||||
'embedding-v1': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1',
|
||||
'bge-large-en': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/bge_large_en',
|
||||
'bge-large-zh': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/bge_large_zh',
|
||||
'tao-8k': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/tao_8k',
|
||||
}
|
||||
|
||||
function_calling_supports = [
|
||||
|
@ -0,0 +1,9 @@
|
||||
model: bge-large-en
|
||||
model_type: text-embedding
|
||||
model_properties:
|
||||
context_size: 512
|
||||
max_chunks: 16
|
||||
pricing:
|
||||
input: '0.0005'
|
||||
unit: '0.001'
|
||||
currency: RMB
|
@ -0,0 +1,9 @@
|
||||
model: bge-large-zh
|
||||
model_type: text-embedding
|
||||
model_properties:
|
||||
context_size: 512
|
||||
max_chunks: 16
|
||||
pricing:
|
||||
input: '0.0005'
|
||||
unit: '0.001'
|
||||
currency: RMB
|
@ -0,0 +1,9 @@
|
||||
model: tao-8k
|
||||
model_type: text-embedding
|
||||
model_properties:
|
||||
context_size: 8192
|
||||
max_chunks: 1
|
||||
pricing:
|
||||
input: '0.0005'
|
||||
unit: '0.001'
|
||||
currency: RMB
|
@ -85,7 +85,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel):
|
||||
tools=tools, stop=stop, stream=stream, user=user,
|
||||
extra_model_kwargs=XinferenceHelper.get_xinference_extra_parameter(
|
||||
server_url=credentials['server_url'],
|
||||
model_uid=credentials['model_uid']
|
||||
model_uid=credentials['model_uid'],
|
||||
api_key=credentials.get('api_key'),
|
||||
)
|
||||
)
|
||||
|
||||
@ -106,7 +107,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel):
|
||||
|
||||
extra_param = XinferenceHelper.get_xinference_extra_parameter(
|
||||
server_url=credentials['server_url'],
|
||||
model_uid=credentials['model_uid']
|
||||
model_uid=credentials['model_uid'],
|
||||
api_key=credentials.get('api_key')
|
||||
)
|
||||
if 'completion_type' not in credentials:
|
||||
if 'chat' in extra_param.model_ability:
|
||||
@ -396,7 +398,8 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel):
|
||||
else:
|
||||
extra_args = XinferenceHelper.get_xinference_extra_parameter(
|
||||
server_url=credentials['server_url'],
|
||||
model_uid=credentials['model_uid']
|
||||
model_uid=credentials['model_uid'],
|
||||
api_key=credentials.get('api_key')
|
||||
)
|
||||
|
||||
if 'chat' in extra_args.model_ability:
|
||||
@ -464,6 +467,7 @@ class XinferenceAILargeLanguageModel(LargeLanguageModel):
|
||||
|
||||
xinference_client = Client(
|
||||
base_url=credentials['server_url'],
|
||||
api_key=credentials.get('api_key'),
|
||||
)
|
||||
|
||||
xinference_model = xinference_client.get_model(credentials['model_uid'])
|
||||
|
@ -108,7 +108,8 @@ class XinferenceRerankModel(RerankModel):
|
||||
|
||||
# initialize client
|
||||
client = Client(
|
||||
base_url=credentials['server_url']
|
||||
base_url=credentials['server_url'],
|
||||
api_key=credentials.get('api_key'),
|
||||
)
|
||||
|
||||
xinference_client = client.get_model(model_uid=credentials['model_uid'])
|
||||
|
@ -52,7 +52,8 @@ class XinferenceSpeech2TextModel(Speech2TextModel):
|
||||
|
||||
# initialize client
|
||||
client = Client(
|
||||
base_url=credentials['server_url']
|
||||
base_url=credentials['server_url'],
|
||||
api_key=credentials.get('api_key'),
|
||||
)
|
||||
|
||||
xinference_client = client.get_model(model_uid=credentials['model_uid'])
|
||||
|
@ -110,14 +110,22 @@ class XinferenceTextEmbeddingModel(TextEmbeddingModel):
|
||||
|
||||
server_url = credentials['server_url']
|
||||
model_uid = credentials['model_uid']
|
||||
extra_args = XinferenceHelper.get_xinference_extra_parameter(server_url=server_url, model_uid=model_uid)
|
||||
api_key = credentials.get('api_key')
|
||||
extra_args = XinferenceHelper.get_xinference_extra_parameter(
|
||||
server_url=server_url,
|
||||
model_uid=model_uid,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
if extra_args.max_tokens:
|
||||
credentials['max_tokens'] = extra_args.max_tokens
|
||||
if server_url.endswith('/'):
|
||||
server_url = server_url[:-1]
|
||||
|
||||
client = Client(base_url=server_url)
|
||||
client = Client(
|
||||
base_url=server_url,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
try:
|
||||
handle = client.get_model(model_uid=model_uid)
|
||||
|
@ -81,7 +81,8 @@ class XinferenceText2SpeechModel(TTSModel):
|
||||
|
||||
extra_param = XinferenceHelper.get_xinference_extra_parameter(
|
||||
server_url=credentials['server_url'],
|
||||
model_uid=credentials['model_uid']
|
||||
model_uid=credentials['model_uid'],
|
||||
api_key=credentials.get('api_key'),
|
||||
)
|
||||
|
||||
if 'text-to-audio' not in extra_param.model_ability:
|
||||
@ -203,7 +204,11 @@ class XinferenceText2SpeechModel(TTSModel):
|
||||
credentials['server_url'] = credentials['server_url'][:-1]
|
||||
|
||||
try:
|
||||
handle = RESTfulAudioModelHandle(credentials['model_uid'], credentials['server_url'], auth_headers={})
|
||||
api_key = credentials.get('api_key')
|
||||
auth_headers = {'Authorization': f'Bearer {api_key}'} if api_key else {}
|
||||
handle = RESTfulAudioModelHandle(
|
||||
credentials['model_uid'], credentials['server_url'], auth_headers=auth_headers
|
||||
)
|
||||
|
||||
model_support_voice = [x.get("value") for x in
|
||||
self.get_tts_model_voices(model=model, credentials=credentials)]
|
||||
|
@ -35,13 +35,13 @@ cache_lock = Lock()
|
||||
|
||||
class XinferenceHelper:
|
||||
@staticmethod
|
||||
def get_xinference_extra_parameter(server_url: str, model_uid: str) -> XinferenceModelExtraParameter:
|
||||
def get_xinference_extra_parameter(server_url: str, model_uid: str, api_key: str) -> XinferenceModelExtraParameter:
|
||||
XinferenceHelper._clean_cache()
|
||||
with cache_lock:
|
||||
if model_uid not in cache:
|
||||
cache[model_uid] = {
|
||||
'expires': time() + 300,
|
||||
'value': XinferenceHelper._get_xinference_extra_parameter(server_url, model_uid)
|
||||
'value': XinferenceHelper._get_xinference_extra_parameter(server_url, model_uid, api_key)
|
||||
}
|
||||
return cache[model_uid]['value']
|
||||
|
||||
@ -56,7 +56,7 @@ class XinferenceHelper:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _get_xinference_extra_parameter(server_url: str, model_uid: str) -> XinferenceModelExtraParameter:
|
||||
def _get_xinference_extra_parameter(server_url: str, model_uid: str, api_key: str) -> XinferenceModelExtraParameter:
|
||||
"""
|
||||
get xinference model extra parameter like model_format and model_handle_type
|
||||
"""
|
||||
@ -70,9 +70,10 @@ class XinferenceHelper:
|
||||
session = Session()
|
||||
session.mount('http://', HTTPAdapter(max_retries=3))
|
||||
session.mount('https://', HTTPAdapter(max_retries=3))
|
||||
headers = {'Authorization': f'Bearer {api_key}'} if api_key else {}
|
||||
|
||||
try:
|
||||
response = session.get(url, timeout=10)
|
||||
response = session.get(url, headers=headers, timeout=10)
|
||||
except (MissingSchema, ConnectionError, Timeout) as e:
|
||||
raise RuntimeError(f'get xinference model extra parameter failed, url: {url}, error: {e}')
|
||||
if response.status_code != 200:
|
||||
|
@ -152,8 +152,27 @@ class PGVector(BaseVector):
|
||||
return docs
|
||||
|
||||
def search_by_full_text(self, query: str, **kwargs: Any) -> list[Document]:
|
||||
# do not support bm25 search
|
||||
return []
|
||||
top_k = kwargs.get("top_k", 5)
|
||||
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"""SELECT meta, text, ts_rank(to_tsvector(coalesce(text, '')), to_tsquery(%s)) AS score
|
||||
FROM {self.table_name}
|
||||
WHERE to_tsvector(text) @@ plainto_tsquery(%s)
|
||||
ORDER BY score DESC
|
||||
LIMIT {top_k}""",
|
||||
# f"'{query}'" is required in order to account for whitespace in query
|
||||
(f"'{query}'", f"'{query}'"),
|
||||
)
|
||||
|
||||
docs = []
|
||||
|
||||
for record in cur:
|
||||
metadata, text, score = record
|
||||
metadata["score"] = score
|
||||
docs.append(Document(page_content=text, metadata=metadata))
|
||||
|
||||
return docs
|
||||
|
||||
def delete(self) -> None:
|
||||
with self._get_cursor() as cur:
|
||||
|
49
api/core/tools/provider/builtin/crossref/_assets/icon.svg
Normal file
49
api/core/tools/provider/builtin/crossref/_assets/icon.svg
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 200 130.2" style="enable-background:new 0 0 200 130.2;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#3EB1C8;}
|
||||
.st1{fill:#D8D2C4;}
|
||||
.st2{fill:#4F5858;}
|
||||
.st3{fill:#FFC72C;}
|
||||
.st4{fill:#EF3340;}
|
||||
</style>
|
||||
<g>
|
||||
<polygon class="st0" points="111.8,95.5 111.8,66.8 135.4,59 177.2,73.3 "/>
|
||||
<polygon class="st1" points="153.6,36.8 111.8,51.2 135.4,59 177.2,44.6 "/>
|
||||
<polygon class="st2" points="135.4,59 177.2,44.6 177.2,73.3 "/>
|
||||
<polygon class="st3" points="177.2,0.3 177.2,29 153.6,36.8 111.8,22.5 "/>
|
||||
<polygon class="st4" points="153.6,36.8 111.8,51.2 111.8,22.5 "/>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M26.3,104.8c-0.5-3.7-4.1-6.5-8.1-6.5c-7.3,0-10.1,6.2-10.1,12.7c0,6.2,2.8,12.4,10.1,12.4
|
||||
c5,0,7.8-3.4,8.4-8.3h7.9c-0.8,9.2-7.2,15.2-16.3,15.2C6.8,130.2,0,121.7,0,111c0-11,6.8-19.6,18.2-19.6c8.2,0,15,4.8,16,13.3
|
||||
H26.3z"/>
|
||||
<path class="st2" d="M37.4,102.5h7v5h0.1c1.4-3.4,5-5.7,8.6-5.7c0.5,0,1.1,0.1,1.6,0.3v6.9c-0.7-0.2-1.8-0.3-2.6-0.3
|
||||
c-5.4,0-7.3,3.9-7.3,8.6v12.1h-7.4V102.5z"/>
|
||||
<path class="st2" d="M68.7,101.8c8.5,0,13.9,5.6,13.9,14.2c0,8.5-5.5,14.1-13.9,14.1c-8.4,0-13.9-5.6-13.9-14.1
|
||||
C54.9,107.4,60.3,101.8,68.7,101.8z M68.7,124.5c5,0,6.5-4.3,6.5-8.6c0-4.3-1.5-8.6-6.5-8.6c-5,0-6.5,4.3-6.5,8.6
|
||||
C62.2,120.2,63.8,124.5,68.7,124.5z"/>
|
||||
<path class="st2" d="M91.2,120.6c0.1,3.2,2.8,4.5,5.7,4.5c2.1,0,4.8-0.8,4.8-3.4c0-2.2-3.1-3-8.4-4.2c-4.3-0.9-8.5-2.4-8.5-7.2
|
||||
c0-6.9,5.9-8.6,11.7-8.6c5.9,0,11.3,2,11.8,8.6h-7c-0.2-2.9-2.4-3.6-5-3.6c-1.7,0-4.1,0.3-4.1,2.5c0,2.6,4.2,3,8.4,4
|
||||
c4.3,1,8.5,2.5,8.5,7.5c0,7.1-6.1,9.3-12.3,9.3c-6.2,0-12.3-2.3-12.6-9.5H91.2z"/>
|
||||
<path class="st2" d="M118.1,120.6c0.1,3.2,2.8,4.5,5.7,4.5c2.1,0,4.8-0.8,4.8-3.4c0-2.2-3.1-3-8.4-4.2
|
||||
c-4.3-0.9-8.5-2.4-8.5-7.2c0-6.9,5.9-8.6,11.7-8.6c5.9,0,11.3,2,11.8,8.6h-7c-0.2-2.9-2.4-3.6-5-3.6c-1.7,0-4.1,0.3-4.1,2.5
|
||||
c0,2.6,4.2,3,8.4,4c4.3,1,8.5,2.5,8.5,7.5c0,7.1-6.1,9.3-12.3,9.3c-6.2,0-12.3-2.3-12.6-9.5H118.1z"/>
|
||||
<path class="st2" d="M138.4,102.5h7v5h0.1c1.4-3.4,5-5.7,8.6-5.7c0.5,0,1.1,0.1,1.6,0.3v6.9c-0.7-0.2-1.8-0.3-2.6-0.3
|
||||
c-5.4,0-7.3,3.9-7.3,8.6v12.1h-7.4V102.5z"/>
|
||||
<path class="st2" d="M163.7,117.7c0.2,4.7,2.5,6.8,6.6,6.8c3,0,5.3-1.8,5.8-3.5h6.5c-2.1,6.3-6.5,9-12.6,9
|
||||
c-8.5,0-13.7-5.8-13.7-14.1c0-8,5.6-14.2,13.7-14.2c9.1,0,13.6,7.7,13,15.9H163.7z M175.7,113.1c-0.7-3.7-2.3-5.7-5.9-5.7
|
||||
c-4.7,0-6,3.6-6.1,5.7H175.7z"/>
|
||||
<path class="st2" d="M187.2,107.5h-4.4v-4.9h4.4v-2.1c0-4.7,3-8.2,9-8.2c1.3,0,2.6,0.2,3.9,0.2V98c-0.9-0.1-1.8-0.2-2.7-0.2
|
||||
c-2,0-2.8,0.8-2.8,3.1v1.6h5.1v4.9h-5.1v21.9h-7.4V107.5z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
20
api/core/tools/provider/builtin/crossref/crossref.py
Normal file
20
api/core/tools/provider/builtin/crossref/crossref.py
Normal file
@ -0,0 +1,20 @@
|
||||
from core.tools.errors import ToolProviderCredentialValidationError
|
||||
from core.tools.provider.builtin.crossref.tools.query_doi import CrossRefQueryDOITool
|
||||
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
|
||||
|
||||
|
||||
class CrossRefProvider(BuiltinToolProviderController):
|
||||
def _validate_credentials(self, credentials: dict) -> None:
|
||||
try:
|
||||
CrossRefQueryDOITool().fork_tool_runtime(
|
||||
runtime={
|
||||
"credentials": credentials,
|
||||
}
|
||||
).invoke(
|
||||
user_id='',
|
||||
tool_parameters={
|
||||
"doi": '10.1007/s00894-022-05373-8',
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise ToolProviderCredentialValidationError(str(e))
|
29
api/core/tools/provider/builtin/crossref/crossref.yaml
Normal file
29
api/core/tools/provider/builtin/crossref/crossref.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
identity:
|
||||
author: Sakura4036
|
||||
name: crossref
|
||||
label:
|
||||
en_US: CrossRef
|
||||
zh_Hans: CrossRef
|
||||
description:
|
||||
en_US: Crossref is a cross-publisher reference linking registration query system using DOI technology created in 2000. Crossref establishes cross-database links between the reference list and citation full text of papers, making it very convenient for readers to access the full text of papers.
|
||||
zh_Hans: Crossref是于2000年创建的使用DOI技术的跨出版商参考文献链接注册查询系统。Crossref建立了在论文的参考文献列表和引文全文之间的跨数据库链接,使得读者能够非常便捷地获取文献全文。
|
||||
icon: icon.svg
|
||||
tags:
|
||||
- search
|
||||
credentials_for_provider:
|
||||
mailto:
|
||||
type: text-input
|
||||
required: true
|
||||
label:
|
||||
en_US: email address
|
||||
zh_Hans: email地址
|
||||
pt_BR: email address
|
||||
placeholder:
|
||||
en_US: Please input your email address
|
||||
zh_Hans: 请输入你的email地址
|
||||
pt_BR: Please input your email address
|
||||
help:
|
||||
en_US: According to the requirements of Crossref, an email address is required
|
||||
zh_Hans: 根据Crossref的要求,需要提供一个邮箱地址
|
||||
pt_BR: According to the requirements of Crossref, an email address is required
|
||||
url: https://api.crossref.org/swagger-ui/index.html
|
25
api/core/tools/provider/builtin/crossref/tools/query_doi.py
Normal file
25
api/core/tools/provider/builtin/crossref/tools/query_doi.py
Normal file
@ -0,0 +1,25 @@
|
||||
from typing import Any, Union
|
||||
|
||||
import requests
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.errors import ToolParameterValidationError
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
class CrossRefQueryDOITool(BuiltinTool):
|
||||
"""
|
||||
Tool for querying the metadata of a publication using its DOI.
|
||||
"""
|
||||
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
doi = tool_parameters.get('doi')
|
||||
if not doi:
|
||||
raise ToolParameterValidationError('doi is required.')
|
||||
# doc: https://github.com/CrossRef/rest-api-doc
|
||||
url = f"https://api.crossref.org/works/{doi}"
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
response = response.json()
|
||||
message = response.get('message', {})
|
||||
|
||||
return self.create_json_message(message)
|
@ -0,0 +1,23 @@
|
||||
identity:
|
||||
name: crossref_query_doi
|
||||
author: Sakura4036
|
||||
label:
|
||||
en_US: CrossRef Query DOI
|
||||
zh_Hans: CrossRef DOI 查询
|
||||
pt_BR: CrossRef Query DOI
|
||||
description:
|
||||
human:
|
||||
en_US: A tool for searching literature information using CrossRef by DOI.
|
||||
zh_Hans: 一个使用CrossRef通过DOI获取文献信息的工具。
|
||||
pt_BR: A tool for searching literature information using CrossRef by DOI.
|
||||
llm: A tool for searching literature information using CrossRef by DOI.
|
||||
parameters:
|
||||
- name: doi
|
||||
type: string
|
||||
required: true
|
||||
label:
|
||||
en_US: DOI
|
||||
zh_Hans: DOI
|
||||
pt_BR: DOI
|
||||
llm_description: DOI for searching in CrossRef
|
||||
form: llm
|
120
api/core/tools/provider/builtin/crossref/tools/query_title.py
Normal file
120
api/core/tools/provider/builtin/crossref/tools/query_title.py
Normal file
@ -0,0 +1,120 @@
|
||||
import time
|
||||
from typing import Any, Union
|
||||
|
||||
import requests
|
||||
|
||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
||||
from core.tools.tool.builtin_tool import BuiltinTool
|
||||
|
||||
|
||||
def convert_time_str_to_seconds(time_str: str) -> int:
|
||||
"""
|
||||
Convert a time string to seconds.
|
||||
example: 1s -> 1, 1m30s -> 90, 1h30m -> 5400, 1h30m30s -> 5430
|
||||
"""
|
||||
time_str = time_str.lower().strip().replace(' ', '')
|
||||
seconds = 0
|
||||
if 'h' in time_str:
|
||||
hours, time_str = time_str.split('h')
|
||||
seconds += int(hours) * 3600
|
||||
if 'm' in time_str:
|
||||
minutes, time_str = time_str.split('m')
|
||||
seconds += int(minutes) * 60
|
||||
if 's' in time_str:
|
||||
seconds += int(time_str.replace('s', ''))
|
||||
return seconds
|
||||
|
||||
|
||||
class CrossRefQueryTitleAPI:
|
||||
"""
|
||||
Tool for querying the metadata of a publication using its title.
|
||||
Crossref API doc: https://github.com/CrossRef/rest-api-doc
|
||||
"""
|
||||
query_url_template: str = "https://api.crossref.org/works?query.bibliographic={query}&rows={rows}&offset={offset}&sort={sort}&order={order}&mailto={mailto}"
|
||||
rate_limit: int = 50
|
||||
rate_interval: float = 1
|
||||
max_limit: int = 1000
|
||||
|
||||
def __init__(self, mailto: str):
|
||||
self.mailto = mailto
|
||||
|
||||
def _query(self, query: str, rows: int = 5, offset: int = 0, sort: str = 'relevance', order: str = 'desc', fuzzy_query: bool = False) -> list[dict]:
|
||||
"""
|
||||
Query the metadata of a publication using its title.
|
||||
:param query: the title of the publication
|
||||
:param rows: the number of results to return
|
||||
:param sort: the sort field
|
||||
:param order: the sort order
|
||||
:param fuzzy_query: whether to return all items that match the query
|
||||
"""
|
||||
url = self.query_url_template.format(query=query, rows=rows, offset=offset, sort=sort, order=order, mailto=self.mailto)
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
rate_limit = int(response.headers['x-ratelimit-limit'])
|
||||
# convert time string to seconds
|
||||
rate_interval = convert_time_str_to_seconds(response.headers['x-ratelimit-interval'])
|
||||
|
||||
self.rate_limit = rate_limit
|
||||
self.rate_interval = rate_interval
|
||||
|
||||
response = response.json()
|
||||
if response['status'] != 'ok':
|
||||
return []
|
||||
|
||||
message = response['message']
|
||||
if fuzzy_query:
|
||||
# fuzzy query return all items
|
||||
return message['items']
|
||||
else:
|
||||
for paper in message['items']:
|
||||
title = paper['title'][0]
|
||||
if title.lower() != query.lower():
|
||||
continue
|
||||
return [paper]
|
||||
return []
|
||||
|
||||
def query(self, query: str, rows: int = 5, sort: str = 'relevance', order: str = 'desc', fuzzy_query: bool = False) -> list[dict]:
|
||||
"""
|
||||
Query the metadata of a publication using its title.
|
||||
:param query: the title of the publication
|
||||
:param rows: the number of results to return
|
||||
:param sort: the sort field
|
||||
:param order: the sort order
|
||||
:param fuzzy_query: whether to return all items that match the query
|
||||
"""
|
||||
rows = min(rows, self.max_limit)
|
||||
if rows > self.rate_limit:
|
||||
# query multiple times
|
||||
query_times = rows // self.rate_limit + 1
|
||||
results = []
|
||||
|
||||
for i in range(query_times):
|
||||
result = self._query(query, rows=self.rate_limit, offset=i * self.rate_limit, sort=sort, order=order, fuzzy_query=fuzzy_query)
|
||||
if fuzzy_query:
|
||||
results.extend(result)
|
||||
else:
|
||||
# fuzzy_query=False, only one result
|
||||
if result:
|
||||
return result
|
||||
time.sleep(self.rate_interval)
|
||||
return results
|
||||
else:
|
||||
# query once
|
||||
return self._query(query, rows, sort=sort, order=order, fuzzy_query=fuzzy_query)
|
||||
|
||||
|
||||
class CrossRefQueryTitleTool(BuiltinTool):
|
||||
"""
|
||||
Tool for querying the metadata of a publication using its title.
|
||||
"""
|
||||
def _invoke(self, user_id: str, tool_parameters: dict[str, Any]) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
||||
query = tool_parameters.get('query')
|
||||
fuzzy_query = tool_parameters.get('fuzzy_query', False)
|
||||
rows = tool_parameters.get('rows', 3)
|
||||
sort = tool_parameters.get('sort', 'relevance')
|
||||
order = tool_parameters.get('order', 'desc')
|
||||
mailto = self.runtime.credentials['mailto']
|
||||
|
||||
result = CrossRefQueryTitleAPI(mailto).query(query, rows, sort, order, fuzzy_query)
|
||||
|
||||
return [self.create_json_message(r) for r in result]
|
105
api/core/tools/provider/builtin/crossref/tools/query_title.yaml
Normal file
105
api/core/tools/provider/builtin/crossref/tools/query_title.yaml
Normal file
@ -0,0 +1,105 @@
|
||||
identity:
|
||||
name: crossref_query_title
|
||||
author: Sakura4036
|
||||
label:
|
||||
en_US: CrossRef Title Query
|
||||
zh_Hans: CrossRef 标题查询
|
||||
pt_BR: CrossRef Title Query
|
||||
description:
|
||||
human:
|
||||
en_US: A tool for querying literature information using CrossRef by title.
|
||||
zh_Hans: 一个使用CrossRef通过标题搜索文献信息的工具。
|
||||
pt_BR: A tool for querying literature information using CrossRef by title.
|
||||
llm: A tool for querying literature information using CrossRef by title.
|
||||
parameters:
|
||||
- name: query
|
||||
type: string
|
||||
required: true
|
||||
label:
|
||||
en_US: 标题
|
||||
zh_Hans: 查询语句
|
||||
pt_BR: 标题
|
||||
human_description:
|
||||
en_US: Query bibliographic information, useful for citation look up. Includes titles, authors, ISSNs and publication years
|
||||
zh_Hans: 用于搜索文献信息,有助于查找引用。包括标题,作者,ISSN和出版年份
|
||||
pt_BR: Query bibliographic information, useful for citation look up. Includes titles, authors, ISSNs and publication years
|
||||
llm_description: key words for querying in Web of Science
|
||||
form: llm
|
||||
- name: fuzzy_query
|
||||
type: boolean
|
||||
default: false
|
||||
label:
|
||||
en_US: Whether to fuzzy search
|
||||
zh_Hans: 是否模糊搜索
|
||||
pt_BR: Whether to fuzzy search
|
||||
human_description:
|
||||
en_US: used for selecting the query type, fuzzy query returns more results, precise query returns 1 or none
|
||||
zh_Hans: 用于选择搜索类型,模糊搜索返回更多结果,精确搜索返回1条结果或无
|
||||
pt_BR: used for selecting the query type, fuzzy query returns more results, precise query returns 1 or none
|
||||
form: form
|
||||
- name: limit
|
||||
type: number
|
||||
required: false
|
||||
label:
|
||||
en_US: max query number
|
||||
zh_Hans: 最大搜索数
|
||||
pt_BR: max query number
|
||||
human_description:
|
||||
en_US: max query number(fuzzy search returns the maximum number of results or precise search the maximum number of matches)
|
||||
zh_Hans: 最大搜索数(模糊搜索返回的最大结果数或精确搜索最大匹配数)
|
||||
pt_BR: max query number(fuzzy search returns the maximum number of results or precise search the maximum number of matches)
|
||||
form: llm
|
||||
default: 50
|
||||
- name: sort
|
||||
type: select
|
||||
required: true
|
||||
options:
|
||||
- value: relevance
|
||||
label:
|
||||
en_US: relevance
|
||||
zh_Hans: 相关性
|
||||
pt_BR: relevance
|
||||
- value: published
|
||||
label:
|
||||
en_US: publication date
|
||||
zh_Hans: 出版日期
|
||||
pt_BR: publication date
|
||||
- value: references-count
|
||||
label:
|
||||
en_US: references-count
|
||||
zh_Hans: 引用次数
|
||||
pt_BR: references-count
|
||||
default: relevance
|
||||
label:
|
||||
en_US: sorting field
|
||||
zh_Hans: 排序字段
|
||||
pt_BR: sorting field
|
||||
human_description:
|
||||
en_US: Sorting of query results
|
||||
zh_Hans: 检索结果的排序字段
|
||||
pt_BR: Sorting of query results
|
||||
form: form
|
||||
- name: order
|
||||
type: select
|
||||
required: true
|
||||
options:
|
||||
- value: desc
|
||||
label:
|
||||
en_US: descending
|
||||
zh_Hans: 降序
|
||||
pt_BR: descending
|
||||
- value: asc
|
||||
label:
|
||||
en_US: ascending
|
||||
zh_Hans: 升序
|
||||
pt_BR: ascending
|
||||
default: desc
|
||||
label:
|
||||
en_US: Order
|
||||
zh_Hans: 排序
|
||||
pt_BR: Order
|
||||
human_description:
|
||||
en_US: Order of query results
|
||||
zh_Hans: 检索结果的排序方式
|
||||
pt_BR: Order of query results
|
||||
form: form
|
@ -60,5 +60,11 @@ parameters:
|
||||
label:
|
||||
en_US: Tokenizer
|
||||
human_description:
|
||||
en_US: cl100k_base - gpt-4,gpt-3.5-turbo,gpt-3.5; o200k_base - gpt-4o,gpt-4o-mini; p50k_base - text-davinci-003,text-davinci-002
|
||||
en_US: |
|
||||
· cl100k_base --- gpt-4, gpt-3.5-turbo, gpt-3.5
|
||||
· o200k_base --- gpt-4o, gpt-4o-mini
|
||||
· p50k_base --- text-davinci-003, text-davinci-002
|
||||
· r50k_base --- text-davinci-001, text-curie-001
|
||||
· p50k_edit --- text-davinci-edit-001, code-davinci-edit-001
|
||||
· gpt2 --- gpt-2
|
||||
form: form
|
||||
|
2
api/poetry.lock
generated
2
api/poetry.lock
generated
@ -9584,4 +9584,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "165e4af9cfbce83ee831dd0e82159446ef595d7a7850ee8644c8e2d24dd7040d"
|
||||
content-hash = "a74c7b6a72145d5074aa84581df6e543ea422810caf0ba1561cd2d35497243ca"
|
||||
|
@ -156,6 +156,7 @@ markdown = "~3.5.1"
|
||||
novita-client = "^0.5.6"
|
||||
numpy = "~1.26.4"
|
||||
openai = "~1.29.0"
|
||||
openpyxl = "~3.1.5"
|
||||
oss2 = "2.18.5"
|
||||
pandas = { version = "~2.2.2", extras = ["performance", "excel"] }
|
||||
psycopg2-binary = "~2.9.6"
|
||||
@ -173,7 +174,6 @@ readabilipy = "0.2.0"
|
||||
redis = { version = "~5.0.3", extras = ["hiredis"] }
|
||||
replicate = "~0.22.0"
|
||||
resend = "~0.7.0"
|
||||
safetensors = "~0.4.3"
|
||||
scikit-learn = "^1.5.1"
|
||||
sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
|
||||
sqlalchemy = "~2.0.29"
|
||||
@ -187,10 +187,16 @@ werkzeug = "~3.0.1"
|
||||
xinference-client = "0.13.3"
|
||||
yarl = "~1.9.4"
|
||||
zhipuai = "1.0.7"
|
||||
rank-bm25 = "~0.2.2"
|
||||
openpyxl = "^3.1.5"
|
||||
# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group.
|
||||
|
||||
############################################################
|
||||
# Related transparent dependencies with pinned verion
|
||||
# required by main implementations
|
||||
############################################################
|
||||
[tool.poetry.group.indriect.dependencies]
|
||||
kaleido = "0.2.1"
|
||||
elasticsearch = "8.14.0"
|
||||
rank-bm25 = "~0.2.2"
|
||||
safetensors = "~0.4.3"
|
||||
|
||||
############################################################
|
||||
# Tool dependencies required by tool implementations
|
||||
@ -198,6 +204,7 @@ elasticsearch = "8.14.0"
|
||||
|
||||
[tool.poetry.group.tool.dependencies]
|
||||
arxiv = "2.1.0"
|
||||
cloudscraper = "1.2.71"
|
||||
matplotlib = "~3.8.2"
|
||||
newspaper3k = "0.2.8"
|
||||
duckduckgo-search = "^6.2.6"
|
||||
@ -209,26 +216,25 @@ twilio = "~9.0.4"
|
||||
vanna = { version = "0.5.5", extras = ["postgres", "mysql", "clickhouse", "duckdb"] }
|
||||
wikipedia = "1.4.0"
|
||||
yfinance = "~0.2.40"
|
||||
cloudscraper = "1.2.71"
|
||||
|
||||
############################################################
|
||||
# VDB dependencies required by vector store clients
|
||||
############################################################
|
||||
|
||||
[tool.poetry.group.vdb.dependencies]
|
||||
alibabacloud_gpdb20160503 = "~3.8.0"
|
||||
alibabacloud_tea_openapi = "~0.3.9"
|
||||
chromadb = "0.5.1"
|
||||
clickhouse-connect = "~0.7.16"
|
||||
elasticsearch = "8.14.0"
|
||||
oracledb = "~2.2.1"
|
||||
pgvecto-rs = { version = "~0.2.1", extras = ['sqlalchemy'] }
|
||||
pgvector = "0.2.5"
|
||||
pymilvus = "~2.4.4"
|
||||
pymysql = "1.1.1"
|
||||
tcvectordb = "1.3.2"
|
||||
tidb-vector = "0.0.9"
|
||||
qdrant-client = "1.7.3"
|
||||
weaviate-client = "~3.21.0"
|
||||
alibabacloud_gpdb20160503 = "~3.8.0"
|
||||
alibabacloud_tea_openapi = "~0.3.9"
|
||||
clickhouse-connect = "~0.7.16"
|
||||
|
||||
############################################################
|
||||
# Dev dependencies for running tests
|
||||
@ -252,5 +258,5 @@ pytest-mock = "~3.14.0"
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.lint.dependencies]
|
||||
ruff = "~0.6.1"
|
||||
dotenv-linter = "~0.5.0"
|
||||
ruff = "~0.6.1"
|
||||
|
@ -5,7 +5,7 @@ from core.model_runtime.entities.text_embedding_entities import TextEmbeddingRes
|
||||
from core.model_runtime.model_providers.wenxin.text_embedding.text_embedding import WenxinTextEmbeddingModel
|
||||
|
||||
|
||||
def test_invoke_embedding_model():
|
||||
def test_invoke_embedding_v1():
|
||||
sleep(3)
|
||||
model = WenxinTextEmbeddingModel()
|
||||
|
||||
@ -21,4 +21,61 @@ def test_invoke_embedding_model():
|
||||
|
||||
assert isinstance(response, TextEmbeddingResult)
|
||||
assert len(response.embeddings) == 3
|
||||
assert isinstance(response.embeddings[0], list)
|
||||
assert isinstance(response.embeddings[0], list)
|
||||
|
||||
|
||||
def test_invoke_embedding_bge_large_en():
|
||||
sleep(3)
|
||||
model = WenxinTextEmbeddingModel()
|
||||
|
||||
response = model.invoke(
|
||||
model='bge-large-en',
|
||||
credentials={
|
||||
'api_key': os.environ.get('WENXIN_API_KEY'),
|
||||
'secret_key': os.environ.get('WENXIN_SECRET_KEY')
|
||||
},
|
||||
texts=['hello', '你好', 'xxxxx'],
|
||||
user="abc-123"
|
||||
)
|
||||
|
||||
assert isinstance(response, TextEmbeddingResult)
|
||||
assert len(response.embeddings) == 3
|
||||
assert isinstance(response.embeddings[0], list)
|
||||
|
||||
|
||||
def test_invoke_embedding_bge_large_zh():
|
||||
sleep(3)
|
||||
model = WenxinTextEmbeddingModel()
|
||||
|
||||
response = model.invoke(
|
||||
model='bge-large-zh',
|
||||
credentials={
|
||||
'api_key': os.environ.get('WENXIN_API_KEY'),
|
||||
'secret_key': os.environ.get('WENXIN_SECRET_KEY')
|
||||
},
|
||||
texts=['hello', '你好', 'xxxxx'],
|
||||
user="abc-123"
|
||||
)
|
||||
|
||||
assert isinstance(response, TextEmbeddingResult)
|
||||
assert len(response.embeddings) == 3
|
||||
assert isinstance(response.embeddings[0], list)
|
||||
|
||||
|
||||
def test_invoke_embedding_tao_8k():
|
||||
sleep(3)
|
||||
model = WenxinTextEmbeddingModel()
|
||||
|
||||
response = model.invoke(
|
||||
model='tao-8k',
|
||||
credentials={
|
||||
'api_key': os.environ.get('WENXIN_API_KEY'),
|
||||
'secret_key': os.environ.get('WENXIN_SECRET_KEY')
|
||||
},
|
||||
texts=['hello', '你好', 'xxxxx'],
|
||||
user="abc-123"
|
||||
)
|
||||
|
||||
assert isinstance(response, TextEmbeddingResult)
|
||||
assert len(response.embeddings) == 3
|
||||
assert isinstance(response.embeddings[0], list)
|
||||
|
@ -21,10 +21,6 @@ class PGVectorTest(AbstractVectorTest):
|
||||
),
|
||||
)
|
||||
|
||||
def search_by_full_text(self):
|
||||
hits_by_full_text: list[Document] = self.vector.search_by_full_text(query=get_example_text())
|
||||
assert len(hits_by_full_text) == 0
|
||||
|
||||
|
||||
def test_pgvector(setup_mock_redis):
|
||||
PGVectorTest().run_all_tests()
|
||||
|
@ -2,7 +2,7 @@ version: '3'
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.7.0
|
||||
image: langgenius/dify-api:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
# Startup mode, 'api' starts the API server.
|
||||
@ -229,7 +229,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.7.0
|
||||
image: langgenius/dify-api:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_WEB_URL: ''
|
||||
@ -400,7 +400,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.7.0
|
||||
image: langgenius/dify-web:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
# The base URL of console application api server, refers to the Console base URL of WEB service if console domain is
|
||||
|
@ -34,7 +34,7 @@ services:
|
||||
|
||||
# The DifySandbox
|
||||
sandbox:
|
||||
image: langgenius/dify-sandbox:0.2.1
|
||||
image: langgenius/dify-sandbox:0.2.6
|
||||
restart: always
|
||||
environment:
|
||||
# The DifySandbox configurations
|
||||
|
@ -188,7 +188,7 @@ x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.7.0
|
||||
image: langgenius/dify-api:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -208,7 +208,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.7.0
|
||||
image: langgenius/dify-api:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -227,7 +227,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.7.0
|
||||
image: langgenius/dify-web:0.7.1
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -272,7 +272,7 @@ services:
|
||||
|
||||
# The DifySandbox
|
||||
sandbox:
|
||||
image: langgenius/dify-sandbox:0.2.1
|
||||
image: langgenius/dify-sandbox:0.2.6
|
||||
restart: always
|
||||
environment:
|
||||
# The DifySandbox configurations
|
||||
|
@ -25,7 +25,7 @@ export default function ChartView({ appId }: IChartViewProps) {
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
|
||||
const isWorkflow = appDetail?.mode === 'workflow'
|
||||
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
|
||||
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
|
||||
|
||||
const onSelect = (item: Item) => {
|
||||
if (item.value === 'all') {
|
||||
@ -37,7 +37,7 @@ export default function ChartView({ appId }: IChartViewProps) {
|
||||
setPeriod({ name: item.name, query: { start: startOfToday, end: endOfToday } })
|
||||
}
|
||||
else {
|
||||
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
|
||||
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
|
||||
...(queryParams.period !== 'all'
|
||||
? {
|
||||
start: dayjs().subtract(queryParams.period as number, 'day').startOf('day').format('YYYY-MM-DD HH:mm'),
|
||||
end: dayjs().format('YYYY-MM-DD HH:mm'),
|
||||
end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'),
|
||||
}
|
||||
: {}),
|
||||
...omit(queryParams, ['period']),
|
||||
|
@ -90,7 +90,7 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.code === 'Enter') {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current)
|
||||
@ -100,7 +100,7 @@ const ChatInput: FC<ChatInputProps> = ({
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
isUseInputMethod.current = e.nativeEvent.isComposing
|
||||
if (e.code === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
setQuery(query.replace(/\n$/, ''))
|
||||
e.preventDefault()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import {
|
||||
RiQuestionLine,
|
||||
@ -70,6 +70,16 @@ const Form: FC<FormProps> = ({
|
||||
onChange({ ...value, [key]: val, ...shouldClearVariable })
|
||||
}
|
||||
|
||||
// convert tooltip '\n' to <br />
|
||||
const renderTooltipContent = (content: string) => {
|
||||
return content.split('\n').map((line, index, array) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
{index < array.length - 1 && <br />}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
|
||||
const renderField = (formSchema: CredentialFormSchema) => {
|
||||
const tooltip = formSchema.tooltip
|
||||
const tooltipContent = (tooltip && (
|
||||
@ -77,7 +87,7 @@ const Form: FC<FormProps> = ({
|
||||
<Tooltip popupContent={
|
||||
// w-[100px] caused problem
|
||||
<div className=''>
|
||||
{tooltip[language] || tooltip.en_US}
|
||||
{renderTooltipContent(tooltip[language] || tooltip.en_US)}
|
||||
</div>
|
||||
} >
|
||||
<RiQuestionLine className='w-3 h-3 text-gray-500' />
|
||||
|
@ -35,7 +35,9 @@ const RunMode = memo(() => {
|
||||
'hover:bg-state-accent-hover cursor-pointer',
|
||||
isRunning && 'bg-state-accent-hover !cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => handleWorkflowStartRunInWorkflow()}
|
||||
onClick={() => {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}}
|
||||
>
|
||||
{
|
||||
isRunning
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { WorkflowRunningStatus } from '../types'
|
||||
import { ControlMode, WorkflowRunningStatus } from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -58,6 +58,7 @@ const ViewHistory = ({
|
||||
handleCancelDebugAndPreviewPanel,
|
||||
} = useWorkflowInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
appDetail: state.appDetail,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
@ -173,6 +174,7 @@ const ViewHistory = ({
|
||||
setOpen(false)
|
||||
handleNodesCancelSelected()
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
setControlMode(ControlMode.Hand)
|
||||
}}
|
||||
>
|
||||
{
|
||||
|
@ -7,11 +7,12 @@ export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-template'
|
||||
export * from './use-checklist'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-nodes-layout'
|
||||
export * from './use-workflow-history'
|
||||
export * from './use-workflow-variables'
|
||||
export * from './use-shortcuts'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-workflow-mode'
|
||||
|
@ -48,6 +48,7 @@ import { useHelpline } from './use-helpline'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowReadOnly,
|
||||
} from './use-workflow'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
|
||||
@ -62,6 +63,7 @@ export const useNodesInteractions = () => {
|
||||
getAfterNodesInSameBranch,
|
||||
} = useWorkflow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { getWorkflowReadOnly } = useWorkflowReadOnly()
|
||||
const { handleSetHelpline } = useHelpline()
|
||||
const {
|
||||
handleNodeIterationChildDrag,
|
||||
@ -1029,14 +1031,7 @@ export const useNodesInteractions = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
const { setClipboardElements } = workflowStore.getState()
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
@ -1062,14 +1057,9 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
mousePosition,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
@ -1107,6 +1097,11 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
|
||||
// If only the iteration start node is copied, remove the isIterationStart flag
|
||||
// This new node is movable and can be placed anywhere
|
||||
if (clipboardElements.length === 1 && newNode.data.isIterationStart)
|
||||
newNode.data.isIterationStart = false
|
||||
|
||||
let newChildren: Node[] = []
|
||||
if (nodeToPaste.data.type === BlockEnum.Iteration) {
|
||||
newNode.data._children = [];
|
||||
@ -1145,14 +1140,6 @@ export const useNodesInteractions = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
@ -1175,7 +1162,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
|
||||
}, [store, getNodesReadOnly, handleNodeDelete])
|
||||
|
||||
const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
|
||||
if (getNodesReadOnly())
|
||||
@ -1234,14 +1221,7 @@ export const useNodesInteractions = () => {
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
const handleHistoryBack = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled)
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
const { setEdges, setNodes } = store.getState()
|
||||
@ -1253,17 +1233,10 @@ export const useNodesInteractions = () => {
|
||||
|
||||
setEdges(edges)
|
||||
setNodes(nodes)
|
||||
}, [store, undo, workflowHistoryStore, workflowStore, getNodesReadOnly])
|
||||
}, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
|
||||
|
||||
const handleHistoryForward = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled)
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
const { setEdges, setNodes } = store.getState()
|
||||
@ -1275,7 +1248,7 @@ export const useNodesInteractions = () => {
|
||||
|
||||
setEdges(edges)
|
||||
setNodes(nodes)
|
||||
}, [redo, store, workflowHistoryStore, workflowStore, getNodesReadOnly])
|
||||
}, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
|
@ -8,7 +8,9 @@ import {
|
||||
} from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useWorkflowUpdate } from '../hooks'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from './use-workflow'
|
||||
import { syncWorkflowDraft } from '@/service/workflow'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { API_PREFIX } from '@/config'
|
||||
|
186
web/app/components/workflow/hooks/use-shortcuts.ts
Normal file
186
web/app/components/workflow/hooks/use-shortcuts.ts
Normal file
@ -0,0 +1,186 @@
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
isEventTargetInputArea,
|
||||
} from '../utils'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useNodesInteractions,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowMoveMode,
|
||||
useWorkflowOrganize,
|
||||
useWorkflowStartRun,
|
||||
} from '.'
|
||||
|
||||
export const useShortcuts = (): void => {
|
||||
const {
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
handleHistoryBack,
|
||||
handleHistoryForward,
|
||||
} = useNodesInteractions()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleEdgeDelete } = useEdgesInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
handleModeHand,
|
||||
handleModePointer,
|
||||
} = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
|
||||
const {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
fitView,
|
||||
} = useReactFlow()
|
||||
|
||||
const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
|
||||
const { showFeaturesPanel } = workflowStore.getState()
|
||||
return !showFeaturesPanel && !isEventTargetInputArea(e.target as HTMLElement)
|
||||
}, [workflowStore])
|
||||
|
||||
useKeyPress(['delete', 'backspace'], (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesDelete()
|
||||
handleEdgeDelete()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesCopy()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesPaste()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesDuplicate()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleStartWorkflowRun()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryBack()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(
|
||||
[`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`],
|
||||
(e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryForward()
|
||||
}
|
||||
},
|
||||
{ exactMatch: true, useCapture: true },
|
||||
)
|
||||
|
||||
useKeyPress('h', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModeHand()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('v', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModePointer()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleLayout()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.1', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.5', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
}
|
@ -3,17 +3,29 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants'
|
||||
import type { WorkflowDataUpdator } from '../types'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import produce from 'immer'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
CUSTOM_NODE, DSL_EXPORT_CHECK,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from '../constants'
|
||||
import type { Node, WorkflowDataUpdator } from '../types'
|
||||
import { ControlMode } from '../types'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useSelectionInteractions,
|
||||
useWorkflowReadOnly,
|
||||
} from '../hooks'
|
||||
import { useEdgesInteractions } from './use-edges-interactions'
|
||||
import { useNodesInteractions } from './use-nodes-interactions'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { exportAppConfig } from '@/service/apps'
|
||||
@ -39,6 +51,158 @@ export const useWorkflowInteractions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowMoveMode = () => {
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const {
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const { handleSelectionCancel } = useSelectionInteractions()
|
||||
|
||||
const handleModePointer = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}, [getNodesReadOnly, setControlMode])
|
||||
|
||||
const handleModeHand = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Hand)
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
return {
|
||||
handleModePointer,
|
||||
handleModeHand,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowOrganize = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
const layout = getLayoutByDagre(nodes, edges)
|
||||
const rankMap = {} as Record<string, Node>
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const rank = layout.node(node.id).rank!
|
||||
|
||||
if (!rankMap[rank]) {
|
||||
rankMap[rank] = node
|
||||
}
|
||||
else {
|
||||
if (rankMap[rank].position.y > node.position.y)
|
||||
rankMap[rank] = node
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const nodeWithPosition = layout.node(node.id)
|
||||
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - node.width! / 2,
|
||||
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const zoom = 0.7
|
||||
setViewport({
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom,
|
||||
})
|
||||
saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
return {
|
||||
handleLayout,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowZoom = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getWorkflowReadOnly } = useWorkflowReadOnly()
|
||||
const {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
fitView,
|
||||
} = useReactFlow()
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
|
||||
|
||||
const handleBackToOriginalSize = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
|
||||
|
||||
const handleSizeToHalf = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
|
||||
|
||||
return {
|
||||
handleFitView,
|
||||
handleBackToOriginalSize,
|
||||
handleSizeToHalf,
|
||||
handleZoomOut,
|
||||
handleZoomIn,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowUpdate = () => {
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
@ -7,19 +7,14 @@ import {
|
||||
import dayjs from 'dayjs'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
} from '../utils'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
@ -34,15 +29,12 @@ import {
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useWorkflowTemplate } from './use-workflow-template'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
fetchNodesDefaultConfigs,
|
||||
@ -68,68 +60,13 @@ export const useIsChatMode = () => {
|
||||
export const useWorkflow = () => {
|
||||
const { locale } = useContext(I18n)
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const setPanelWidth = useCallback((width: number) => {
|
||||
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
||||
workflowStore.setState({ panelWidth: width })
|
||||
}, [workflowStore])
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
const layout = getLayoutByDagre(nodes, edges)
|
||||
const rankMap = {} as Record<string, Node>
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const rank = layout.node(node.id).rank!
|
||||
|
||||
if (!rankMap[rank]) {
|
||||
rankMap[rank] = node
|
||||
}
|
||||
else {
|
||||
if (rankMap[rank].position.y > node.position.y)
|
||||
rankMap[rank] = node
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const nodeWithPosition = layout.node(node.id)
|
||||
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - node.width! / 2,
|
||||
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const zoom = 0.7
|
||||
setViewport({
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom,
|
||||
})
|
||||
saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
@ -392,19 +329,8 @@ export const useWorkflow = () => {
|
||||
return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
}, [store])
|
||||
|
||||
const enableShortcuts = useCallback(() => {
|
||||
const { setShortcutsDisabled } = workflowStore.getState()
|
||||
setShortcutsDisabled(false)
|
||||
}, [workflowStore])
|
||||
|
||||
const disableShortcuts = useCallback(() => {
|
||||
const { setShortcutsDisabled } = workflowStore.getState()
|
||||
setShortcutsDisabled(true)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
setPanelWidth,
|
||||
handleLayout,
|
||||
getTreeLeafNodes,
|
||||
getBeforeNodesInSameBranch,
|
||||
getBeforeNodesInSameBranchIncludeParent,
|
||||
@ -418,8 +344,6 @@ export const useWorkflow = () => {
|
||||
getNode,
|
||||
getBeforeNodeById,
|
||||
getIterationNodeChildren,
|
||||
enableShortcuts,
|
||||
disableShortcuts,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
useEventListener,
|
||||
useKeyPress,
|
||||
} from 'ahooks'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
@ -34,6 +33,9 @@ import type {
|
||||
EnvironmentVariable,
|
||||
Node,
|
||||
} from './types'
|
||||
import {
|
||||
ControlMode,
|
||||
} from './types'
|
||||
import { WorkflowContextProvider } from './context'
|
||||
import {
|
||||
useDSL,
|
||||
@ -43,10 +45,10 @@ import {
|
||||
useNodesSyncDraft,
|
||||
usePanelInteractions,
|
||||
useSelectionInteractions,
|
||||
useShortcuts,
|
||||
useWorkflow,
|
||||
useWorkflowInit,
|
||||
useWorkflowReadOnly,
|
||||
useWorkflowStartRun,
|
||||
useWorkflowUpdate,
|
||||
} from './hooks'
|
||||
import Header from './header'
|
||||
@ -70,10 +72,8 @@ import {
|
||||
useWorkflowStore,
|
||||
} from './store'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
isEventTargetInputArea,
|
||||
} from './utils'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
@ -81,7 +81,7 @@ import {
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from './constants'
|
||||
import { WorkflowHistoryProvider, useWorkflowHistoryStore } from './workflow-history-store'
|
||||
import { WorkflowHistoryProvider } from './workflow-history-store'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||
@ -225,17 +225,12 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleNodeConnectStart,
|
||||
handleNodeConnectEnd,
|
||||
handleNodeContextMenu,
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
handleHistoryBack,
|
||||
handleHistoryForward,
|
||||
} = useNodesInteractions()
|
||||
const {
|
||||
handleEdgeEnter,
|
||||
handleEdgeLeave,
|
||||
handleEdgeDelete,
|
||||
handleEdgesChange,
|
||||
} = useEdgesInteractions()
|
||||
const {
|
||||
@ -250,7 +245,6 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
const {
|
||||
isValidConnection,
|
||||
} = useWorkflow()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
const {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
@ -262,41 +256,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
},
|
||||
})
|
||||
|
||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||
|
||||
useKeyPress(['delete', 'backspace'], (e) => {
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
handleNodesDelete()
|
||||
})
|
||||
useKeyPress(['delete', 'backspace'], handleEdgeDelete)
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
handleNodesCopy()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
handleNodesPaste()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(
|
||||
`${getKeyboardKeyCodeBySystem('ctrl')}.z`,
|
||||
() => workflowHistoryShortcutsEnabled && handleHistoryBack(),
|
||||
{ exactMatch: true, useCapture: true },
|
||||
)
|
||||
|
||||
useKeyPress(
|
||||
[`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`],
|
||||
() => workflowHistoryShortcutsEnabled && handleHistoryForward(),
|
||||
{ exactMatch: true, useCapture: true },
|
||||
)
|
||||
useShortcuts()
|
||||
|
||||
const store = useStoreApi()
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@ -388,14 +348,14 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodesConnectable={!nodesReadOnly}
|
||||
nodesFocusable={!nodesReadOnly}
|
||||
edgesFocusable={!nodesReadOnly}
|
||||
panOnDrag={controlMode === 'hand' && !workflowReadOnly}
|
||||
panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly}
|
||||
zoomOnPinch={!workflowReadOnly}
|
||||
zoomOnScroll={!workflowReadOnly}
|
||||
zoomOnDoubleClick={!workflowReadOnly}
|
||||
isValidConnection={isValidConnection}
|
||||
selectionKeyCode={null}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly}
|
||||
selectionOnDrag={controlMode === ControlMode.Pointer && !workflowReadOnly}
|
||||
minZoom={0.25}
|
||||
>
|
||||
<Background
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -10,13 +9,14 @@ import {
|
||||
RiHand,
|
||||
RiStickyNoteAddLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useSelectionInteractions,
|
||||
useWorkflow,
|
||||
useWorkflowMoveMode,
|
||||
useWorkflowOrganize,
|
||||
} from '../hooks'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '../utils'
|
||||
import {
|
||||
ControlMode,
|
||||
} from '../types'
|
||||
import { useStore } from '../store'
|
||||
import AddBlock from './add-block'
|
||||
import TipPopup from './tip-popup'
|
||||
@ -26,62 +26,13 @@ import cn from '@/utils/classnames'
|
||||
const Control = () => {
|
||||
const { t } = useTranslation()
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const { handleLayout } = useWorkflow()
|
||||
const { handleModePointer, handleModeHand } = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleAddNote } = useOperator()
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const { handleSelectionCancel } = useSelectionInteractions()
|
||||
|
||||
const handleModePointer = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
setControlMode('pointer')
|
||||
}, [getNodesReadOnly, setControlMode])
|
||||
const handleModeHand = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
setControlMode('hand')
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
useKeyPress('h', (e) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
handleModeHand()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('v', (e) => {
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
handleModePointer()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
const goLayout = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
handleLayout()
|
||||
}
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
|
||||
e.preventDefault()
|
||||
goLayout()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
const addNote = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (getNodesReadOnly())
|
||||
@ -110,7 +61,7 @@ const Control = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer',
|
||||
controlMode === 'pointer' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
controlMode === ControlMode.Pointer ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={handleModePointer}
|
||||
@ -122,7 +73,7 @@ const Control = () => {
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer',
|
||||
controlMode === 'hand' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
controlMode === ControlMode.Hand ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={handleModeHand}
|
||||
@ -137,7 +88,7 @@ const Control = () => {
|
||||
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={goLayout}
|
||||
onClick={handleLayout}
|
||||
>
|
||||
<RiFunctionAddLine className='w-4 h-4' />
|
||||
</div>
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
RiZoomInLine,
|
||||
RiZoomOutLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useReactFlow,
|
||||
@ -20,9 +19,7 @@ import {
|
||||
useWorkflowReadOnly,
|
||||
} from '../hooks'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
getKeyboardKeyNameBySystem,
|
||||
isEventTargetInputArea,
|
||||
} from '../utils'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
import TipPopup from './tip-popup'
|
||||
@ -116,87 +113,6 @@ const ZoomInOut: FC = () => {
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.1', (e) => {
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.2', (e) => {
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
zoomTo(2)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.5', (e) => {
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
if (isEventTargetInputArea(e.target as HTMLElement))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
@ -289,11 +205,6 @@ const ZoomInOut: FC = () => {
|
||||
<ShortcutsName keys={['shift', '1']} />
|
||||
)
|
||||
}
|
||||
{
|
||||
option.key === ZoomType.zoomTo200 && (
|
||||
<ShortcutsName keys={['shift', '2']} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ import { Panel as NodePanel } from '../nodes'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
} from '../hooks'
|
||||
import DebugAndPreview from './debug-and-preview'
|
||||
import Record from './record'
|
||||
@ -28,10 +27,6 @@ const Panel: FC = () => {
|
||||
const showEnvPanel = useStore(s => s.showEnvPanel)
|
||||
const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
const {
|
||||
enableShortcuts,
|
||||
disableShortcuts,
|
||||
} = useWorkflow()
|
||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
@ -44,8 +39,6 @@ const Panel: FC = () => {
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={cn('absolute top-14 right-0 bottom-2 flex z-10 outline-none')}
|
||||
onFocus={disableShortcuts}
|
||||
onBlur={enableShortcuts}
|
||||
key={`${isRestoring}`}
|
||||
>
|
||||
{
|
||||
|
@ -99,8 +99,6 @@ type Shape = {
|
||||
setWorkflowTools: (tools: ToolWithProvider[]) => void
|
||||
clipboardElements: Node[]
|
||||
setClipboardElements: (clipboardElements: Node[]) => void
|
||||
shortcutsDisabled: boolean
|
||||
setShortcutsDisabled: (shortcutsDisabled: boolean) => void
|
||||
showDebugAndPreviewPanel: boolean
|
||||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
showEnvPanel: boolean
|
||||
@ -217,8 +215,6 @@ export const createWorkflowStore = () => {
|
||||
setWorkflowTools: workflowTools => set(() => ({ workflowTools })),
|
||||
clipboardElements: [],
|
||||
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
|
||||
shortcutsDisabled: false,
|
||||
setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
|
||||
showDebugAndPreviewPanel: false,
|
||||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
showEnvPanel: false,
|
||||
|
@ -29,6 +29,11 @@ export enum BlockEnum {
|
||||
Assigner = 'assigner', // is now named as VariableAssigner
|
||||
}
|
||||
|
||||
export enum ControlMode {
|
||||
Pointer = 'pointer',
|
||||
Hand = 'hand',
|
||||
}
|
||||
|
||||
export type Branch = {
|
||||
id: string
|
||||
name: string
|
||||
|
@ -49,6 +49,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Abbrechen',
|
||||
emoji: 'Emoji',
|
||||
image: 'Bild',
|
||||
},
|
||||
switch: 'Zu Workflow-Orchestrierung wechseln',
|
||||
switchTipStart: 'Eine neue App-Kopie wird für Sie erstellt, und die neue Kopie wird zur Workflow-Orchestrierung wechseln. Die neue Kopie wird ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
emoji: 'Emoji',
|
||||
image: 'Imagen',
|
||||
},
|
||||
switch: 'Cambiar a Orquestación de Flujo de Trabajo',
|
||||
switchTipStart: 'Se creará una nueva copia de la app para ti y la nueva copia cambiará a Orquestación de Flujo de Trabajo. La nueva copia no permitirá',
|
||||
|
@ -74,6 +74,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'باشه',
|
||||
cancel: 'لغو',
|
||||
emoji: 'ایموجی',
|
||||
image: 'تصویر',
|
||||
},
|
||||
switch: 'تغییر به سازماندهی گردش کار',
|
||||
switchTipStart: 'یک نسخه جدید از برنامه برای شما ایجاد خواهد شد و نسخه جدید به سازماندهی گردش کار تغییر خواهد کرد. نسخه جدید ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Annuler',
|
||||
emoji: 'Emoji',
|
||||
image: 'Image',
|
||||
},
|
||||
switch: 'Passer à l\'orchestration de flux de travail',
|
||||
switchTipStart: 'Une nouvelle copie de l\'application sera créée pour vous, et la nouvelle copie passera à l\'orchestration de flux de travail. La nouvelle copie ne permettra pas le ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'ठीक है',
|
||||
cancel: 'रद्द करें',
|
||||
emoji: 'इमोजी',
|
||||
image: 'छवि',
|
||||
},
|
||||
switch: 'वर्कफ़्लो ऑर्केस्ट्रेट पर स्विच करें',
|
||||
switchTipStart: 'आपके लिए एक नई ऐप कॉपी बनाई जाएगी, और नई कॉपी वर्कफ़्लो ऑर्केस्ट्रेट में स्विच हो जाएगी। नई कॉपी ',
|
||||
|
@ -76,6 +76,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Annulla',
|
||||
emoji: 'Emoji',
|
||||
image: 'Immagine',
|
||||
},
|
||||
switch: 'Passa a Orchestrazione del flusso di lavoro',
|
||||
switchTipStart:
|
||||
|
@ -75,6 +75,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'キャンセル',
|
||||
emoji: '絵文字',
|
||||
image: '画像',
|
||||
},
|
||||
switch: 'ワークフロー オーケストレートに切り替える',
|
||||
switchTipStart: '新しいアプリのコピーが作成され、新しいコピーがワークフロー オーケストレートに切り替わります。新しいコピーは ',
|
||||
|
@ -66,6 +66,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: '확인',
|
||||
cancel: '취소',
|
||||
emoji: '이모지',
|
||||
image: '이미지',
|
||||
},
|
||||
switch: '워크플로우 오케스트레이션으로 전환하기',
|
||||
switchTipStart: '새로운 앱의 복사본이 생성되어 새로운 복사본이 워크플로우 오케스트레이션으로 전환됩니다. 새로운 복사본은 ',
|
||||
|
@ -76,6 +76,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Anuluj',
|
||||
emoji: 'Emoji',
|
||||
image: 'Obraz',
|
||||
},
|
||||
switch: 'Przełącz na Orkiestrację Przepływu Pracy',
|
||||
switchTipStart:
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
emoji: 'Emoji',
|
||||
image: 'Imagem',
|
||||
},
|
||||
switch: 'Mudar para Orquestração de Fluxo de Trabalho',
|
||||
switchTipStart: 'Será criada uma nova cópia do aplicativo para você e a nova cópia mudará para Orquestração de Fluxo de Trabalho. A nova cópia não permitirá a ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Anulează',
|
||||
emoji: 'Emoji',
|
||||
image: 'Imagine',
|
||||
},
|
||||
switch: 'Comută la Orchestrare Flux de Lucru',
|
||||
switchTipStart: 'O nouă copie a aplicației va fi creată pentru tine, iar noua copie va comuta la Orchestrare Flux de Lucru. Noua copie ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'Tamam',
|
||||
cancel: 'İptal',
|
||||
emoji: 'Emoji',
|
||||
image: 'Görsel',
|
||||
},
|
||||
switch: 'Workflow Orkestrasyonuna Geç',
|
||||
switchTipStart: 'Sizin için yeni bir uygulama kopyası oluşturulacak ve yeni kopya Workflow Orkestrasyonuna geçecektir. Yeni kopya ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'OK',
|
||||
cancel: 'Скасувати',
|
||||
emoji: 'Емодзі',
|
||||
image: 'Зображення',
|
||||
},
|
||||
switch: 'Перейти до оркестрації робочого процесу',
|
||||
switchTipStart: 'Для вас буде створена нова копія додатка, і нова копія перейде до оркестрації робочого процесу. Нова копія не дозволить ',
|
||||
|
@ -70,6 +70,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: 'Đồng ý',
|
||||
cancel: 'Hủy',
|
||||
emoji: 'Biểu tượng cảm xúc',
|
||||
image: 'Hình ảnh',
|
||||
},
|
||||
switch: 'Chuyển sang quản lý quy trình',
|
||||
switchTipStart: 'Một bản sao ứng dụng mới sẽ được tạo và chuyển sang quản lý quy trình. Bản sao mới sẽ ',
|
||||
|
@ -76,9 +76,9 @@ const translation = {
|
||||
},
|
||||
model: {
|
||||
params: {
|
||||
temperature: 'Nhiệt độ',
|
||||
temperature: 'Độ sáng tạo',
|
||||
temperatureTip:
|
||||
'Kiểm soát độ ngẫu nhiên: Giảm nhiệt độ dẫn đến ít kết quả ngẫu nhiên hơn. Khi nhiệt độ gần bằng 0, mô hình sẽ trở nên xác định và lặp lại.',
|
||||
'Kiểm soát độ ngẫu nhiên: Giảm độ sáng tạo dẫn đến ít kết quả ngẫu nhiên hơn. Khi độ sáng tạo gần bằng 0, mô hình sẽ trở nên xác định và lặp lại.',
|
||||
top_p: 'Top P',
|
||||
top_pTip:
|
||||
'Kiểm soát đa dạng thông qua lấy mẫu nhân nhóm: 0.5 có nghĩa là nửa số tùy chọn có khả năng cao được xem xét.',
|
||||
|
@ -69,6 +69,8 @@ const translation = {
|
||||
iconPicker: {
|
||||
ok: '確認',
|
||||
cancel: '取消',
|
||||
emoji: '表情符號',
|
||||
image: '圖片',
|
||||
},
|
||||
switch: '遷移為工作流編排',
|
||||
switchTipStart: '將為您建立一個使用工作流編排的新應用。新應用將',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
|
Loading…
Reference in New Issue
Block a user