Compare commits

..

7 Commits

30 changed files with 842 additions and 370 deletions

View File

@@ -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(

View File

@@ -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}.")

View File

@@ -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

View 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

View 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))

View 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

View 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)

View File

@@ -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

View 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]

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.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.format(queryDateFormat) } })
}
}

View File

@@ -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

View File

@@ -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)
}}
>
{

View File

@@ -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'

View File

@@ -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,

View File

@@ -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'

View 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,
})
}

View File

@@ -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()

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -1,7 +1,6 @@
import {
memo,
useCallback,
useEffect,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
@@ -55,33 +54,14 @@ const NoteNode = ({
handleNodeDelete(id)
}, [id, handleNodeDelete])
const preventZoom = (e: WheelEvent) => {
if (data.selected && e.ctrlKey) {
e.preventDefault()
e.stopPropagation()
}
}
useClickAway(() => {
handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } })
}, ref)
useEffect(() => {
const node = ref.current
if (node)
node.addEventListener('wheel', preventZoom, { passive: false })
return () => {
if (node)
node.removeEventListener('wheel', preventZoom)
}
}, [data.selected])
return (
<div
className={cn(
'flex flex-col relative rounded-md shadow-xs border hover:shadow-md',
data.selected && 'nodrag nopan nowheel',
)}
style={{
background: THEME_MAP[theme].bg,
@@ -122,7 +102,7 @@ const NoteNode = ({
}
<div className='grow px-3 py-2.5 overflow-y-auto'>
<div className={cn(
data.selected && 'cursor-text',
data.selected && 'nodrag nopan nowheel cursor-text',
)}>
<NoteEditor
containerElement={ref.current}

View File

@@ -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>

View File

@@ -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>
))
}

View File

@@ -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}`}
>
{

View File

@@ -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,

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "dify-web",
"version": "0.7.0",
"version": "0.7.1",
"private": true,
"engines": {
"node": ">=18.17.0"