Merge branch 'main' into fix/chore-fix

This commit is contained in:
Yeuoly 2024-12-09 16:08:19 +08:00
commit 16b49ac436
No known key found for this signature in database
GPG Key ID: A66E7E320FB19F61
31 changed files with 332 additions and 151 deletions

View File

@ -1,5 +1,6 @@
from datetime import UTC, datetime from datetime import UTC, datetime
from flask import request
from flask_login import current_user from flask_login import current_user
from flask_restful import Resource, inputs, marshal_with, reqparse from flask_restful import Resource, inputs, marshal_with, reqparse
from sqlalchemy import and_ from sqlalchemy import and_
@ -20,8 +21,17 @@ class InstalledAppsListApi(Resource):
@account_initialization_required @account_initialization_required
@marshal_with(installed_app_list_fields) @marshal_with(installed_app_list_fields)
def get(self): def get(self):
app_id = request.args.get("app_id", default=None, type=str)
current_tenant_id = current_user.current_tenant_id current_tenant_id = current_user.current_tenant_id
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
if app_id:
installed_apps = (
db.session.query(InstalledApp)
.filter(and_(InstalledApp.tenant_id == current_tenant_id, InstalledApp.app_id == app_id))
.all()
)
else:
installed_apps = db.session.query(InstalledApp).filter(InstalledApp.tenant_id == current_tenant_id).all()
current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant) current_user.role = TenantService.get_user_role(current_user, current_user.current_tenant)
installed_apps = [ installed_apps = [

View File

@ -413,6 +413,7 @@ class ToolWorkflowProviderCreateApi(Resource):
description=args["description"], description=args["description"],
parameters=args["parameters"], parameters=args["parameters"],
privacy_policy=args["privacy_policy"], privacy_policy=args["privacy_policy"],
labels=args["labels"],
) )

View File

@ -2,7 +2,7 @@ from datetime import datetime
from enum import Enum, StrEnum from enum import Enum, StrEnum
from typing import Any, Optional from typing import Any, Optional
from pydantic import BaseModel, field_validator from pydantic import BaseModel
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk
from core.workflow.entities.node_entities import NodeRunMetadataKey from core.workflow.entities.node_entities import NodeRunMetadataKey
@ -113,18 +113,6 @@ class QueueIterationNextEvent(AppQueueEvent):
output: Optional[Any] = None # output for the current iteration output: Optional[Any] = None # output for the current iteration
duration: Optional[float] = None duration: Optional[float] = None
@field_validator("output", mode="before")
@classmethod
def set_output(cls, v):
"""
Set output
"""
if v is None:
return None
if isinstance(v, int | float | str | bool | dict | list):
return v
raise ValueError("output must be a valid type")
class QueueIterationCompletedEvent(AppQueueEvent): class QueueIterationCompletedEvent(AppQueueEvent):
""" """

View File

@ -0,0 +1,38 @@
model: gemini-exp-1206
label:
en_US: Gemini exp 1206
model_type: llm
features:
- agent-thought
- vision
- tool-call
- stream-tool-call
model_properties:
mode: chat
context_size: 2097152
parameter_rules:
- name: temperature
use_template: temperature
- name: top_p
use_template: top_p
- name: top_k
label:
zh_Hans: 取样数量
en_US: Top k
type: int
help:
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
en_US: Only sample from the top K options for each subsequent token.
required: false
- name: max_output_tokens
use_template: max_tokens
default: 8192
min: 1
max: 8192
- name: json_schema
use_template: json_schema
pricing:
input: '0.00'
output: '0.00'
unit: '0.000001'
currency: USD

View File

@ -162,7 +162,7 @@ class TidbService:
clusters = [] clusters = []
tidb_serverless_list_map = {item.cluster_id: item for item in tidb_serverless_list} tidb_serverless_list_map = {item.cluster_id: item for item in tidb_serverless_list}
cluster_ids = [item.cluster_id for item in tidb_serverless_list] cluster_ids = [item.cluster_id for item in tidb_serverless_list]
params = {"clusterIds": cluster_ids, "view": "FULL"} params = {"clusterIds": cluster_ids, "view": "BASIC"}
response = requests.get( response = requests.get(
f"{api_url}/clusters:batchGet", params=params, auth=HTTPDigestAuth(public_key, private_key) f"{api_url}/clusters:batchGet", params=params, auth=HTTPDigestAuth(public_key, private_key)
) )

View File

