diff --git a/api/controllers/console/explore/installed_app.py b/api/controllers/console/explore/installed_app.py index b60c4e176b..3de179164d 100644 --- a/api/controllers/console/explore/installed_app.py +++ b/api/controllers/console/explore/installed_app.py @@ -1,5 +1,6 @@ from datetime import UTC, datetime +from flask import request from flask_login import current_user from flask_restful import Resource, inputs, marshal_with, reqparse from sqlalchemy import and_ @@ -20,8 +21,17 @@ class InstalledAppsListApi(Resource): @account_initialization_required @marshal_with(installed_app_list_fields) def get(self): + app_id = request.args.get("app_id", default=None, type=str) 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) installed_apps = [ diff --git a/api/controllers/console/workspace/tool_providers.py b/api/controllers/console/workspace/tool_providers.py index e5fcd19754..d18e0ba949 100644 --- a/api/controllers/console/workspace/tool_providers.py +++ b/api/controllers/console/workspace/tool_providers.py @@ -413,6 +413,7 @@ class ToolWorkflowProviderCreateApi(Resource): description=args["description"], parameters=args["parameters"], privacy_policy=args["privacy_policy"], + labels=args["labels"], ) diff --git a/api/core/app/entities/queue_entities.py b/api/core/app/entities/queue_entities.py index 15543638fc..5e9b6517ba 100644 --- a/api/core/app/entities/queue_entities.py +++ b/api/core/app/entities/queue_entities.py @@ -2,7 +2,7 @@ from datetime import datetime from enum import Enum, StrEnum 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.workflow.entities.node_entities import NodeRunMetadataKey @@ -113,18 +113,6 @@ class QueueIterationNextEvent(AppQueueEvent): output: Optional[Any] = None # output for the current iteration 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): """ diff --git a/api/core/model_runtime/model_providers/google/llm/gemini-exp-1206.yaml b/api/core/model_runtime/model_providers/google/llm/gemini-exp-1206.yaml new file mode 100644 index 0000000000..1743d8b968 --- /dev/null +++ b/api/core/model_runtime/model_providers/google/llm/gemini-exp-1206.yaml @@ -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 diff --git a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py index a6f3ad7fef..8dd5922ad0 100644 --- a/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py +++ b/api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_service.py @@ -162,7 +162,7 @@ class TidbService: clusters = [] 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] - params = {"clusterIds": cluster_ids, "view": "FULL"} + params = {"clusterIds": cluster_ids, "view": "BASIC"} response = requests.get( f"{api_url}/clusters:batchGet", params=params, auth=HTTPDigestAuth(public_key, private_key) ) diff --git a/api/core/workflow/graph_engine/entities/event.py b/api/core/workflow/graph_engine/entities/event.py index e95522a36f..55db6fce9a 100644 --- a/api/core/workflow/graph_engine/entities/event.py +++ b/api/core/workflow/graph_engine/entities/event.py @@ -1,3 +1,4 @@ +from collections.abc import Mapping from datetime import datetime from typing import Any, Optional @@ -140,8 +141,8 @@ class BaseIterationEvent(GraphEngineEvent): class IterationRunStartedEvent(BaseIterationEvent): start_at: datetime = Field(..., description="start at") - inputs: Optional[dict[str, Any]] = None - metadata: Optional[dict[str, Any]] = None + inputs: Optional[Mapping[str, Any]] = None + metadata: Optional[Mapping[str, Any]] = None predecessor_node_id: Optional[str] = None @@ -153,18 +154,18 @@ class IterationRunNextEvent(BaseIterationEvent): class IterationRunSucceededEvent(BaseIterationEvent): start_at: datetime = Field(..., description="start at") - inputs: Optional[dict[str, Any]] = None - outputs: Optional[dict[str, Any]] = None - metadata: Optional[dict[str, Any]] = None + inputs: Optional[Mapping[str, Any]] = None + outputs: Optional[Mapping[str, Any]] = None + metadata: Optional[Mapping[str, Any]] = None steps: int = 0 iteration_duration_map: Optional[dict[str, float]] = None class IterationRunFailedEvent(BaseIterationEvent): start_at: datetime = Field(..., description="start at") - inputs: Optional[dict[str, Any]] = None - outputs: Optional[dict[str, Any]] = None - metadata: Optional[dict[str, Any]] = None + inputs: Optional[Mapping[str, Any]] = None + outputs: Optional[Mapping[str, Any]] = None + metadata: Optional[Mapping[str, Any]] = None steps: int = 0 error: str = Field(..., description="failed reason") diff --git a/api/core/workflow/nodes/document_extractor/node.py b/api/core/workflow/nodes/document_extractor/node.py index d490a2eb03..59afe7ac87 100644 --- a/api/core/workflow/nodes/document_extractor/node.py +++ b/api/core/workflow/nodes/document_extractor/node.py @@ -1,6 +1,8 @@ import csv import io import json +import os +import tempfile import docx 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: try: - with io.BytesIO(file_content) as file: - if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY: - elements = partition_via_api( - file=file, - api_url=dify_config.UNSTRUCTURED_API_URL, - api_key=dify_config.UNSTRUCTURED_API_KEY, - ) - else: + if dify_config.UNSTRUCTURED_API_URL and dify_config.UNSTRUCTURED_API_KEY: + with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as temp_file: + temp_file.write(file_content) + temp_file.flush() + with open(temp_file.name, "rb") as file: + elements = partition_via_api( + file=file, + 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) return "\n".join([getattr(element, "text", "") for element in elements]) except Exception as e: diff --git a/api/core/workflow/nodes/iteration/iteration_node.py b/api/core/workflow/nodes/iteration/iteration_node.py index aadbeb4f53..f12d04e852 100644 --- a/api/core/workflow/nodes/iteration/iteration_node.py +++ b/api/core/workflow/nodes/iteration/iteration_node.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast from flask import Flask, current_app 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 ( NodeRunMetadataKey, NodeRunResult, @@ -156,33 +156,35 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_data=self.node_data, index=0, pre_iteration_output=None, + duration=None, ) iter_run_map: dict[str, float] = {} outputs: list[Any] = [None] * len(iterator_list_value) try: if self.node_data.is_parallel: futures: list[Future] = [] - q = Queue() + q: Queue = Queue() thread_pool = GraphEngineThreadPool(max_workers=self.node_data.parallel_nums, max_submit_count=100) for index, item in enumerate(iterator_list_value): future: Future = thread_pool.submit( self._run_single_iter_parallel, - current_app._get_current_object(), # type: ignore - contextvars.copy_context(), - q, - iterator_list_value, - inputs, - outputs, - start_at, - graph_engine, - iteration_graph, - index, - item, - iter_run_map, + flask_app=current_app._get_current_object(), # type: ignore + q=q, + context=contextvars.copy_context(), + iterator_list_value=iterator_list_value, + inputs=inputs, + outputs=outputs, + start_at=start_at, + graph_engine=graph_engine, + iteration_graph=iteration_graph, + index=index, + item=item, + iter_run_map=iter_run_map, ) future.add_done_callback(thread_pool.task_done_callback) futures.append(future) succeeded_count = 0 + empty_count = 0 while True: try: event = q.get(timeout=1) @@ -210,17 +212,22 @@ class IterationNode(BaseNode[IterationNodeData]): else: for _ in range(len(iterator_list_value)): yield from self._run_single_iter( - iterator_list_value, - variable_pool, - inputs, - outputs, - start_at, - graph_engine, - iteration_graph, - iter_run_map, + iterator_list_value=iterator_list_value, + variable_pool=variable_pool, + inputs=inputs, + outputs=outputs, + start_at=start_at, + graph_engine=graph_engine, + iteration_graph=iteration_graph, + iter_run_map=iter_run_map, ) if self.node_data.error_handle_mode == ErrorHandleMode.REMOVE_ABNORMAL_OUTPUT: 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( iteration_id=self.id, iteration_node_id=self.node_id, @@ -228,7 +235,7 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_data=self.node_data, start_at=start_at, inputs=inputs, - outputs={"output": jsonable_encoder(outputs)}, + outputs={"output": outputs}, steps=len(iterator_list_value), metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, ) @@ -236,8 +243,11 @@ class IterationNode(BaseNode[IterationNodeData]): yield RunCompletedEvent( run_result=NodeRunResult( status=WorkflowNodeExecutionStatus.SUCCEEDED, - outputs={"output": jsonable_encoder(outputs)}, - metadata={NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map}, + outputs={"output": outputs}, + metadata={ + NodeRunMetadataKey.ITERATION_DURATION_MAP: iter_run_map, + NodeRunMetadataKey.TOTAL_TOKENS: graph_engine.graph_runtime_state.total_tokens, + }, ) ) except IterationNodeError as e: @@ -250,7 +260,7 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_data=self.node_data, start_at=start_at, inputs=inputs, - outputs={"output": jsonable_encoder(outputs)}, + outputs={"output": outputs}, steps=len(iterator_list_value), metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, error=str(e), @@ -282,7 +292,7 @@ class IterationNode(BaseNode[IterationNodeData]): :param node_data: node data :return: """ - variable_mapping = { + variable_mapping: dict[str, Sequence[str]] = { 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( 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: sub_node_variable_mapping = {} @@ -331,8 +341,12 @@ class IterationNode(BaseNode[IterationNodeData]): return variable_mapping def _handle_event_metadata( - self, event: BaseNodeEvent, iter_run_index: str, parallel_mode_run_id: str - ) -> NodeRunStartedEvent | BaseNodeEvent: + self, + *, + event: BaseNodeEvent | InNodeEvent, + iter_run_index: int, + parallel_mode_run_id: str | None, + ) -> NodeRunStartedEvent | BaseNodeEvent | InNodeEvent: """ add iteration metadata to event. """ @@ -357,9 +371,10 @@ class IterationNode(BaseNode[IterationNodeData]): def _run_single_iter( self, - iterator_list_value: list[str], + *, + iterator_list_value: Sequence[str], variable_pool: VariablePool, - inputs: dict[str, list], + inputs: Mapping[str, list], outputs: list, start_at: datetime, graph_engine: "GraphEngine", @@ -375,15 +390,12 @@ class IterationNode(BaseNode[IterationNodeData]): try: rst = graph_engine.run() # get current iteration index - variable = variable_pool.get([self.node_id, "index"]) - if variable is None: + index_variable = variable_pool.get([self.node_id, "index"]) + if not isinstance(index_variable, IntegerVariable): 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}" 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: if isinstance(event, (BaseNodeEvent | BaseParallelBranchEvent)) and not event.in_iteration_id: event.in_iteration_id = self.node_id @@ -396,7 +408,9 @@ class IterationNode(BaseNode[IterationNodeData]): continue 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): if isinstance(event, GraphRunFailedEvent): # iteration run failed @@ -409,7 +423,7 @@ class IterationNode(BaseNode[IterationNodeData]): parallel_mode_run_id=parallel_mode_run_id, start_at=start_at, inputs=inputs, - outputs={"output": jsonable_encoder(outputs)}, + outputs={"output": outputs}, steps=len(iterator_list_value), metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, error=event.error, @@ -422,7 +436,7 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_data=self.node_data, start_at=start_at, inputs=inputs, - outputs={"output": jsonable_encoder(outputs)}, + outputs={"output": outputs}, steps=len(iterator_list_value), metadata={"total_tokens": graph_engine.graph_runtime_state.total_tokens}, error=event.error, @@ -434,9 +448,11 @@ class IterationNode(BaseNode[IterationNodeData]): ) ) return - else: - event = cast(InNodeEvent, event) - metadata_event = self._handle_event_metadata(event, current_index, parallel_mode_run_id) + elif isinstance(event, InNodeEvent): + # event = cast(InNodeEvent, event) + 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 self.node_data.error_handle_mode == ErrorHandleMode.CONTINUE_ON_ERROR: yield NodeInIterationFailedEvent( @@ -518,7 +534,7 @@ class IterationNode(BaseNode[IterationNodeData]): iteration_node_data=self.node_data, index=next_index, 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, ) @@ -545,11 +561,12 @@ class IterationNode(BaseNode[IterationNodeData]): def _run_single_iter_parallel( self, + *, flask_app: Flask, context: contextvars.Context, q: Queue, - iterator_list_value: list[str], - inputs: dict[str, list], + iterator_list_value: Sequence[str], + inputs: Mapping[str, list], outputs: list, start_at: datetime, graph_engine: "GraphEngine", @@ -557,7 +574,7 @@ class IterationNode(BaseNode[IterationNodeData]): index: int, item: Any, iter_run_map: dict[str, float], - ) -> Generator[NodeEvent | InNodeEvent, None, None]: + ): """ run single iteration in parallel mode """ diff --git a/api/libs/oauth_data_source.py b/api/libs/oauth_data_source.py index 48249e4a35..1d39abd8fa 100644 --- a/api/libs/oauth_data_source.py +++ b/api/libs/oauth_data_source.py @@ -253,6 +253,8 @@ class NotionOAuth(OAuthDataSource): } response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers) 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_type = parent["type"] if parent_type == "block_id": diff --git a/api/services/tools/workflow_tools_manage_service.py b/api/services/tools/workflow_tools_manage_service.py index e112e08357..f3e5ac0503 100644 --- a/api/services/tools/workflow_tools_manage_service.py +++ b/api/services/tools/workflow_tools_manage_service.py @@ -82,6 +82,10 @@ class WorkflowToolManageService: db.session.add(workflow_tool_provider) db.session.commit() + if labels is not None: + ToolLabelManager.update_tool_labels( + ToolTransformService.workflow_provider_to_controller(workflow_tool_provider), labels + ) return {"result": "success"} @classmethod diff --git a/api/tests/unit_tests/configs/test_dify_config.py b/api/tests/unit_tests/configs/test_dify_config.py index 0eb310a51a..385eb08c36 100644 --- a/api/tests/unit_tests/configs/test_dify_config.py +++ b/api/tests/unit_tests/configs/test_dify_config.py @@ -37,7 +37,11 @@ def test_dify_config_undefined_entry(example_env_file): 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): + # clear system environment variables + os.environ.clear() # load dotenv file with pydantic-settings config = DifyConfig(_env_file=example_env_file) diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 1ffb132cf8..fa5bcb596a 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -9,7 +9,7 @@ import s from './style.module.css' import cn from '@/utils/classnames' import type { App } from '@/types/app' 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 DuplicateAppModal 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 DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import { fetchWorkflowDraft } from '@/service/workflow' +import { fetchInstalledAppList } from '@/service/explore' export type AppCardProps = { app: App @@ -209,6 +210,21 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { e.preventDefault() setShowConfirmDelete(true) } + const onClickInstalledApp = async (e: React.MouseEvent) => { + 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 (
+
{ } popupClassName={ (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'} />
diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 0558e29956..3ba35a7336 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -5,7 +5,8 @@ import { } from 'react' import { useTranslation } from 'react-i18next' 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 SuggestedAction from './suggested-action' import PublishWithMultipleModel from './publish-with-multiple-model' @@ -15,6 +16,7 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' +import { fetchInstalledAppList } from '@/service/explore' import EmbeddedModal from '@/app/components/app/overview/embedded' import { useStore as useAppStore } from '@/app/components/app/store' import { useGetLanguage } from '@/context/i18n' @@ -105,6 +107,19 @@ const AppPublisher = ({ setPublished(false) }, [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) return ( @@ -205,6 +220,15 @@ const AppPublisher = ({ {t('workflow.common.embedIntoSite')} )} + { + handleOpenInExplore() + }} + disabled={!publishedAt} + icon={} + > + {t('workflow.common.openInExplore')} + }>{t('workflow.common.accessAPIReference')} {appDetail?.mode === 'workflow' && ( void - onUpload?: (file?: File) => void +export type OnImageInput = { + (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void + (isCropped: false, file: File): void } -const Uploader: FC = ({ +type UploaderProps = { + className?: string + onImageInput?: OnImageInput +} + +const ImageInput: FC = ({ className, - onImageCropped, - onUpload, + onImageInput, }) => { const [inputImage, setInputImage] = useState<{ file: File; url: string }>() const [isAnimatedImage, setIsAnimatedImage] = useState(false) @@ -37,8 +40,7 @@ const Uploader: FC = ({ const onCropComplete = async (_: Area, croppedAreaPixels: Area) => { if (!inputImage) return - onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) - onUpload?.(undefined) + onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name) } const handleLocalFileInput = (e: ChangeEvent) => { @@ -48,7 +50,7 @@ const Uploader: FC = ({ checkIsAnimatedImage(file).then((isAnimatedImage) => { setIsAnimatedImage(!!isAnimatedImage) if (isAnimatedImage) - onUpload?.(file) + onImageInput?.(false, file) }) } } @@ -117,4 +119,4 @@ const Uploader: FC = ({ ) } -export default Uploader +export default ImageInput diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index 8a10d28653..277e2fa1d0 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -8,12 +8,14 @@ import Button from '../button' import { ImagePlus } from '../icons/src/vender/line/images' import { useLocalFileUploader } from '../image-uploader/hooks' 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 getCroppedImg from './utils' import type { AppIconType, ImageFile } from '@/types/app' import cn from '@/utils/classnames' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' + export type AppIconEmojiSelection = { type: 'emoji' icon: string @@ -69,14 +71,15 @@ const AppIconPicker: FC = ({ }, }) - const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>() - const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => { - setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) - } + type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string } + const [inputImageInfo, setInputImageInfo] = useState() - const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>() - const handleUpload = async (file?: File) => { - setUploadImageInfo({ file }) + const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => { + setInputImageInfo( + isCropped + ? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! } + : { file: fileOrTempUrl as File }, + ) } const handleSelect = async () => { @@ -90,15 +93,15 @@ const AppIconPicker: FC = ({ } } else { - if (!imageCropInfo && !uploadImageInfo) + if (!inputImageInfo) return setUploading(true) - if (imageCropInfo.file) { - handleLocalFileUpload(imageCropInfo.file) + if ('file' in inputImageInfo) { + handleLocalFileUpload(inputImageInfo.file) return } - const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName) - const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) + const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName) + const file = new File([blob], inputImageInfo.fileName, { type: blob.type }) handleLocalFileUpload(file) } } @@ -127,10 +130,8 @@ const AppIconPicker: FC = ({
} - - - - + +
diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts index 99154d56da..f63b75eaa1 100644 --- a/web/app/components/base/app-icon-picker/utils.ts +++ b/web/app/components/base/app-icon-picker/utils.ts @@ -116,12 +116,12 @@ export default async function getCroppedImg( }) } -export function checkIsAnimatedImage(file) { +export function checkIsAnimatedImage(file: File): Promise { return new Promise((resolve, reject) => { const fileReader = new FileReader() fileReader.onload = function (e) { - const arr = new Uint8Array(e.target.result) + const arr = new Uint8Array(e.target?.result as ArrayBuffer) // Check file extension const fileName = file.name.toLowerCase() @@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) { } // Function to check for WebP signature -function isWebP(arr) { +function isWebP(arr: Uint8Array) { return ( arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46 && 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 checkWebPAnimation(arr) { +function checkWebPAnimation(arr: Uint8Array) { // Search for the ANIM chunk in WebP to determine if it's animated 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) diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 36c146a2a0..5db223e3f4 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -68,7 +68,7 @@ const EmojiPickerInner: FC = ({ }, [onSelect, selectedEmoji, selectedBackground]) return
-
+
{ svgCode - &&
setImagePreviewUrl(svgCode)}> - {svgCode && mermaid_chart} + &&
setImagePreviewUrl(svgCode)}> + {svgCode && mermaid_chart}
} {isLoading diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx index 6e1b1ed143..fc8c1ce9c9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -72,7 +72,7 @@ const VariableTag = ({ {isEnv && } {isChatVar && }
{variableName} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 9c2cba6e41..eb28279c0c 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -274,7 +274,7 @@ const VarReferenceVars: FC = ({ { !hideSearch && ( <> -
e.stopPropagation()}> +
e.stopPropagation()}> { if (isSubVariableKey) @@ -190,6 +193,17 @@ const ConditionItem = ({ onRemoveCondition?.(caseId, condition.id) }, [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 (
) : ( - )} diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx new file mode 100644 index 0000000000..68a012d1a0 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx @@ -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 ( + + onOpenChange(!open)}> +
+ +
+
+ +
+ +
+
+
+ ) +} + +export default ConditionVarSelector diff --git a/web/app/components/workflow/nodes/if-else/components/condition-value.tsx b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx index 182e38f71e..792064e6ed 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-value.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx @@ -73,7 +73,7 @@ const ConditionValue = ({
= ({ for (const key in outputs) { if (Array.isArray(outputs[key])) { outputs[key].map((output: any) => { - if (output.dify_model_identity === '__dify__file__') + if (output?.dify_model_identity === '__dify__file__') fileList.push(output) return null }) } - else if (outputs[key].dify_model_identity === '__dify__file__') { + else if (outputs[key]?.dify_model_identity === '__dify__file__') { fileList.push(outputs[key]) } } diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 822356449b..c23767af72 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -101,6 +101,7 @@ const translation = { switchLabel: 'The app copy to be created', removeOriginal: 'Delete the original app', switchStart: 'Start switch', + openInExplore: 'Open in Explore', typeSelector: { all: 'ALL Types', chatbot: 'Chatbot', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 6f83203a41..b386fb2d2b 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -32,6 +32,7 @@ const translation = { restore: 'Restore', runApp: 'Run App', batchRunApp: 'Batch Run App', + openInExplore: 'Open in Explore', accessAPIReference: 'Access API Reference', embedIntoSite: 'Embed Into Site', addTitle: 'Add title...', diff --git a/web/i18n/ja-JP/app-log.ts b/web/i18n/ja-JP/app-log.ts index 1d77a17b06..233d70f8e3 100644 --- a/web/i18n/ja-JP/app-log.ts +++ b/web/i18n/ja-JP/app-log.ts @@ -80,7 +80,7 @@ const translation = { title: '会話ログ', workflowTitle: 'ログの詳細', fileListLabel: 'ファイルの詳細', - fileListDetail: 'ディテール', + fileListDetail: '詳細', }, promptLog: 'プロンプトログ', agentLog: 'エージェントログ', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 81b1bf44a4..ecce0b3b50 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -93,6 +93,7 @@ const translation = { switchLabel: '作成されるアプリのコピー', removeOriginal: '元のアプリを削除する', switchStart: '切り替えを開始する', + openInExplore: '"探索" で開く', typeSelector: { all: 'すべてのタイプ', chatbot: 'チャットボット', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index df694810ff..98108e9735 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -32,6 +32,7 @@ const translation = { restore: '復元', runApp: 'アプリを実行', batchRunApp: 'バッチでアプリを実行', + openInExplore: '"探索" で開く', accessAPIReference: 'APIリファレンスにアクセス', embedIntoSite: 'サイトに埋め込む', addTitle: 'タイトルを追加...', diff --git a/web/service/explore.ts b/web/service/explore.ts index bb608f7ee5..e9e17416d1 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise => { return get(`/explore/apps/${id}`) } -export const fetchInstalledAppList = () => { - return get('/installed-apps') +export const fetchInstalledAppList = (app_id?: string | null) => { + return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`) } export const installApp = (id: string) => {