mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
Merge branch 'fix/chore-fix' into dev/plugin-deploy
This commit is contained in:
@ -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<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 (
|
||||
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
|
||||
<button className={s.actionItem} onClick={onClickSettings}>
|
||||
@ -233,6 +249,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
</>
|
||||
)}
|
||||
<Divider className="!my-1" />
|
||||
<button className={s.actionItem} onClick={onClickInstalledApp}>
|
||||
<span className={s.actionName}>{t('app.openInExplore')}</span>
|
||||
</button>
|
||||
<Divider className="!my-1" />
|
||||
<div
|
||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||
onClick={onClickDelete}
|
||||
@ -353,10 +373,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
}
|
||||
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'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -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')}
|
||||
</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>
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<WorkflowToolConfigureButton
|
||||
|
||||
@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
|
||||
import { checkIsAnimatedImage } from './utils'
|
||||
import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
|
||||
|
||||
type UploaderProps = {
|
||||
className?: string
|
||||
onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => 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<UploaderProps> = ({
|
||||
type UploaderProps = {
|
||||
className?: string
|
||||
onImageInput?: OnImageInput
|
||||
}
|
||||
|
||||
const ImageInput: FC<UploaderProps> = ({
|
||||
className,
|
||||
onImageCropped,
|
||||
onUpload,
|
||||
onImageInput,
|
||||
}) => {
|
||||
const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
|
||||
const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
|
||||
@ -37,8 +40,7 @@ const Uploader: FC<UploaderProps> = ({
|
||||
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<HTMLInputElement>) => {
|
||||
@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
|
||||
checkIsAnimatedImage(file).then((isAnimatedImage) => {
|
||||
setIsAnimatedImage(!!isAnimatedImage)
|
||||
if (isAnimatedImage)
|
||||
onUpload?.(file)
|
||||
onImageInput?.(false, file)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default Uploader
|
||||
export default ImageInput
|
||||
@ -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<AppIconPickerProps> = ({
|
||||
},
|
||||
})
|
||||
|
||||
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<InputImageInfo>()
|
||||
|
||||
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<AppIconPickerProps> = ({
|
||||
}
|
||||
}
|
||||
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<AppIconPickerProps> = ({
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<Divider className='m-0' />
|
||||
|
||||
<EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
|
||||
<Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
|
||||
<EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} />
|
||||
<ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} />
|
||||
|
||||
<Divider className='m-0' />
|
||||
<div className='w-full flex items-center justify-center p-3 gap-2'>
|
||||
|
||||
@ -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) => {
|
||||
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)
|
||||
|
||||
@ -69,7 +69,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
}, [onSelect, selectedEmoji, selectedBackground])
|
||||
|
||||
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="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" />
|
||||
|
||||
@ -158,13 +158,13 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
|
||||
|
||||
export const getFilesInLogs = (rawData: any) => {
|
||||
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 {
|
||||
varName: 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 {
|
||||
varName: key,
|
||||
list: getProcessedFilesFromResponse(rawData[key]),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import { usePrevious } from 'ahooks'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
@ -14,12 +13,6 @@ mermaidAPI = null
|
||||
if (typeof window !== 'undefined')
|
||||
mermaidAPI = mermaid.mermaidAPI
|
||||
|
||||
const style = {
|
||||
minWidth: '480px',
|
||||
height: 'auto',
|
||||
overflow: 'auto',
|
||||
}
|
||||
|
||||
const svgToBase64 = (svgGraph: string) => {
|
||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||
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 [look, setLook] = useState<'classic' | 'handDrawn'>('classic')
|
||||
|
||||
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
|
||||
const prevPrimitiveCode = usePrevious(props.PrimitiveCode)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const timeRef = useRef<number>()
|
||||
@ -51,12 +43,10 @@ const Flowchart = React.forwardRef((props: {
|
||||
|
||||
try {
|
||||
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)
|
||||
setSvgCode(base64Svg)
|
||||
setIsLoading(false)
|
||||
if (chartId.current && base64Svg)
|
||||
localStorage.setItem(chartId.current, base64Svg)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@ -79,19 +69,11 @@ const Flowchart = React.forwardRef((props: {
|
||||
},
|
||||
})
|
||||
|
||||
localStorage.removeItem(chartId.current)
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}
|
||||
}, [look])
|
||||
|
||||
useEffect(() => {
|
||||
const cachedSvg: any = localStorage.getItem(chartId.current)
|
||||
|
||||
if (cachedSvg) {
|
||||
setSvgCode(cachedSvg)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (timeRef.current)
|
||||
window.clearTimeout(timeRef.current)
|
||||
|
||||
@ -130,8 +112,8 @@ const Flowchart = React.forwardRef((props: {
|
||||
</div>
|
||||
{
|
||||
svgCode
|
||||
&& <div className="mermaid cursor-pointer" style={style} onClick={() => setImagePreviewUrl(svgCode)}>
|
||||
{svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="mermaid_chart" />}
|
||||
&& <div className="mermaid cursor-pointer h-auto w-full object-fit: cover" onClick={() => setImagePreviewUrl(svgCode)}>
|
||||
{svgCode && <img src={svgCode} alt="mermaid_chart" />}
|
||||
</div>
|
||||
}
|
||||
{isLoading
|
||||
|
||||
@ -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' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<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}
|
||||
>
|
||||
{variableName}
|
||||
|
||||
@ -274,7 +274,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
{
|
||||
!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
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
|
||||
@ -25,10 +25,12 @@ import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../default
|
||||
import ConditionWrap from '../condition-wrap'
|
||||
import ConditionOperator from './condition-operator'
|
||||
import ConditionInput from './condition-input'
|
||||
import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag'
|
||||
|
||||
import ConditionVarSelector from './condition-var-selector'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
@ -82,6 +84,7 @@ const ConditionItem = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const doUpdateCondition = useCallback((newCondition: Condition) => {
|
||||
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 (
|
||||
<div className={cn('flex mb-1 last-of-type:mb-0', className)}>
|
||||
<div className={cn(
|
||||
@ -221,11 +235,14 @@ const ConditionItem = ({
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<VariableTag
|
||||
<ConditionVarSelector
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
valueSelector={condition.variable_selector || []}
|
||||
varType={condition.varType}
|
||||
availableNodes={availableNodes}
|
||||
isShort
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
onChange={handleVarChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -73,7 +73,7 @@ const ConditionValue = ({
|
||||
|
||||
<div
|
||||
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]',
|
||||
)}
|
||||
title={variableName}
|
||||
|
||||
@ -35,12 +35,12 @@ const OutputPanel: FC<OutputPanelProps> = ({
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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...',
|
||||
|
||||
@ -80,7 +80,7 @@ const translation = {
|
||||
title: '会話ログ',
|
||||
workflowTitle: 'ログの詳細',
|
||||
fileListLabel: 'ファイルの詳細',
|
||||
fileListDetail: 'ディテール',
|
||||
fileListDetail: '詳細',
|
||||
},
|
||||
promptLog: 'プロンプトログ',
|
||||
agentLog: 'エージェントログ',
|
||||
|
||||
@ -93,6 +93,7 @@ const translation = {
|
||||
switchLabel: '作成されるアプリのコピー',
|
||||
removeOriginal: '元のアプリを削除する',
|
||||
switchStart: '切り替えを開始する',
|
||||
openInExplore: '"探索" で開く',
|
||||
typeSelector: {
|
||||
all: 'すべてのタイプ',
|
||||
chatbot: 'チャットボット',
|
||||
|
||||
@ -32,6 +32,7 @@ const translation = {
|
||||
restore: '復元',
|
||||
runApp: 'アプリを実行',
|
||||
batchRunApp: 'バッチでアプリを実行',
|
||||
openInExplore: '"探索" で開く',
|
||||
accessAPIReference: 'APIリファレンスにアクセス',
|
||||
embedIntoSite: 'サイトに埋め込む',
|
||||
addTitle: 'タイトルを追加...',
|
||||
|
||||
17914
web/pnpm-lock.yaml
generated
17914
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -12,8 +12,8 @@ export const fetchAppDetail = (id: string): Promise<any> => {
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user