@ -1,3 +1,4 @@
from collections.abc import Mapping
from datetime import datetime from datetime import datetime
from typing import Any, Optional from typing import Any, Optional
@ -140,8 +141,8 @@ class BaseIterationEvent(GraphEngineEvent):
class IterationRunStartedEvent(BaseIterationEvent): class IterationRunStartedEvent(BaseIterationEvent):
start_at: datetime = Field(..., description="start at") start_at: datetime = Field(..., description="start at")
inputs: Optional[dict[str, Any]] = None inputs: Optional[Mapping[str, Any]] = None
metadata: Optional[dict[str, Any]] = None metadata: Optional[Mapping[str, Any]] = None
predecessor_node_id: Optional[str] = None predecessor_node_id: Optional[str] = None
@ -153,18 +154,18 @@ class IterationRunNextEvent(BaseIterationEvent):
class IterationRunSucceededEvent(BaseIterationEvent): class IterationRunSucceededEvent(BaseIterationEvent):
start_at: datetime = Field(..., description="start at") start_at: datetime = Field(..., description="start at")
inputs: Optional[dict[str, Any]] = None inputs: Optional[Mapping[str, Any]] = None
outputs: Optional[dict[str, Any]] = None outputs: Optional[Mapping[str, Any]] = None
metadata: Optional[dict[str, Any]] = None metadata: Optional[Mapping[str, Any]] = None
steps: int = 0 steps: int = 0
iteration_duration_map: Optional[dict[str, float]] = None iteration_duration_map: Optional[dict[str, float]] = None
class IterationRunFailedEvent(BaseIterationEvent): class IterationRunFailedEvent(BaseIterationEvent):
start_at: datetime = Field(..., description="start at") start_at: datetime = Field(..., description="start at")
inputs: Optional[dict[str, Any]] = None inputs: Optional[Mapping[str, Any]] = None
outputs: Optional[dict[str, Any]] = None outputs: Optional[Mapping[str, Any]] = None
metadata: Optional[dict[str, Any]] = None metadata: Optional[Mapping[str, Any]] = None
steps: int = 0 steps: int = 0
error: str = Field(..., description="failed reason") error: str = Field(..., description="failed reason")

View File

@ -1,6 +1,8 @@
import csv import csv
import io import io
import json import json
import os
import tempfile
import docx import docx
import pandas as pd import pandas as pd
@ -264,14 +266,20 @@ def _extract_text_from_ppt(file_content: bytes) -> str:
def _extract_text_from_pptx(file_content: bytes) -> str: def _extract_text_from_pptx(file_content: bytes) -> str:
try: try:
with io.BytesIO(file_content) as file: if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY:
if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY: with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as temp_file:
elements = partition_via_api( temp_file.write(file_content)
file=file, temp_file.flush()
api_url=dify_config.UNSTRUCTURED_API_URL, with open(temp_file.name, "rb") as file:
api_key=dify_config.UNSTRUCTURED_API_KEY, elements = partition_via_api(
) file=file,
else: metadata_filename=temp_file.name,
api_url=dify_config.UNSTRUCTURED_API_URL,
api_key=dify_config.UNSTRUCTURED_API_KEY,
)
os.unlink(temp_file.name)
else:
with io.BytesIO(file_content) as file:
elements = partition_pptx(file=file) elements = partition_pptx(file=file)
return "\n".join([getattr(element, "text", "") for element in elements]) return "\n".join([getattr(element, "text", "") for element in elements])
except Exception as e: except Exception as e:

View File

