mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
feat: Refactor context generate modal UI and improve UX
This commit is contained in:
parent
9400863949
commit
2f70f778c9
@ -51,15 +51,19 @@ const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, on
|
||||
onClick={handleToggle}
|
||||
asChild
|
||||
>
|
||||
|
||||
<div className={cn('system-xs-medium flex items-center text-text-tertiary', isOpen && 'text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
|
||||
<div className={cn(
|
||||
'system-xs-medium flex items-center text-text-tertiary',
|
||||
isOpen && 'text-text-secondary',
|
||||
moreThanOneVersion && 'cursor-pointer',
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
{t('generate.version', { ns: 'appDebug' })}
|
||||
{' '}
|
||||
{value + 1}
|
||||
{isLatest && ` · ${t('generate.latest', { ns: 'appDebug' })}`}
|
||||
</div>
|
||||
{moreThanOneVersion && <RiArrowDownSLine className="size-3 " />}
|
||||
{moreThanOneVersion && <RiArrowDownSLine className="size-3" />}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className={cn(
|
||||
|
||||
@ -1,25 +1,29 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
|
||||
import type { CodeNodeType, OutputVar } from '@/app/components/workflow/nodes/code/types'
|
||||
import type { ContextGenerateMessage, ContextGenerateResponse } from '@/service/debug'
|
||||
import type { CompletionParams, Model, ModelModeType } from '@/types/app'
|
||||
import { RiSendPlaneLine } from '@remixicon/react'
|
||||
import { useSessionStorageState } from 'ahooks'
|
||||
import { RiArrowDownSLine, RiArrowRightLine, RiCheckLine, RiCloseLine, RiRefreshLine, RiSendPlaneLine, RiSparklingLine } from '@remixicon/react'
|
||||
import { useEventListener, useSessionStorageState, useSize } from 'ahooks'
|
||||
import useBoolean from 'ahooks/lib/useBoolean'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ResPlaceholder from '@/app/components/app/configuration/config/automatic/res-placeholder'
|
||||
import VersionSelector from '@/app/components/app/configuration/config/automatic/version-selector'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
import { CodeAssistant } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import { SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks/use-node-data-update'
|
||||
@ -27,6 +31,7 @@ import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { generateContext } from '@/service/debug'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
@ -40,9 +45,14 @@ type Props = {
|
||||
codeNodeId: string
|
||||
}
|
||||
|
||||
const minCodeHeight = 220
|
||||
const minOutputHeight = 160
|
||||
const splitHandleHeight = 6
|
||||
type ContextGenerateChatMessage = ContextGenerateMessage & {
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
const minCodeHeight = 80
|
||||
const minOutputHeight = 80
|
||||
const splitHandleHeight = 4
|
||||
const defaultCodePanelHeight = 556
|
||||
const defaultCompletionParams: CompletionParams = {
|
||||
temperature: 0.7,
|
||||
max_tokens: 0,
|
||||
@ -90,7 +100,7 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
paramKey,
|
||||
codeNodeId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const nodes = useStore(s => s.nodes)
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -129,15 +139,17 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
current,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
clearVersions,
|
||||
} = useContextGenData({
|
||||
storageKey,
|
||||
})
|
||||
|
||||
const [promptMessages, setPromptMessages] = useSessionStorageState<ContextGenerateMessage[]>(
|
||||
const [promptMessages, setPromptMessages] = useSessionStorageState<ContextGenerateChatMessage[]>(
|
||||
`${storageKey}-messages`,
|
||||
{ defaultValue: [] },
|
||||
)
|
||||
|
||||
const language = useMemo(() => (i18n.language || 'en-US').replace('-', '_'), [i18n.language])
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [isGenerating, { setTrue: setGeneratingTrue, setFalse: setGeneratingFalse }] = useBoolean(false)
|
||||
const [modelOverride, setModelOverride] = useState<Model | null>(() => {
|
||||
@ -197,8 +209,79 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
|
||||
}, [model])
|
||||
|
||||
const chatListRef = useRef<HTMLDivElement>(null)
|
||||
const promptMessageCount = promptMessages?.length ?? 0
|
||||
const hasHistory = (versions?.length ?? 0) > 0 || promptMessageCount > 0
|
||||
const isInitView = !isGenerating && !hasHistory
|
||||
const defaultAssistantMessage = t('nodes.tool.contextGenerate.defaultAssistantMessage', { ns: 'workflow' })
|
||||
const suggestedSkeletonItems = useMemo(() => ([
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
]), [])
|
||||
const versionOptions = useMemo(() => {
|
||||
const latestSuffix = t('generate.latest', { ns: 'appDebug' })
|
||||
const versionPrefix = t('generate.version', { ns: 'appDebug' })
|
||||
return versions.map((_, index) => ({
|
||||
index,
|
||||
label: `${versionPrefix} ${index + 1}${index === versions.length - 1 ? ` · ${latestSuffix}` : ''}`,
|
||||
}))
|
||||
}, [t, versions])
|
||||
const currentVersionIndexSafe = currentVersionIndex ?? 0
|
||||
const currentVersionLabel = versionOptions[currentVersionIndexSafe]?.label
|
||||
?? `${t('generate.version', { ns: 'appDebug' })} ${currentVersionIndexSafe + 1}`
|
||||
|
||||
const rightPlaceholderLines = useMemo(() => {
|
||||
const placeholder = t('nodes.tool.contextGenerate.rightSidePlaceholder', { ns: 'workflow' })
|
||||
return String(placeholder).split('\n').filter(Boolean)
|
||||
}, [t])
|
||||
|
||||
const [isVersionMenuOpen, setVersionMenuOpen] = useState(false)
|
||||
const handleVersionMenuOpen = useCallback((open: boolean) => {
|
||||
if (versions.length > 1)
|
||||
setVersionMenuOpen(open)
|
||||
else
|
||||
setVersionMenuOpen(false)
|
||||
}, [versions.length])
|
||||
const handleVersionMenuToggle = useCallback(() => {
|
||||
if (versions.length > 1)
|
||||
setVersionMenuOpen(value => !value)
|
||||
}, [versions.length])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (isGenerating)
|
||||
return
|
||||
setPromptMessages([])
|
||||
setInputValue('')
|
||||
clearVersions()
|
||||
}, [clearVersions, isGenerating, setPromptMessages])
|
||||
|
||||
const renderModelTrigger = useCallback((params: TriggerProps) => {
|
||||
const label = params.currentModel?.label
|
||||
? renderI18nObject(params.currentModel.label, language)
|
||||
: (params.currentModel?.model || params.modelId || model.name)
|
||||
const modelName = params.currentModel?.model || params.modelId || model.name
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-lg px-1.5 py-1 text-xs text-text-tertiary',
|
||||
params.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
<ModelIcon
|
||||
provider={params.currentProvider}
|
||||
modelName={modelName}
|
||||
className="!h-4 !w-4"
|
||||
iconClassName="!h-4 !w-4"
|
||||
/>
|
||||
<span className="max-w-[200px] truncate font-medium text-text-tertiary">
|
||||
{label}
|
||||
</span>
|
||||
<RiArrowDownSLine className="h-3.5 w-3.5 text-text-tertiary" />
|
||||
</div>
|
||||
)
|
||||
}, [language, model])
|
||||
|
||||
const chatListRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!chatListRef.current)
|
||||
return
|
||||
@ -207,6 +290,7 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
chatListRef.current.scrollTop = chatListRef.current.scrollHeight
|
||||
}, [promptMessageCount, isGenerating])
|
||||
|
||||
const generateStartRef = useRef<number | null>(null)
|
||||
const handleGenerate = useCallback(async () => {
|
||||
const trimmed = inputValue.trim()
|
||||
if (!trimmed || isGenerating)
|
||||
@ -214,18 +298,19 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
if (!flowId || !toolNodeId || !paramKey)
|
||||
return
|
||||
|
||||
const userMessage: ContextGenerateMessage = { role: 'user', content: trimmed }
|
||||
const nextMessages: ContextGenerateMessage[] = [...(promptMessages ?? []), userMessage]
|
||||
const userMessage: ContextGenerateChatMessage = { role: 'user', content: trimmed }
|
||||
const nextMessages: ContextGenerateChatMessage[] = [...(promptMessages ?? []), userMessage]
|
||||
setPromptMessages(nextMessages)
|
||||
setInputValue('')
|
||||
setGeneratingTrue()
|
||||
generateStartRef.current = Date.now()
|
||||
try {
|
||||
const response = await generateContext({
|
||||
workflow_id: flowId,
|
||||
node_id: toolNodeId,
|
||||
parameter_name: paramKey,
|
||||
language: normalizeCodeLanguage(current?.code_language || codeNodeData?.code_language) as 'python3' | 'javascript',
|
||||
prompt_messages: nextMessages,
|
||||
prompt_messages: nextMessages.map(({ role, content }) => ({ role, content })),
|
||||
model_config: {
|
||||
provider: model.provider,
|
||||
name: model.name,
|
||||
@ -241,18 +326,25 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
return
|
||||
}
|
||||
|
||||
const assistantMessage = response.message || t('nodes.tool.contextGenerate.defaultAssistantMessage', { ns: 'workflow' })
|
||||
const assistantEntry: ContextGenerateMessage = { role: 'assistant', content: assistantMessage }
|
||||
const assistantMessage = response.message || defaultAssistantMessage
|
||||
const durationMs = generateStartRef.current ? Date.now() - generateStartRef.current : undefined
|
||||
const assistantEntry: ContextGenerateChatMessage = {
|
||||
role: 'assistant',
|
||||
content: assistantMessage,
|
||||
durationMs,
|
||||
}
|
||||
setPromptMessages([...nextMessages, assistantEntry])
|
||||
addVersion(response)
|
||||
}
|
||||
finally {
|
||||
setGeneratingFalse()
|
||||
generateStartRef.current = null
|
||||
}
|
||||
}, [
|
||||
addVersion,
|
||||
codeNodeData?.code_language,
|
||||
current?.code_language,
|
||||
defaultAssistantMessage,
|
||||
flowId,
|
||||
inputValue,
|
||||
isGenerating,
|
||||
@ -264,12 +356,15 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
setPromptMessages,
|
||||
setGeneratingFalse,
|
||||
setGeneratingTrue,
|
||||
t,
|
||||
toolNodeId,
|
||||
])
|
||||
|
||||
const displayVersion = current || fallbackVersion
|
||||
const displayVersion = isInitView ? null : (current || fallbackVersion)
|
||||
const displayCodeLanguage = normalizeCodeLanguage(displayVersion?.code_language)
|
||||
const codeLanguageLabel = displayCodeLanguage === CodeLanguage.javascript
|
||||
// fixme: do not use i18n to display
|
||||
? t('nodes.tool.contextGenerate.codeLanguage.javascript', { ns: 'workflow' })
|
||||
: t('nodes.tool.contextGenerate.codeLanguage.python3', { ns: 'workflow' })
|
||||
const displayOutputData = useMemo(() => {
|
||||
if (!displayVersion)
|
||||
return {}
|
||||
@ -323,178 +418,440 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
}, [codeNodeId, nodes])
|
||||
|
||||
const rightContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [codePanelHeight, setCodePanelHeight] = useState(360)
|
||||
const rightContainerSize = useSize(rightContainerRef)
|
||||
const [codePanelHeight, setCodePanelHeight] = useState(defaultCodePanelHeight)
|
||||
const draggingRef = useRef(false)
|
||||
const dragStartRef = useRef({ startY: 0, startHeight: 0 })
|
||||
const maxCodePanelHeight = useMemo(() => {
|
||||
const containerHeight = rightContainerSize?.height ?? 0
|
||||
if (!containerHeight)
|
||||
return null
|
||||
return Math.max(minCodeHeight, containerHeight - minOutputHeight - splitHandleHeight)
|
||||
}, [rightContainerSize?.height])
|
||||
const resolvedCodePanelHeight = useMemo(() => {
|
||||
if (!maxCodePanelHeight)
|
||||
return codePanelHeight
|
||||
// Reason: Clamp the panel height so the output area always has space.
|
||||
return Math.min(codePanelHeight, maxCodePanelHeight)
|
||||
}, [codePanelHeight, maxCodePanelHeight])
|
||||
|
||||
const handleResizeStart = useCallback((event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
draggingRef.current = true
|
||||
dragStartRef.current = {
|
||||
startY: event.clientY,
|
||||
startHeight: codePanelHeight,
|
||||
startHeight: resolvedCodePanelHeight,
|
||||
}
|
||||
document.body.style.userSelect = 'none'
|
||||
}, [codePanelHeight])
|
||||
}, [resolvedCodePanelHeight])
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!draggingRef.current)
|
||||
return
|
||||
useEventListener('mousemove', (event) => {
|
||||
if (!draggingRef.current)
|
||||
return
|
||||
|
||||
const containerHeight = rightContainerRef.current?.offsetHeight || 0
|
||||
const maxHeight = Math.max(minCodeHeight, containerHeight - minOutputHeight - splitHandleHeight)
|
||||
const delta = event.clientY - dragStartRef.current.startY
|
||||
const nextHeight = Math.min(Math.max(dragStartRef.current.startHeight + delta, minCodeHeight), maxHeight)
|
||||
setCodePanelHeight(nextHeight)
|
||||
}
|
||||
const containerHeight = rightContainerRef.current?.offsetHeight || 0
|
||||
if (!containerHeight)
|
||||
return
|
||||
const maxHeight = Math.max(minCodeHeight, containerHeight - minOutputHeight - splitHandleHeight)
|
||||
const delta = event.clientY - dragStartRef.current.startY
|
||||
const nextHeight = Math.min(Math.max(dragStartRef.current.startHeight + delta, minCodeHeight), maxHeight)
|
||||
setCodePanelHeight(nextHeight)
|
||||
})
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (draggingRef.current) {
|
||||
draggingRef.current = false
|
||||
document.body.style.userSelect = ''
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
window.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove)
|
||||
window.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}, [])
|
||||
useEventListener('mouseup', () => {
|
||||
if (!draggingRef.current)
|
||||
return
|
||||
draggingRef.current = false
|
||||
document.body.style.userSelect = ''
|
||||
})
|
||||
|
||||
const canRun = !!displayVersion?.code || !!codeNodeData?.code
|
||||
const emptyPanelClassName = cn(
|
||||
'flex h-full flex-col',
|
||||
isInitView
|
||||
? 'rounded-l-xl bg-components-panel-bg pb-1 pl-1'
|
||||
: 'rounded-[10px] bg-components-panel-bg',
|
||||
)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className="min-w-[1140px] !p-0"
|
||||
className={cn(
|
||||
'max-w-[calc(100vw-32px)] border-[0.5px] border-components-panel-border bg-background-body !p-0 shadow-xl shadow-shadow-shadow-5',
|
||||
isInitView ? 'w-[1280px]' : 'w-[1200px]',
|
||||
)}
|
||||
>
|
||||
<div className="relative flex h-[680px] flex-wrap">
|
||||
<div className="flex h-full w-[420px] shrink-0 flex-col border-r border-divider-regular p-6">
|
||||
<div className="mb-4 text-lg font-bold leading-[28px] text-text-primary">
|
||||
{t('nodes.tool.contextGenerate.title', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<ModelParameterModal
|
||||
popupClassName="!w-[520px]"
|
||||
portalToFollowElemContentClassName="z-[1000]"
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex h-[720px] max-h-[calc(100vh-32px)] flex-wrap">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-[400px] shrink-0 flex-col border-r border-divider-regular bg-background-body',
|
||||
isInitView ? 'justify-center pb-20' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={chatListRef}
|
||||
className="flex-1 space-y-2 overflow-y-auto pr-1"
|
||||
className={cn(
|
||||
'bg-gradient-to-b from-background-body to-transparent backdrop-blur-[4px]',
|
||||
isInitView ? 'px-5 py-4' : 'px-4 pb-4 pt-3',
|
||||
)}
|
||||
>
|
||||
{(promptMessages || []).map((message, index) => {
|
||||
const isUser = message.role === 'user'
|
||||
return (
|
||||
<div
|
||||
key={`${message.role}-${index}`}
|
||||
className={cn('flex', isUser ? 'justify-end' : 'justify-start')}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="title-2xl-semi-bold bg-gradient-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text text-transparent">
|
||||
{t('nodes.tool.contextGenerate.title', { ns: 'workflow' })}
|
||||
</div>
|
||||
{isInitView && (
|
||||
<div className="mt-1 text-[13px] italic leading-4 text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.subtitle', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isInitView && (
|
||||
<ActionButton
|
||||
size="m"
|
||||
className={cn('!h-8 !w-8', isGenerating && 'pointer-events-none opacity-50')}
|
||||
onClick={handleReset}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[320px] whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm',
|
||||
isUser
|
||||
? 'bg-background-gradient-bg-fill-chat-bubble-bg-3 text-text-primary'
|
||||
: 'bg-chat-bubble-bg text-text-primary',
|
||||
)}
|
||||
>
|
||||
{message.content}
|
||||
<RiRefreshLine className="h-4 w-4 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isInitView
|
||||
? (
|
||||
<div className="flex w-full flex-col gap-1 px-2">
|
||||
<div className="bg-gradient-to-b from-[rgba(255,255,255,0.01)] to-background-body px-2 pb-2 pt-3">
|
||||
<div className="flex h-[120px] flex-col justify-between overflow-hidden rounded-xl border-[0.5px] border-components-input-border-active bg-components-panel-bg shadow-shadow-shadow-5 backdrop-blur-[5px]">
|
||||
<div className="flex min-h-[64px] px-3 pb-1 pt-2.5">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
handleGenerate()
|
||||
}
|
||||
}}
|
||||
placeholder={t('nodes.tool.contextGenerate.initPlaceholder', { ns: 'workflow' }) as string}
|
||||
className="w-full resize-none bg-transparent text-sm leading-5 text-text-primary placeholder:text-text-quaternary focus:outline-none"
|
||||
disabled={isGenerating}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 p-2">
|
||||
<ModelParameterModal
|
||||
popupClassName="!w-[520px]"
|
||||
portalToFollowElemContentClassName="z-[1000]"
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
renderTrigger={renderModelTrigger}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="!h-8 !w-8 shrink-0 !rounded-lg !px-0"
|
||||
disabled={!inputValue.trim() || isGenerating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
<RiSendPlaneLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-px px-2">
|
||||
<div className="flex items-center px-3 pb-2 pt-4">
|
||||
<SkeletonRectangle className="h-3 w-20" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 px-3">
|
||||
{suggestedSkeletonItems.map(item => (
|
||||
<SkeletonRow key={item} className="py-1">
|
||||
<div className="h-4 w-4 rounded-sm bg-divider-subtle opacity-60" />
|
||||
<SkeletonRectangle className="h-3 w-[260px]" />
|
||||
</SkeletonRow>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{isGenerating && (
|
||||
<div className="flex justify-start">
|
||||
<div className="flex items-center gap-2 rounded-2xl bg-chat-bubble-bg px-4 py-3 text-sm text-text-primary">
|
||||
<LoadingAnim type="text" />
|
||||
<span>{t('nodes.tool.contextGenerate.generating', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter')
|
||||
handleGenerate()
|
||||
}}
|
||||
placeholder={t('nodes.tool.contextGenerate.inputPlaceholder', { ns: 'workflow' }) as string}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="shrink-0 px-3"
|
||||
disabled={!inputValue.trim() || isGenerating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
<RiSendPlaneLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-0 grow flex-col bg-background-default-subtle p-6 pb-0">
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between">
|
||||
<div>
|
||||
<div className="text-base font-semibold leading-[160%] text-text-secondary">
|
||||
{t('nodes.tool.contextGenerate.codeBlock', { ns: 'workflow' })}
|
||||
</div>
|
||||
{versions.length > 0 && (
|
||||
<VersionSelector
|
||||
versionLen={versions.length}
|
||||
value={currentVersionIndex || 0}
|
||||
onChange={setCurrentVersionIndex}
|
||||
/>
|
||||
: (
|
||||
<>
|
||||
<div
|
||||
ref={chatListRef}
|
||||
className="flex-1 overflow-y-auto px-4 py-2"
|
||||
>
|
||||
<div className="flex w-full flex-col items-end gap-4 pt-3">
|
||||
{(() => {
|
||||
let assistantIndex = -1
|
||||
return (promptMessages || []).map((message, index) => {
|
||||
if (message.role === 'assistant')
|
||||
assistantIndex += 1
|
||||
const versionMeta = message.role === 'assistant' ? versionOptions[assistantIndex] : null
|
||||
const isSelected = versionMeta?.index === currentVersionIndexSafe
|
||||
const showThoughtProcess = message.role === 'assistant' && message.content !== defaultAssistantMessage
|
||||
const durationLabel = message.role === 'assistant' && message.durationMs
|
||||
? `${(message.durationMs / 1000).toFixed(1)}s`
|
||||
: null
|
||||
return (
|
||||
<div
|
||||
key={`${message.role}-${index}`}
|
||||
className={cn('flex w-full', message.role === 'user' ? 'justify-end' : 'justify-start')}
|
||||
>
|
||||
{message.role === 'user'
|
||||
? (
|
||||
<div className="max-w-[320px] whitespace-pre-wrap rounded-xl bg-util-colors-blue-brand-blue-brand-500 px-3 py-2 text-sm leading-5 text-text-primary-on-surface">
|
||||
{message.content}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
{showThoughtProcess && (
|
||||
<div className="flex w-full items-center gap-1 rounded-xl bg-background-gradient-bg-fill-chat-bubble-bg-2 px-2 py-2 text-[13px] text-text-secondary">
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<RiSparklingLine className="h-4 w-4 text-text-secondary" />
|
||||
</div>
|
||||
<span className="flex-1 truncate">
|
||||
{message.content}
|
||||
</span>
|
||||
{durationLabel && (
|
||||
<span className="text-xs text-text-tertiary">
|
||||
{durationLabel}
|
||||
</span>
|
||||
)}
|
||||
<RiArrowDownSLine className="h-4 w-4 -rotate-90 text-text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="whitespace-pre-wrap px-2 text-sm leading-5 text-text-primary">
|
||||
{showThoughtProcess ? defaultAssistantMessage : message.content}
|
||||
</div>
|
||||
{versionMeta && (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex min-h-[40px] w-full items-center gap-2 rounded-[12px] border-[0.5px] bg-components-card-bg px-3 py-2 text-left',
|
||||
isSelected
|
||||
? 'border-[1.5px] border-components-option-card-option-selected-border'
|
||||
: 'border-components-panel-border-subtle',
|
||||
)}
|
||||
onClick={() => setCurrentVersionIndex(versionMeta.index)}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-[5px] border-[0.5px] border-divider-subtle bg-util-colors-blue-blue-500 p-[2px] shadow-xs">
|
||||
<CodeAssistant className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="flex-1 text-[13px] font-medium text-text-primary">
|
||||
{versionMeta.label}
|
||||
</span>
|
||||
<RiArrowRightLine className="h-4 w-4 text-text-tertiary" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
{isGenerating && (
|
||||
<div className="flex w-full items-center gap-2 rounded-xl bg-background-gradient-bg-fill-chat-bubble-bg-2 px-2 py-2 text-xs text-text-secondary">
|
||||
<LoadingAnim type="text" />
|
||||
<span>{t('nodes.tool.contextGenerate.generating', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gradient-to-b from-[rgba(255,255,255,0.01)] to-background-body px-1 pb-1 pt-3">
|
||||
<div className="flex min-h-[112px] flex-col justify-between overflow-hidden rounded-xl border-[0.5px] border-components-input-border-active bg-components-panel-bg shadow-shadow-shadow-5 backdrop-blur-[5px]">
|
||||
<div className="flex min-h-[64px] px-3 pb-1 pt-2.5">
|
||||
<textarea
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
handleGenerate()
|
||||
}
|
||||
}}
|
||||
placeholder={t('nodes.tool.contextGenerate.inputPlaceholder', { ns: 'workflow' }) as string}
|
||||
className="w-full resize-none bg-transparent text-sm leading-5 text-text-primary placeholder:text-text-quaternary focus:outline-none"
|
||||
disabled={isGenerating}
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end gap-2 p-2">
|
||||
<ModelParameterModal
|
||||
popupClassName="!w-[520px]"
|
||||
portalToFollowElemContentClassName="z-[1000]"
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
onCompletionParamsChange={handleCompletionParamsChange}
|
||||
hideDebugWithMultipleModel
|
||||
renderTrigger={renderModelTrigger}
|
||||
/>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
className="!h-8 !w-8 shrink-0 !rounded-lg !px-0"
|
||||
disabled={!inputValue.trim() || isGenerating}
|
||||
onClick={handleGenerate}
|
||||
>
|
||||
<RiSendPlaneLine className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full w-0 grow flex-col bg-background-body',
|
||||
isInitView ? 'py-1' : 'pt-1',
|
||||
)}
|
||||
>
|
||||
{isInitView && (
|
||||
<div className="flex h-10 items-center justify-end px-3 py-1">
|
||||
<ActionButton size="m" className="!h-8 !w-8" onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleRun}
|
||||
disabled={!canRun || isGenerating || isRunning}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.run', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => applyToNode(true)}
|
||||
disabled={!current || isGenerating}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.apply', { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
{!isInitView && (
|
||||
<div className="flex shrink-0 items-center justify-between px-3 py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="text-[13px] font-semibold uppercase text-text-secondary">
|
||||
{t('nodes.tool.contextGenerate.generatedCode', { ns: 'workflow' })}
|
||||
</div>
|
||||
<PortalToFollowElem
|
||||
placement="bottom-start"
|
||||
offset={{
|
||||
mainAxis: 6,
|
||||
crossAxis: -4,
|
||||
}}
|
||||
open={isVersionMenuOpen}
|
||||
onOpenChange={handleVersionMenuOpen}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild onClick={handleVersionMenuToggle}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex items-center gap-1 text-xs font-medium text-text-tertiary',
|
||||
versions.length > 1 ? 'cursor-pointer' : 'cursor-default',
|
||||
)}
|
||||
>
|
||||
<span>{currentVersionLabel}</span>
|
||||
{versions.length > 1 && <RiArrowDownSLine className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className="z-[1010]">
|
||||
<div className="w-[208px] rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
<div className="system-xs-medium-uppercase flex h-[22px] items-center px-3 text-text-tertiary">
|
||||
{t('generate.versions', { ns: 'appDebug' })}
|
||||
</div>
|
||||
{versionOptions.map(option => (
|
||||
<button
|
||||
key={option.index}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex h-7 w-full items-center rounded-lg px-2 text-[13px] text-text-secondary',
|
||||
option.index === currentVersionIndexSafe
|
||||
? 'bg-state-base-hover'
|
||||
: 'hover:bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
setCurrentVersionIndex(option.index)
|
||||
setVersionMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<span className="flex-1 truncate text-left">{option.label}</span>
|
||||
{option.index === currentVersionIndexSafe && (
|
||||
<RiCheckLine className="h-4 w-4 text-text-accent" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isRunning
|
||||
? (
|
||||
<div className="flex h-8 items-center gap-2 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-bg px-3 text-xs font-medium text-text-secondary">
|
||||
<span className="h-2 w-2 rounded-full bg-util-colors-blue-blue-500" />
|
||||
{t('nodes.tool.contextGenerate.running', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRun}
|
||||
disabled={!canRun || isGenerating}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.run', { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
onClick={() => applyToNode(true)}
|
||||
disabled={!current || isGenerating}
|
||||
>
|
||||
{t('nodes.tool.contextGenerate.apply', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<div className="mx-1 h-4 w-px bg-divider-regular" />
|
||||
<ActionButton size="m" className="!h-8 !w-8" onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref={rightContainerRef} className="flex h-full flex-col overflow-hidden">
|
||||
)}
|
||||
<div
|
||||
ref={rightContainerRef}
|
||||
className={cn(
|
||||
'flex h-full flex-col overflow-hidden',
|
||||
isInitView ? 'px-0 pb-0' : 'px-3 pb-3',
|
||||
)}
|
||||
>
|
||||
{isGenerating && !displayVersion && (
|
||||
<div className="flex h-full flex-col items-center justify-center space-y-3">
|
||||
<div className={cn(emptyPanelClassName, 'items-center justify-center')}>
|
||||
<Loading />
|
||||
<div className="text-[13px] text-text-tertiary">
|
||||
<div className="mt-3 text-[13px] text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.generating', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isGenerating && !displayVersion && (
|
||||
<ResPlaceholder />
|
||||
<div className={emptyPanelClassName}>
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 pb-20 text-center">
|
||||
<CodeAssistant className="h-8 w-8 text-divider-regular" />
|
||||
<div className="text-xs leading-4 text-text-quaternary">
|
||||
{rightPlaceholderLines.map((line, index) => (
|
||||
<p key={`${line}-${index}`}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{displayVersion && (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div
|
||||
className="flex min-h-[220px] flex-col overflow-hidden rounded-lg border border-components-panel-border bg-components-panel-bg"
|
||||
style={{ height: codePanelHeight }}
|
||||
className="flex min-h-[80px] flex-col overflow-hidden rounded-[10px] bg-components-input-bg-normal"
|
||||
style={{ height: resolvedCodePanelHeight }}
|
||||
>
|
||||
<div className="px-3 pb-1 pt-2 text-xs font-semibold uppercase text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.code', { ns: 'workflow' })}
|
||||
<div className="flex items-center border-b border-divider-subtle px-2 py-1">
|
||||
<div className="flex flex-1 items-center px-1 py-0.5">
|
||||
<span className="text-xs font-semibold uppercase text-text-secondary">
|
||||
{codeLanguageLabel}
|
||||
</span>
|
||||
</div>
|
||||
<CopyFeedbackNew content={displayVersion.code || ''} className="!h-6 !w-6" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3">
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3 pt-2">
|
||||
<CodeEditor
|
||||
noWrapper
|
||||
isExpand
|
||||
@ -507,17 +864,21 @@ const ContextGenerateModal: FC<Props> = ({
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-[6px] w-full cursor-row-resize items-center justify-center bg-transparent p-0"
|
||||
className="flex h-4 w-full cursor-row-resize items-center px-2"
|
||||
aria-label={t('nodes.tool.contextGenerate.resizeHandle', { ns: 'workflow' })}
|
||||
onPointerDown={handleResizeStart}
|
||||
>
|
||||
<div className="h-1 w-8 rounded-full bg-divider-subtle" />
|
||||
<div className="h-[2px] w-full rounded-full bg-divider-subtle" />
|
||||
</button>
|
||||
<div className="flex min-h-[160px] flex-1 flex-col overflow-hidden rounded-lg border border-components-panel-border bg-components-panel-bg">
|
||||
<div className="px-3 pb-1 pt-2 text-xs font-semibold uppercase text-text-tertiary">
|
||||
{t('nodes.tool.contextGenerate.output', { ns: 'workflow' })}
|
||||
<div className="flex min-h-[80px] flex-1 flex-col overflow-hidden rounded-[10px] bg-components-input-bg-normal">
|
||||
<div className="flex items-center border-b border-divider-subtle px-2 py-1">
|
||||
<div className="flex flex-1 items-center px-1 py-0.5">
|
||||
<span className="text-xs font-semibold uppercase text-text-secondary">
|
||||
{t('nodes.tool.contextGenerate.output', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3">
|
||||
<div className="flex-1 overflow-hidden px-3 pb-3 pt-2">
|
||||
<CodeEditor
|
||||
noWrapper
|
||||
isExpand
|
||||
|
||||
@ -26,9 +26,15 @@ const useContextGenData = ({ storageKey }: Params) => {
|
||||
})
|
||||
}, [setCurrentVersionIndex, setVersions, versions?.length])
|
||||
|
||||
const clearVersions = useCallback(() => {
|
||||
setVersions([])
|
||||
setCurrentVersionIndex(0)
|
||||
}, [setCurrentVersionIndex, setVersions])
|
||||
|
||||
return {
|
||||
versions,
|
||||
addVersion,
|
||||
clearVersions,
|
||||
currentVersionIndex,
|
||||
setCurrentVersionIndex,
|
||||
current,
|
||||
|
||||
@ -789,12 +789,20 @@
|
||||
"nodes.tool.contextGenerate.apply": "Apply",
|
||||
"nodes.tool.contextGenerate.code": "Code",
|
||||
"nodes.tool.contextGenerate.codeBlock": "Code Block",
|
||||
"nodes.tool.contextGenerate.codeLanguage.javascript": "JavaScript",
|
||||
"nodes.tool.contextGenerate.codeLanguage.python3": "Python 3",
|
||||
"nodes.tool.contextGenerate.defaultAssistantMessage": "I've finished, please have a check on it.",
|
||||
"nodes.tool.contextGenerate.generatedCode": "Generated Code",
|
||||
"nodes.tool.contextGenerate.generating": "Generating...",
|
||||
"nodes.tool.contextGenerate.initPlaceholder": "Describe how to assemble variables from previous nodes, and press '/' to insert variables.",
|
||||
"nodes.tool.contextGenerate.inputPlaceholder": "Ask for change...",
|
||||
"nodes.tool.contextGenerate.instruction": "Instruction",
|
||||
"nodes.tool.contextGenerate.output": "Output",
|
||||
"nodes.tool.contextGenerate.resizeHandle": "Resize handle",
|
||||
"nodes.tool.contextGenerate.rightSidePlaceholder": "Please enter the instruction on the left.\nThe generated code will appear here.",
|
||||
"nodes.tool.contextGenerate.run": "Run",
|
||||
"nodes.tool.contextGenerate.running": "Running",
|
||||
"nodes.tool.contextGenerate.subtitle": "Assemble multiple variables into one from previous nodes",
|
||||
"nodes.tool.contextGenerate.title": "Assemble Variables",
|
||||
"nodes.tool.inputVars": "Input Variables",
|
||||
"nodes.tool.insertPlaceholder1": "Type or press",
|
||||
@ -1035,7 +1043,6 @@
|
||||
"skillSidebar.loadError": "Failed to load files",
|
||||
"skillSidebar.menu.cannotMoveToDescendant": "Cannot move a folder into its descendant",
|
||||
"skillSidebar.menu.cannotMoveToSelf": "Cannot move a folder into itself",
|
||||
"skillSidebar.menu.cannotMoveToSelf": "Cannot move a folder into itself",
|
||||
"skillSidebar.menu.copy": "Copy",
|
||||
"skillSidebar.menu.copyNotSupported": "Copy is not supported yet",
|
||||
"skillSidebar.menu.createError": "Failed to create item",
|
||||
|
||||
@ -775,6 +775,7 @@
|
||||
"nodes.tool.contextGenerate.inputPlaceholder": "変更を依頼...",
|
||||
"nodes.tool.contextGenerate.output": "出力",
|
||||
"nodes.tool.contextGenerate.resizeHandle": "サイズ調整ハンドル",
|
||||
"nodes.tool.contextGenerate.rightSidePlaceholder": "左側に指示を入力してください。生成されたコードがここに表示されます。",
|
||||
"nodes.tool.contextGenerate.run": "実行",
|
||||
"nodes.tool.contextGenerate.title": "変数を組み立てる",
|
||||
"nodes.tool.inputVars": "入力変数",
|
||||
|
||||
@ -1017,7 +1017,6 @@
|
||||
"skillSidebar.loadError": "加载文件失败",
|
||||
"skillSidebar.menu.cannotMoveToDescendant": "无法将文件夹移动到其子文件夹中",
|
||||
"skillSidebar.menu.cannotMoveToSelf": "无法将文件夹移动到自身内部",
|
||||
"skillSidebar.menu.cannotMoveToSelf": "无法将文件夹移动到自身内部",
|
||||
"skillSidebar.menu.copy": "复制",
|
||||
"skillSidebar.menu.copyNotSupported": "暂不支持复制功能",
|
||||
"skillSidebar.menu.createError": "创建失败",
|
||||
|
||||
@ -773,8 +773,10 @@
|
||||
"nodes.tool.contextGenerate.defaultAssistantMessage": "我已完成,請查看。",
|
||||
"nodes.tool.contextGenerate.generating": "生成中...",
|
||||
"nodes.tool.contextGenerate.inputPlaceholder": "請求修改...",
|
||||
"nodes.tool.contextGenerate.instruction": "指令",
|
||||
"nodes.tool.contextGenerate.output": "輸出",
|
||||
"nodes.tool.contextGenerate.resizeHandle": "調整大小把手",
|
||||
"nodes.tool.contextGenerate.rightSidePlaceholder": "請在左側輸入指令。生成的程式碼將顯示在這裡。",
|
||||
"nodes.tool.contextGenerate.run": "執行",
|
||||
"nodes.tool.contextGenerate.title": "組裝變數",
|
||||
"nodes.tool.inputVars": "輸入變數",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user