diff --git a/web/app/components/app/configuration/config/automatic/version-selector.tsx b/web/app/components/app/configuration/config/automatic/version-selector.tsx index 91fb3950d2..8463488a8c 100644 --- a/web/app/components/app/configuration/config/automatic/version-selector.tsx +++ b/web/app/components/app/configuration/config/automatic/version-selector.tsx @@ -51,15 +51,19 @@ const VersionSelector: React.FC = ({ versionLen, value, on onClick={handleToggle} asChild > - -
+
{t('generate.version', { ns: 'appDebug' })} {' '} {value + 1} {isLatest && ` · ${t('generate.latest', { ns: 'appDebug' })}`}
- {moreThanOneVersion && } + {moreThanOneVersion && }
= ({ 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 = ({ current, currentVersionIndex, setCurrentVersionIndex, + clearVersions, } = useContextGenData({ storageKey, }) - const [promptMessages, setPromptMessages] = useSessionStorageState( + const [promptMessages, setPromptMessages] = useSessionStorageState( `${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(() => { @@ -197,8 +209,79 @@ const ContextGenerateModal: FC = ({ localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) }, [model]) - const chatListRef = useRef(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 ( +
+ + + {label} + + +
+ ) + }, [language, model]) + + const chatListRef = useRef(null) useEffect(() => { if (!chatListRef.current) return @@ -207,6 +290,7 @@ const ContextGenerateModal: FC = ({ chatListRef.current.scrollTop = chatListRef.current.scrollHeight }, [promptMessageCount, isGenerating]) + const generateStartRef = useRef(null) const handleGenerate = useCallback(async () => { const trimmed = inputValue.trim() if (!trimmed || isGenerating) @@ -214,18 +298,19 @@ const ContextGenerateModal: FC = ({ 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 = ({ 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 = ({ 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 = ({ }, [codeNodeId, nodes]) const rightContainerRef = useRef(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) => { 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 ( -
-
-
- {t('nodes.tool.contextGenerate.title', { ns: 'workflow' })} -
-
- -
+
+
- {(promptMessages || []).map((message, index) => { - const isUser = message.role === 'user' - return ( -
+
+
+ {t('nodes.tool.contextGenerate.title', { ns: 'workflow' })} +
+ {isInitView && ( +
+ {t('nodes.tool.contextGenerate.subtitle', { ns: 'workflow' })} +
+ )} +
+ {!isInitView && ( + -
- {message.content} + + + )} +
+
+ + {isInitView + ? ( +
+
+
+
+