@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast
from flask import Flask, current_app from flask import Flask, current_app
from configs import dify_config from configs import dify_config
from core.model_runtime.utils.encoders import jsonable_encoder from core.variables import IntegerVariable
from core.workflow.entities.node_entities import ( from core.workflow.entities.node_entities import (
NodeRunMetadataKey, NodeRunMetadataKey,
NodeRunResult, NodeRunResult,
@ -156,33 +156,35 @@ class IterationNode(BaseNode[IterationNodeData]):
iteration_node_data=self.node_data, iteration_node_data=self.node_data,
index=0, index=0,
pre_iteration_output=None, pre_iteration_output=None,
duration=None,
) )
iter_run_map: dict[str, float] = {} iter_run_map: dict[str, float] = {}
outputs: list[Any] = [None] * len(iterator_list_value) outputs: list[Any] = [None] * len(iterator_list_value)
try: try:
if self.node_data.is_parallel: if self.node_data.is_parallel:
futures: list[Future] = [] futures: list[Future] = []
q = Queue() q: Queue = Queue()
thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100) thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100)
for index, item in enumerate(iterator_list_value): for index, item in enumerate(iterator_list_value):
future: Future = thread_pool.submit( future: Future = thread_pool.submit(
self._run_single_iter_parallel, self._run_single_iter_parallel,
current_app._get_current_object(), # type: ignore flask_app=current_app._get_current_object(), # type: ignore
contextvars.copy_context(), q=q,
q, context=contextvars.copy_context(),
iterator_list_value, iterator_list_value=iterator_list_value,
inputs, inputs=inputs,
outputs, outputs=outputs,
start_at, start_at=start_at,
graph_engine, graph_engine=graph_engine,
iteration_graph, iteration_graph=iteration_graph,
index, index=index,
item, item=item,
iter_run_map, iter_run_map=iter_run_map,
) )
future.add_done_callback(thread_pool.task_done_callback) future.add_done_callback(thread_pool.task_done_callback)
futures.append(future) futures.append(future)
succeeded_count = 0 succeeded_count = 0
empty_count = 0
while True: while True:
try: try:
event = q.get(timeout=1) event = q.get(timeout=1)
@ -210,17 +212,22 @@ class IterationNode(BaseNode[IterationNodeData]):
else: else:
for _ in range(len(iterator_list_value)): for _ in range(len(iterator_list_value)):
yield from self._run_single_iter( yield from self._run_single_iter(
iterator_list_value, iterator_list_value=iterator_list_value,
variable_pool, variable_pool=variable_pool,
inputs, inputs=inputs,
outputs, outputs=outputs,
start_at, start_at=start_at,
graph_engine, graph_engine=graph_engine,
iteration_graph, iteration_graph=iteration_graph,
iter_run_map, iter_run_map=iter_run_map,
) )
if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT: if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT:
outputs = [output for output in outputs if output is not None] outputs = [output for output in outputs if output is not None]
# Flatten the list of lists
if isinstance(outputs, list) and all(isinstance(output, list) for output in outputs):
outputs = [item for sublist in outputs for item in sublist]
yield IterationRunSucceededEvent( yield IterationRunSucceededEvent(
iteration_id=self.id, iteration_id=self.id,
iteration_node_id=self.node_id, iteration_node_id=self.node_id,
@ -228,7 +235,7 @@ class IterationNode(BaseNode[IterationNodeData]):
iteration_node_data=self.node_data, iteration_node_data=self.node_data,
start_at=start_at, start_at=start_at,
inputs=inputs, inputs=inputs,
outputs={"output": jsonable_encoder(outputs)}, outputs={"output": outputs},
steps=len(iterator_list_value), steps=len(iterator_list_value),
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
) )
@ -236,8 +243,11 @@ class IterationNode(BaseNode[IterationNodeData]):
yield RunCompletedEvent( yield RunCompletedEvent(
run_result=NodeRunResult( run_result=NodeRunResult(
status=WorkflowNodeExecutionStatus.SUCCEEDED, status=WorkflowNodeExecutionStatus.SUCCEEDED,
outputs={"output": jsonable_encoder(outputs)}, outputs={"output": outputs},
metadata={NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map}, metadata={
NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map,
NodeRunMetadataKey.TOTAL_TOKENS: graph_engine.graph_runtime_state.total_tokens,
},
) )
) )
except IterationNodeError as e: except IterationNodeError as e:
@ -250,7 +260,7 @@ class IterationNode(BaseNode[IterationNodeData]):
iteration_node_data=self.node_data, iteration_node_data=self.node_data,
start_at=start_at, start_at=start_at,
inputs=inputs, inputs=inputs,
outputs={"output": jsonable_encoder(outputs)}, outputs={"output": outputs},
steps=len(iterator_list_value), steps=len(iterator_list_value),
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
error=str(e), error=str(e),
@ -282,7 +292,7 @@ class IterationNode(BaseNode[IterationNodeData]):
:param node_data: node data :param node_data: node data
:return: :return:
""" """
variable_mapping = { variable_mapping: dict[str, Sequence[str]] = {
f"{node_id}.input_selector": node_data.iterator_selector, f"{node_id}.input_selector": node_data.iterator_selector,
} }
@ -310,7 +320,7 @@ class IterationNode(BaseNode[IterationNodeData]):
sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping( sub_node_variable_mapping = node_cls.extract_variable_selector_to_variable_mapping(
graph_config=graph_config, config=sub_node_config graph_config=graph_config, config=sub_node_config
) )
sub_node_variable_mapping = cast(dict[str, list[str]], sub_node_variable_mapping) sub_node_variable_mapping = cast(dict[str, Sequence[str]], sub_node_variable_mapping)
except NotImplementedError: except NotImplementedError:
sub_node_variable_mapping = {} sub_node_variable_mapping = {}
@ -331,8 +341,12 @@ class IterationNode(BaseNode[IterationNodeData]):
return variable_mapping return variable_mapping
def _handle_event_metadata( def _handle_event_metadata(
self, event: BaseNodeEvent, iter_run_index: str, parallel_mode_run_id: str self,
) -> NodeRunStartedEvent | BaseNodeEvent: *,
event: BaseNodeEvent | InNodeEvent,
iter_run_index: int,
parallel_mode_run_id: str | None,
) -> NodeRunStartedEvent | BaseNodeEvent | InNodeEvent:
""" """
add iteration metadata to event. add iteration metadata to event.
""" """
@ -357,9 +371,10 @@ class IterationNode(BaseNode[IterationNodeData]):
def _run_single_iter( def _run_single_iter(
self, self,
iterator_list_value: list[str], *,
iterator_list_value: Sequence[str],
variable_pool: VariablePool, variable_pool: VariablePool,
inputs: dict[str, list], inputs: Mapping[str, list],
outputs: list, outputs: list,
start_at: datetime, start_at: datetime,
graph_engine: "GraphEngine", graph_engine: "GraphEngine",
@ -375,15 +390,12 @@ class IterationNode(BaseNode[IterationNodeData]):
try: try:
rst = graph_engine.run() rst = graph_engine.run()
# get current iteration index # get current iteration index
variable = variable_pool.get([self.node_id, "index"]) index_variable = variable_pool.get([self.node_id, "index"])
if variable is None: if not isinstance(index_variable, IntegerVariable):
raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found") raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found")
current_index = variable.value current_index = index_variable.value
iteration_run_id = parallel_mode_run_id if parallel_mode_run_id is not None else f"{current_index}" iteration_run_id = parallel_mode_run_id if parallel_mode_run_id is not None else f"{current_index}"
next_index = int(current_index) + 1 next_index = int(current_index) + 1
if current_index is None:
raise IterationIndexNotFoundError(f"iteration {self.node_id} current index not found")
for event in rst: for event in rst:
if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id: if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id:
event.in_iteration_id = self.node_id event.in_iteration_id = self.node_id
@ -396,7 +408,9 @@ class IterationNode(BaseNode[IterationNodeData]):
continue continue
if isinstance(event, NodeRunSucceededEvent): if isinstance(event, NodeRunSucceededEvent):
yield self._handle_event_metadata(event, current_index, parallel_mode_run_id) yield self._handle_event_metadata(
event=event, iter_run_index=current_index, parallel_mode_run_id=parallel_mode_run_id
)
elif isinstance(event, BaseGraphEvent): elif isinstance(event, BaseGraphEvent):
if isinstance(event, GraphRunFailedEvent): if isinstance(event, GraphRunFailedEvent):
# iteration run failed # iteration run failed
@ -409,7 +423,7 @@ class IterationNode(BaseNode[IterationNodeData]):
parallel_mode_run_id=parallel_mode_run_id, parallel_mode_run_id=parallel_mode_run_id,
start_at=start_at, start_at=start_at,
inputs=inputs, inputs=inputs,
outputs={"output": jsonable_encoder(outputs)}, outputs={"output": outputs},
steps=len(iterator_list_value), steps=len(iterator_list_value),
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
error=event.error, error=event.error,
@ -422,7 +436,7 @@ class IterationNode(BaseNode[IterationNodeData]):
iteration_node_data=self.node_data, iteration_node_data=self.node_data,
start_at=start_at, start_at=start_at,
inputs=inputs, inputs=inputs,
outputs={"output": jsonable_encoder(outputs)}, outputs={"output": outputs},
steps=len(iterator_list_value), steps=len(iterator_list_value),
metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens},
error=event.error, error=event.error,
@ -434,9 +448,11 @@ class IterationNode(BaseNode[IterationNodeData]):
) )
) )
return return
else: elif isinstance(event, InNodeEvent):
event = cast(InNodeEvent, event) # event = cast(InNodeEvent, event)
metadata_event = self._handle_event_metadata(event, current_index, parallel_mode_run_id) metadata_event = self._handle_event_metadata(
event=event, iter_run_index=current_index, parallel_mode_run_id=parallel_mode_run_id
)
if isinstance(event, NodeRunFailedEvent): if isinstance(event, NodeRunFailedEvent):
if self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR: if self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR:
yield NodeInIterationFailedEvent( yield NodeInIterationFailedEvent(
@ -518,7 +534,7 @@ class IterationNode(BaseNode[IterationNodeData]):
iteration_node_data=self.node_data, iteration_node_data=self.node_data,
index=next_index, index=next_index,
parallel_mode_run_id=parallel_mode_run_id, parallel_mode_run_id=parallel_mode_run_id,
pre_iteration_output=jsonable_encoder(current_iteration_output) if current_iteration_output else None, pre_iteration_output=current_iteration_output or None,
duration=duration, duration=duration,
) )
@ -545,11 +561,12 @@ class IterationNode(BaseNode[IterationNodeData]):
def _run_single_iter_parallel( def _run_single_iter_parallel(
self, self,
*,
flask_app: Flask, flask_app: Flask,
context: contextvars.Context, context: contextvars.Context,
q: Queue, q: Queue,
iterator_list_value: list[str], iterator_list_value: Sequence[str],
inputs: dict[str, list], inputs: Mapping[str, list],
outputs: list, outputs: list,
start_at: datetime, start_at: datetime,
graph_engine: "GraphEngine", graph_engine: "GraphEngine",
@ -557,7 +574,7 @@ class IterationNode(BaseNode[IterationNodeData]):
index: int, index: int,
item: Any, item: Any,
iter_run_map: dict[str, float], iter_run_map: dict[str, float],
) -> Generator[NodeEvent | InNodeEvent, None, None]: ):
""" """
run single iteration in parallel mode run single iteration in parallel mode
""" """

View File

@ -253,6 +253,8 @@ class NotionOAuth(OAuthDataSource):
} }
response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers) response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers)
response_json = response.json() response_json = response.json()
if response.status_code != 200:
raise ValueError(f"Error fetching block parent page ID: {response_json.message}")
parent = response_json["parent"] parent = response_json["parent"]
parent_type = parent["type"] parent_type = parent["type"]
if parent_type == "block_id": if parent_type == "block_id":

View File

@ -82,6 +82,10 @@ class WorkflowToolManageService:
db.session.add(workflow_tool_provider) db.session.add(workflow_tool_provider)
db.session.commit() db.session.commit()
if labels is not None:
ToolLabelManager.update_tool_labels(
ToolTransformService.workflow_provider_to_controller(workflow_tool_provider), labels
)
return {"result": "success"} return {"result": "success"}
@classmethod @classmethod

View File

@ -37,7 +37,11 @@ def test_dify_config_undefined_entry(example_env_file):
assert config["LOG_LEVEL"] == "INFO" assert config["LOG_LEVEL"] == "INFO"
# NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected.
# This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`.
def test_dify_config(example_env_file): def test_dify_config(example_env_file):
# clear system environment variables
os.environ.clear()
# load dotenv file with pydantic-settings # load dotenv file with pydantic-settings
config = DifyConfig(_env_file=example_env_file) config = DifyConfig(_env_file=example_env_file)

View File

@ -9,7 +9,7 @@ import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast' import Toast, { ToastContext } from '@/app/components/base/toast'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal' import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
@ -31,6 +31,7 @@ import TagSelector from '@/app/components/base/tag-management/selector'
import type { EnvironmentVariable } from '@/app/components/workflow/types' import type { EnvironmentVariable } from '@/app/components/workflow/types'
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
import { fetchWorkflowDraft } from '@/service/workflow' import { fetchWorkflowDraft } from '@/service/workflow'
import { fetchInstalledAppList } from '@/service/explore'
export type AppCardProps = { export type AppCardProps = {
app: App app: App
@ -209,6 +210,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault() e.preventDefault()
setShowConfirmDelete(true) setShowConfirmDelete(true)
} }
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
try {
const { installed_apps }: any = await fetchInstalledAppList(app.id) || {}
if (installed_apps?.length > 0)
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
else
throw new Error('No app found in Explore')
}
catch (e: any) {
Toast.notify({ type: 'error', message: `${e.message || e}` })
}
}
return ( return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}> <div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<button className={s.actionItem} onClick={onClickSettings}> <button className={s.actionItem} onClick={onClickSettings}>
@ -233,6 +249,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
</> </>
)} )}
<Divider className="!my-1" /> <Divider className="!my-1" />
<button className={s.actionItem} onClick={onClickInstalledApp}>
<span className={s.actionName}>{t('app.openInExplore')}</span>
</button>
<Divider className="!my-1" />
<div <div
className={cn(s.actionItem, s.deleteActionItem, 'group')} className={cn(s.actionItem, s.deleteActionItem, 'group')}
onClick={onClickDelete} onClick={onClickDelete}
@ -353,10 +373,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
} }
popupClassName={ popupClassName={
(app.mode === 'completion' || app.mode === 'chat') (app.mode === 'completion' || app.mode === 'chat')
? '!w-[238px] translate-x-[-110px]' ? '!w-[256px] translate-x-[-224px]'
: '' : '!w-[160px] translate-x-[-128px]'
} }
className={'!w-[128px] h-fit !z-20'} className={'h-fit !z-20'}
/> />
</div> </div>
</> </>

View File

@ -5,7 +5,8 @@ import {
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
import Toast from '../../base/toast'
import type { ModelAndParameter } from '../configuration/debug/types' import type { ModelAndParameter } from '../configuration/debug/types'
import SuggestedAction from './suggested-action' import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model' import PublishWithMultipleModel from './publish-with-multiple-model'
@ -15,6 +16,7 @@ import {
PortalToFollowElemContent, PortalToFollowElemContent,
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } from '@/app/components/base/portal-to-follow-elem'
import { fetchInstalledAppList } from '@/service/explore'
import EmbeddedModal from '@/app/components/app/overview/embedded' import EmbeddedModal from '@/app/components/app/overview/embedded'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useGetLanguage } from '@/context/i18n' import { useGetLanguage } from '@/context/i18n'
@ -105,6 +107,19 @@ const AppPublisher = ({
setPublished(false) setPublished(false)
}, [disabled, onToggle, open]) }, [disabled, onToggle, open])
const handleOpenInExplore = useCallback(async () => {
try {
const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {}
if (installed_apps?.length > 0)
window.open(`/explore/installed/${installed_apps[0].id}`, '_blank')
else
throw new Error('No app found in Explore')
}
catch (e: any) {
Toast.notify({ type: 'error', message: `${e.message || e}` })
}
}, [appDetail?.id])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false) const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return ( return (
@ -205,6 +220,15 @@ const AppPublisher = ({
{t('workflow.common.embedIntoSite')} {t('workflow.common.embedIntoSite')}
</SuggestedAction> </SuggestedAction>
)} )}
<SuggestedAction
onClick={() => {
handleOpenInExplore()
}}
disabled={!publishedAt}
icon={<RiPlanetLine className='w-4 h-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction> <SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
{appDetail?.mode === 'workflow' && ( {appDetail?.mode === 'workflow' && (
<WorkflowToolConfigureButton <WorkflowToolConfigureButton

View File

@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
import { checkIsAnimatedImage } from './utils' import { checkIsAnimatedImage } from './utils'
import { ALLOW_FILE_EXTENSIONS } from '@/types/app' import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
type UploaderProps = { export type OnImageInput = {
className?: string (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void (isCropped: false, file: File): void
onUpload?: (file?: File) => void
} }
const Uploader: FC<UploaderProps> = ({ type UploaderProps = {
className?: string
onImageInput?: OnImageInput
}
const ImageInput: FC<UploaderProps> = ({
className, className,
onImageCropped, onImageInput,
onUpload,
}) => { }) => {
const [inputImage, setInputImage] = useState<{ file: File; url: string }>() const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false) const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
@ -37,8 +40,7 @@ const Uploader: FC<UploaderProps> = ({
const onCropComplete = async (_: Area, croppedAreaPixels: Area) => { const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
if (!inputImage) if (!inputImage)
return return
onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
onUpload?.(undefined)
} }
const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => { const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
checkIsAnimatedImage(file).then((isAnimatedImage) => { checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage) setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage) if (isAnimatedImage)
onUpload?.(file) onImageInput?.(false, file)
}) })
} }
} }
@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
) )
} }
export default Uploader export default ImageInput

View File

@ -8,12 +8,14 @@ import Button from '../button'
import { ImagePlus } from '../icons/src/vender/line/images' import { ImagePlus } from '../icons/src/vender/line/images'
import { useLocalFileUploader } from '../image-uploader/hooks' import { useLocalFileUploader } from '../image-uploader/hooks'
import EmojiPickerInner from '../emoji-picker/Inner' import EmojiPickerInner from '../emoji-picker/Inner'
import Uploader from './Uploader' import type { OnImageInput } from './ImageInput'
import ImageInput from './ImageInput'
import s from './style.module.css' import s from './style.module.css'
import getCroppedImg from './utils' import getCroppedImg from './utils'
import type { AppIconType, ImageFile } from '@/types/app' import type { AppIconType, ImageFile } from '@/types/app'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
export type AppIconEmojiSelection = { export type AppIconEmojiSelection = {
type: 'emoji' type: 'emoji'
icon: string icon: string
@ -69,14 +71,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
}, },
}) })
const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>() type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => { const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
}
const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>() const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
const handleUpload = async (file?: File) => { setInputImageInfo(
setUploadImageInfo({ file }) isCropped
? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
: { file: fileOrTempUrl as File },
)
} }
const handleSelect = async () => { const handleSelect = async () => {
@ -90,15 +93,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
} }
} }
else { else {
if (!imageCropInfo && !uploadImageInfo) if (!inputImageInfo)
return return
setUploading(true) setUploading(true)
if (imageCropInfo.file) { if ('file' in inputImageInfo) {
handleLocalFileUpload(imageCropInfo.file) handleLocalFileUpload(inputImageInfo.file)
return return
} }
const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName) const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
handleLocalFileUpload(file) handleLocalFileUpload(file)
} }
} }
@ -127,10 +130,8 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
</div> </div>
</div>} </div>}
<Divider className='m-0' /> <EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} />
<ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} />
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
<Divider className='m-0' /> <Divider className='m-0' />
<div className='w-full flex items-center justify-center p-3 gap-2'> <div className='w-full flex items-center justify-center p-3 gap-2'>

View File

@ -116,12 +116,12 @@ export default async function getCroppedImg(
}) })
} }
export function checkIsAnimatedImage(file) { export function checkIsAnimatedImage(file: File): Promise<boolean> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileReader = new FileReader() const fileReader = new FileReader()
fileReader.onload = function (e) { fileReader.onload = function (e) {
const arr = new Uint8Array(e.target.result) const arr = new Uint8Array(e.target?.result as ArrayBuffer)
// Check file extension // Check file extension
const fileName = file.name.toLowerCase() const fileName = file.name.toLowerCase()
@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) {
} }
// Function to check for WebP signature // Function to check for WebP signature
function isWebP(arr) { function isWebP(arr: Uint8Array) {
return ( return (
arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46 arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
&& arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50 && arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
@ -156,7 +156,7 @@ function isWebP(arr) {
} }
// Function to check if the WebP is animated (contains ANIM chunk) // Function to check if the WebP is animated (contains ANIM chunk)
function checkWebPAnimation(arr) { function checkWebPAnimation(arr: Uint8Array) {
// Search for the ANIM chunk in WebP to determine if it's animated // Search for the ANIM chunk in WebP to determine if it's animated
for (let i = 12; i < arr.length - 4; i++) { for (let i = 12; i < arr.length - 4; i++) {
if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D) if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)

View File

@ -68,7 +68,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
}, [onSelect, selectedEmoji, selectedBackground]) }, [onSelect, selectedEmoji, selectedBackground])
return <div className={cn(className)}> return <div className={cn(className)}>
<div className='flex flex-col items-center w-full px-3'> <div className='flex flex-col items-center w-full px-3 pb-2'>
<div className="relative w-full"> <div className="relative w-full">
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"> <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" /> <MagnifyingGlassIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />

View File

@ -158,13 +158,13 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
export const getFilesInLogs = (rawData: any) => { export const getFilesInLogs = (rawData: any) => {
const result = Object.keys(rawData || {}).map((key) => { const result = Object.keys(rawData || {}).map((key) => {
if (typeof rawData[key] === 'object' && rawData[key].dify_model_identity === '__dify__file__') { if (typeof rawData[key] === 'object' && rawData[key]?.dify_model_identity === '__dify__file__') {
return { return {
varName: key, varName: key,
list: getProcessedFilesFromResponse([rawData[key]]), list: getProcessedFilesFromResponse([rawData[key]]),
} }
} }
if (Array.isArray(rawData[key]) && rawData[key].some(item => item.dify_model_identity === '__dify__file__')) { if (Array.isArray(rawData[key]) && rawData[key].some(item => item?.dify_model_identity === '__dify__file__')) {
return { return {
varName: key, varName: key,
list: getProcessedFilesFromResponse(rawData[key]), list: getProcessedFilesFromResponse(rawData[key]),

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import mermaid from 'mermaid' import mermaid from 'mermaid'
import { usePrevious } from 'ahooks' import { usePrevious } from 'ahooks'
import CryptoJS from 'crypto-js'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline' import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
@ -14,12 +13,6 @@ mermaidAPI = null
if (typeof window !== 'undefined') if (typeof window !== 'undefined')
mermaidAPI = mermaid.mermaidAPI mermaidAPI = mermaid.mermaidAPI
const style = {
minWidth: '480px',
height: 'auto',
overflow: 'auto',
}
const svgToBase64 = (svgGraph: string) => { const svgToBase64 = (svgGraph: string) => {
const svgBytes = new TextEncoder().encode(svgGraph) const svgBytes = new TextEncoder().encode(svgGraph)
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' }) const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
@ -38,7 +31,6 @@ const Flowchart = React.forwardRef((props: {
const [svgCode, setSvgCode] = useState(null) const [svgCode, setSvgCode] = useState(null)
const [look, setLook] = useState<'classic' | 'handDrawn'>('classic') const [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
const prevPrimitiveCode = usePrevious(props.PrimitiveCode) const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const timeRef = useRef<NodeJS.Timeout>() const timeRef = useRef<NodeJS.Timeout>()
@ -51,12 +43,10 @@ const Flowchart = React.forwardRef((props: {
try { try {
if (typeof window !== 'undefined' && mermaidAPI) { if (typeof window !== 'undefined' && mermaidAPI) {
const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode) const svgGraph = await mermaidAPI.render('flowchart', PrimitiveCode)
const base64Svg: any = await svgToBase64(svgGraph.svg) const base64Svg: any = await svgToBase64(svgGraph.svg)
setSvgCode(base64Svg) setSvgCode(base64Svg)
setIsLoading(false) setIsLoading(false)
if (chartId.current && base64Svg)
localStorage.setItem(chartId.current, base64Svg)
} }
} }
catch (error) { catch (error) {
@ -79,19 +69,11 @@ const Flowchart = React.forwardRef((props: {
}, },
}) })
localStorage.removeItem(chartId.current)
renderFlowchart(props.PrimitiveCode) renderFlowchart(props.PrimitiveCode)
} }
}, [look]) }, [look])
useEffect(() => { useEffect(() => {
const cachedSvg: any = localStorage.getItem(chartId.current)
if (cachedSvg) {
setSvgCode(cachedSvg)
setIsLoading(false)
return
}
if (timeRef.current) if (timeRef.current)
clearTimeout(timeRef.current) clearTimeout(timeRef.current)
@ -130,8 +112,8 @@ const Flowchart = React.forwardRef((props: {
</div> </div>
{ {
svgCode svgCode
&& <div className="mermaid cursor-pointer" style={style} onClick={() => setImagePreviewUrl(svgCode)}> && <div className="mermaid cursor-pointer h-auto w-full object-fit: cover" onClick={() => setImagePreviewUrl(svgCode)}>
{svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="mermaid_chart" />} {svgCode && <img src={svgCode} alt="mermaid_chart" />}
</div> </div>
} }
{isLoading {isLoading

View File

@ -72,7 +72,7 @@ const VariableTag = ({
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />} {isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />} {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
<div <div
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')} className={cn('truncate ml-0.5 text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
title={variableName} title={variableName}
> >
{variableName} {variableName}

View File

@ -274,7 +274,7 @@ const VarReferenceVars: FC<Props> = ({
{ {
!hideSearch && ( !hideSearch && (
<> <>
<div className={cn('mb-2 mx-1', searchBoxClassName)} onClick={e => e.stopPropagation()}> <div className={cn('mb-1 mx-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}>
<Input <Input
showLeftIcon showLeftIcon
showClearIcon showClearIcon

View File

@ -25,10 +25,12 @@ import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default
import ConditionWrap from '../condition-wrap' import ConditionWrap from '../condition-wrap'
import ConditionOperator from './condition-operator' import ConditionOperator from './condition-operator'
import ConditionInput from './condition-input' import ConditionInput from './condition-input'
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
import ConditionVarSelector from './condition-var-selector'
import type { import type {
Node, Node,
NodeOutPutVar, NodeOutPutVar,
ValueSelector,
Var, Var,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types' import { VarType } from '@/app/components/workflow/types'
@ -82,6 +84,7 @@ const ConditionItem = ({
const { t } = useTranslation() const { t } = useTranslation()
const [isHovered, setIsHovered] = useState(false) const [isHovered, setIsHovered] = useState(false)
const [open, setOpen] = useState(false)
const doUpdateCondition = useCallback((newCondition: Condition) => { const doUpdateCondition = useCallback((newCondition: Condition) => {
if (isSubVariableKey) if (isSubVariableKey)
@ -190,6 +193,17 @@ const ConditionItem = ({
onRemoveCondition?.(caseId, condition.id) onRemoveCondition?.(caseId, condition.id)
}, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition]) }, [caseId, condition, conditionId, isSubVariableKey, onRemoveCondition, onRemoveSubVariableCondition])
const handleVarChange = useCallback((valueSelector: ValueSelector, varItem: Var) => {
const newCondition = produce(condition, (draft) => {
draft.variable_selector = valueSelector
draft.varType = varItem.type
draft.value = ''
draft.comparison_operator = getOperators(varItem.type)[0]
})
doUpdateCondition(newCondition)
setOpen(false)
}, [condition, doUpdateCondition])
return ( return (
<div className={cn('flex mb-1 last-of-type:mb-0', className)}> <div className={cn('flex mb-1 last-of-type:mb-0', className)}>
<div className={cn( <div className={cn(
@ -221,11 +235,14 @@ const ConditionItem = ({
/> />
) )
: ( : (
<VariableTag <ConditionVarSelector
open={open}
onOpenChange={setOpen}
valueSelector={condition.variable_selector || []} valueSelector={condition.variable_selector || []}
varType={condition.varType} varType={condition.varType}
availableNodes={availableNodes} availableNodes={availableNodes}
isShort nodesOutputVars={nodesOutputVars}
onChange={handleVarChange}
/> />
)} )}

View File

@ -0,0 +1,58 @@
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
import type { Node, NodeOutPutVar, ValueSelector, Var, VarType } from '@/app/components/workflow/types'
type ConditionVarSelectorProps = {
open: boolean
onOpenChange: (open: boolean) => void
valueSelector: ValueSelector
varType: VarType
availableNodes: Node[]
nodesOutputVars: NodeOutPutVar[]
onChange: (valueSelector: ValueSelector, varItem: Var) => void
}
const ConditionVarSelector = ({
open,
onOpenChange,
valueSelector,
varType,
availableNodes,
nodesOutputVars,
onChange,
}: ConditionVarSelectorProps) => {
return (
<PortalToFollowElem
open={open}
onOpenChange={onOpenChange}
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 0,
}}
>
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
<div className="cursor-pointer">
<VariableTag
valueSelector={valueSelector}
varType={varType}
availableNodes={availableNodes}
isShort
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className='w-[296px] bg-components-panel-bg-blur rounded-lg border-[0.5px] border-components-panel-border shadow-lg'>
<VarReferenceVars
vars={nodesOutputVars}
isSupportFileVar
onChange={onChange}
/>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default ConditionVarSelector

View File

@ -73,7 +73,7 @@ const ConditionValue = ({
<div <div
className={cn( className={cn(
'shrink-0 truncate text-xs font-medium text-text-accent', 'shrink-0 ml-0.5 truncate text-xs font-medium text-text-accent',
!notHasValue && 'max-w-[70px]', !notHasValue && 'max-w-[70px]',
)} )}
title={variableName} title={variableName}

View File

@ -35,12 +35,12 @@ const OutputPanel: FC<OutputPanelProps> = ({
for (const key in outputs) { for (const key in outputs) {
if (Array.isArray(outputs[key])) { if (Array.isArray(outputs[key])) {
outputs[key].map((output: any) => { outputs[key].map((output: any) => {
if (output.dify_model_identity === '__dify__file__') if (output?.dify_model_identity === '__dify__file__')
fileList.push(output) fileList.push(output)
return null return null
}) })
} }
else if (outputs[key].dify_model_identity === '__dify__file__') { else if (outputs[key]?.dify_model_identity === '__dify__file__') {
fileList.push(outputs[key]) fileList.push(outputs[key])
} }
} }

View File

@ -101,6 +101,7 @@ const translation = {
switchLabel: 'The app copy to be created', switchLabel: 'The app copy to be created',
removeOriginal: 'Delete the original app', removeOriginal: 'Delete the original app',
switchStart: 'Start switch', switchStart: 'Start switch',
openInExplore: 'Open in Explore',
typeSelector: { typeSelector: {
all: 'ALL Types', all: 'ALL Types',
chatbot: 'Chatbot', chatbot: 'Chatbot',

View File

@ -32,6 +32,7 @@ const translation = {
restore: 'Restore', restore: 'Restore',
runApp: 'Run App', runApp: 'Run App',
batchRunApp: 'Batch Run App', batchRunApp: 'Batch Run App',
openInExplore: 'Open in Explore',
accessAPIReference: 'Access API Reference', accessAPIReference: 'Access API Reference',
embedIntoSite: 'Embed Into Site', embedIntoSite: 'Embed Into Site',
addTitle: 'Add title...', addTitle: 'Add title...',

View File

@ -80,7 +80,7 @@ const translation = {
title: '会話ログ', title: '会話ログ',
workflowTitle: 'ログの詳細', workflowTitle: 'ログの詳細',
fileListLabel: 'ファイルの詳細', fileListLabel: 'ファイルの詳細',
fileListDetail: 'ディテール', fileListDetail: '詳細',
}, },
promptLog: 'プロンプトログ', promptLog: 'プロンプトログ',
agentLog: 'エージェントログ', agentLog: 'エージェントログ',

View File

@ -93,6 +93,7 @@ const translation = {
switchLabel: '作成されるアプリのコピー', switchLabel: '作成されるアプリのコピー',
removeOriginal: '元のアプリを削除する', removeOriginal: '元のアプリを削除する',
switchStart: '切り替えを開始する', switchStart: '切り替えを開始する',
openInExplore: '"探索" で開く',
typeSelector: { typeSelector: {
all: 'すべてのタイプ', all: 'すべてのタイプ',
chatbot: 'チャットボット', chatbot: 'チャットボット',

View File

@ -32,6 +32,7 @@ const translation = {
restore: '復元', restore: '復元',
runApp: 'アプリを実行', runApp: 'アプリを実行',
batchRunApp: 'バッチでアプリを実行', batchRunApp: 'バッチでアプリを実行',
openInExplore: '"探索" で開く',
accessAPIReference: 'APIリファレンスにアクセス', accessAPIReference: 'APIリファレンスにアクセス',
embedIntoSite: 'サイトに埋め込む', embedIntoSite: 'サイトに埋め込む',
addTitle: 'タイトルを追加...', addTitle: 'タイトルを追加...',

View File

@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise<any> => {
return get(`/explore/apps/${id}`) return get(`/explore/apps/${id}`)
} }
export const fetchInstalledAppList = () => { export const fetchInstalledAppList = (app_id?: string | null) => {
return get('/installed-apps') return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`)
} }
export const installApp = (id: string) => { export const installApp = (id: string) => {