Merge pull request #798 from SuanmoSuanyangTechnology/feature/ui_upgrade_zy
Feature/UI upgrade zy
This commit is contained in:
@@ -91,10 +91,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '3',
|
key: '3',
|
||||||
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/userInfo.svg')]"></div>,
|
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/userInfo.svg')]"></div>,
|
||||||
label: <Flex justify="space-between" align="center">
|
label: <Flex justify="space-between" align="center">
|
||||||
{t('header.userInfo')}
|
{t('header.userInfo')}
|
||||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/arrow_t_r.svg')]"></div>
|
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/arrow_t_r.svg')]"></div>
|
||||||
</Flex>,
|
</Flex>,
|
||||||
className: 'rb:text-[#212332]!',
|
className: 'rb:text-[#212332]!',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -103,10 +103,10 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '4',
|
key: '4',
|
||||||
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/settings.svg')]"></div>,
|
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/settings.svg')]"></div>,
|
||||||
label: <Flex justify="space-between" align="center">
|
label: <Flex justify="space-between" align="center">
|
||||||
{t('header.settings')}
|
{t('header.settings')}
|
||||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/arrow_t_r.svg')]"></div>
|
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/arrow_t_r.svg')]"></div>
|
||||||
</Flex>,
|
</Flex>,
|
||||||
className: 'rb:text-[#212332]!',
|
className: 'rb:text-[#212332]!',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -120,7 +120,7 @@ const AppHeader: FC<{source?: 'space' | 'manage';}> = ({source = 'manage'}) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '6',
|
key: '6',
|
||||||
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/menuNew/logout_red.svg')]"></div>,
|
icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/menuNew/logout_red.svg')]"></div>,
|
||||||
label: t('header.logout'),
|
label: t('header.logout'),
|
||||||
danger: true,
|
danger: true,
|
||||||
className: 'rb:hover:rb:bg-transparent rb:hover:text-[#FF5D34]!',
|
className: 'rb:hover:rb:bg-transparent rb:hover:text-[#FF5D34]!',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:15:11
|
* @Date: 2026-02-02 15:15:11
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-02 15:15:11
|
* @Last Modified time: 2026-04-07 14:04:33
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* CodeBlock Component
|
* CodeBlock Component
|
||||||
@@ -27,6 +27,7 @@ type ICodeBlockProps = {
|
|||||||
needCopy?: boolean;
|
needCopy?: boolean;
|
||||||
size?: 'small' | 'default';
|
size?: 'small' | 'default';
|
||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
|
background?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Code block component for displaying formatted code with optional copy functionality */
|
/** Code block component for displaying formatted code with optional copy functionality */
|
||||||
@@ -34,7 +35,8 @@ const CodeBlock: FC<ICodeBlockProps> = ({
|
|||||||
value,
|
value,
|
||||||
needCopy = true,
|
needCopy = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
showLineNumbers = false
|
showLineNumbers = false,
|
||||||
|
background = '#F0F3F8'
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +45,7 @@ const CodeBlock: FC<ICodeBlockProps> = ({
|
|||||||
style={atelierHeathLight}
|
style={atelierHeathLight}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
padding: '8px 12px 8px 12px',
|
padding: '8px 12px 8px 12px',
|
||||||
backgroundColor: '#F0F3F8',
|
backgroundColor: background,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
fontSize: size === 'small' ? 12 : 14,
|
fontSize: size === 'small' ? 12 : 14,
|
||||||
wordBreak: 'break-all'
|
wordBreak: 'break-all'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:17:31
|
* @Date: 2026-02-02 15:17:31
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 16:06:03
|
* @Last Modified time: 2026-04-07 15:14:02
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* RbMarkdown Component
|
* RbMarkdown Component
|
||||||
@@ -97,10 +97,6 @@ const buildComponents = (onFormSubmit?: (values: Record<string, any>) => void) =
|
|||||||
const ctx = useContext(FormContext)
|
const ctx = useContext(FormContext)
|
||||||
return <RbButton {...props} onClick={() => ctx?.onSubmit?.(ctx.values)}>{[children]}</RbButton>
|
return <RbButton {...props} onClick={() => ctx?.onSubmit?.(ctx.values)}>{[children]}</RbButton>
|
||||||
},
|
},
|
||||||
table: ({ children, ...props }: any) => <div className="rb:overflow-x-auto rb:max-w-full"><table className="rb:border rb:border-[#D9D9D9] rb:mb-2" {...props}>{children}</table></div>,
|
|
||||||
tr: ({ children, ...props }: any) => <tr className="rb:border rb:border-[#D9D9D9]" {...props}>{children}</tr>,
|
|
||||||
th: ({ children, ...props }: any) => <th className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left rb:font-bold" {...props}>{children}</th>,
|
|
||||||
td: ({ children, ...props }: any) => <td className="rb:border rb:border-[#D9D9D9] rb:px-2 rb:py-1 rb:text-left" {...props}>{children}</td>,
|
|
||||||
input: ({ children, ...props }: any) => {
|
input: ({ children, ...props }: any) => {
|
||||||
const ctx = useContext(FormContext)
|
const ctx = useContext(FormContext)
|
||||||
const handleChange = useCallback((val: any) => {
|
const handleChange = useCallback((val: any) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:27:52
|
* @Date: 2026-02-03 16:27:52
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-27 19:07:24
|
* @Last Modified time: 2026-04-07 16:28:33
|
||||||
*/
|
*/
|
||||||
import { type FC, useRef, useMemo, useCallback } from 'react';
|
import { type FC, useRef, useMemo, useCallback } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
@@ -18,7 +18,6 @@ import type { CopyModalRef, AgentRef, ClusterRef, WorkflowRef, FeaturesConfigFor
|
|||||||
import { deleteApplication, appExport } from '@/api/application'
|
import { deleteApplication, appExport } from '@/api/application'
|
||||||
import CopyModal from './CopyModal'
|
import CopyModal from './CopyModal'
|
||||||
import PageHeader from '@/components/Layout/PageHeader'
|
import PageHeader from '@/components/Layout/PageHeader'
|
||||||
import FeaturesConfig from './FeaturesConfig'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab keys for application configuration
|
* Tab keys for application configuration
|
||||||
@@ -70,7 +69,6 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
application, activeTab, handleChangeTab, refresh,
|
application, activeTab, handleChangeTab, refresh,
|
||||||
workflowRef,
|
workflowRef,
|
||||||
appRef,
|
appRef,
|
||||||
features,
|
|
||||||
onFeaturesChange,
|
onFeaturesChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -175,10 +173,9 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
return items
|
return items
|
||||||
}, [t, handleClick, application])
|
}, [t, handleClick, application])
|
||||||
|
|
||||||
const handleSaveFeaturesConfig = useCallback((value: FeaturesConfigForm) => {
|
const handleFeaturesConfig = () => {
|
||||||
appRef?.current?.handleSaveFeaturesConfig?.(value)
|
workflowRef.current?.handleFeaturesConfig?.()
|
||||||
onFeaturesChange?.(value)
|
}
|
||||||
}, [appRef, onFeaturesChange])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -209,12 +206,12 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({
|
|||||||
</Flex>}
|
</Flex>}
|
||||||
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
|
extra={application?.type === 'workflow' && source !== 'sharing' && activeTab === 'arrangement'
|
||||||
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
|
? <Flex align="center" justify="end" gap={10} className="rb:h-8">
|
||||||
<FeaturesConfig
|
<Popover content={t('application.features')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
||||||
source={application?.type}
|
<div
|
||||||
value={features as FeaturesConfigForm}
|
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/features.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
||||||
refresh={handleSaveFeaturesConfig}
|
onClick={handleFeaturesConfig}
|
||||||
chatVariables={(workflowRef.current?.chatVariables || []).map(v => ({ ...v, display_name: v.name }))}
|
></div>
|
||||||
/>
|
</Popover>
|
||||||
<Popover content={t('workflow.clear')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
<Popover content={t('workflow.clear')} classNames={{ body: 'rb:py-0.5! rb:px-1! rb:rounded-[6px]! rb:text-[12px]!' }}>
|
||||||
<div
|
<div
|
||||||
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/clear.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
className="rb:cursor-pointer rb:size-7.5 rb:border rb:border-[#EBEBEB] rb:hover:bg-[#F6F6F6] rb:rounded-[10px] rb:bg-[url('@/assets/images/workflow/clear.svg')] rb:bg-size-[16px_16px] rb:bg-center rb:bg-no-repeat"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:27:56
|
* @Date: 2026-02-03 16:27:56
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 17:49:51
|
* @Last Modified time: 2026-04-07 16:13:44
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Copy Application Modal
|
* Copy Application Modal
|
||||||
@@ -205,6 +205,7 @@ const FeaturesConfigModal = forwardRef<FeaturesConfigModalRef, FeaturesConfigMod
|
|||||||
/>
|
/>
|
||||||
<OpenStatementSettingModal
|
<OpenStatementSettingModal
|
||||||
ref={openStatementSettingModalRef}
|
ref={openStatementSettingModalRef}
|
||||||
|
source={source}
|
||||||
chatVariables={chatVariables}
|
chatVariables={chatVariables}
|
||||||
onSave={handleSaveStatement}
|
onSave={handleSaveStatement}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-03-05
|
* @Date: 2026-03-05
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-27 14:38:28
|
* @Last Modified time: 2026-04-07 16:58:10
|
||||||
*/
|
*/
|
||||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import { Button, Form, Input, Flex, App } from 'antd';
|
import { Button, Form, Input, Flex, App } from 'antd';
|
||||||
@@ -12,6 +12,8 @@ import RbModal from '@/components/RbModal';
|
|||||||
import type { FeaturesConfigForm } from '../../types'
|
import type { FeaturesConfigForm } from '../../types'
|
||||||
import type { Variable } from '../VariableList/types'
|
import type { Variable } from '../VariableList/types'
|
||||||
import Tag from '@/components/Tag'
|
import Tag from '@/components/Tag'
|
||||||
|
import type { Application } from '@/views/ApplicationManagement/types';
|
||||||
|
import Editor from '@/views/Workflow/components/Editor';
|
||||||
|
|
||||||
export interface OpenStatementSettingModalRef {
|
export interface OpenStatementSettingModalRef {
|
||||||
handleOpen: (values?: FeaturesConfigForm['opening_statement']) => void;
|
handleOpen: (values?: FeaturesConfigForm['opening_statement']) => void;
|
||||||
@@ -21,17 +23,21 @@ export interface OpenStatementSettingModalRef {
|
|||||||
interface OpenStatementSettingModalProps {
|
interface OpenStatementSettingModalProps {
|
||||||
onSave: (values: FeaturesConfigForm['opening_statement']) => void;
|
onSave: (values: FeaturesConfigForm['opening_statement']) => void;
|
||||||
chatVariables?: Variable[];
|
chatVariables?: Variable[];
|
||||||
|
source?: Application['type'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenStatementSettingModalProps>(({
|
const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenStatementSettingModalProps>(({
|
||||||
onSave,
|
onSave,
|
||||||
chatVariables = []
|
chatVariables = [],
|
||||||
|
source
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { modal } = App.useApp()
|
const { modal } = App.useApp()
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [form] = Form.useForm<FeaturesConfigForm['opening_statement']>();
|
const [form] = Form.useForm<FeaturesConfigForm['opening_statement']>();
|
||||||
|
|
||||||
|
console.log('chatVariables', chatVariables)
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
@@ -45,10 +51,11 @@ const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenS
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
form.validateFields().then(values => {
|
form.validateFields().then(values => {
|
||||||
const { suggested_questions, ...rest } = values
|
const { suggested_questions, ...rest } = values
|
||||||
const filterSuggestedQuestions = suggested_questions.filter(vo => vo && vo.trim() !== '' && vo !== null)
|
const filterSuggestedQuestions = suggested_questions?.filter(vo => vo && vo.trim() !== '' && vo !== null)
|
||||||
if (values?.enabled && values?.statement && values?.statement?.trim() !== '') {
|
if (values?.enabled && values?.statement && values?.statement?.trim() !== '') {
|
||||||
const usedVars = [...new Set([...values.statement?.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]))]
|
const usedVars = [...new Set([...values.statement?.matchAll(/\{\{(\w+)\}\}/g)].map(m => m[1]))]
|
||||||
|
|
||||||
|
console.log('usedVars', usedVars, chatVariables)
|
||||||
const validNames = new Set(chatVariables.map(v => v.name))
|
const validNames = new Set(chatVariables.map(v => v.name))
|
||||||
const invalid = usedVars.filter(v => !validNames.has(v))
|
const invalid = usedVars.filter(v => !validNames.has(v))
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
@@ -100,9 +107,12 @@ const OpenStatementSettingModal = forwardRef<OpenStatementSettingModalRef, OpenS
|
|||||||
label={t('application.opening_statement')}
|
label={t('application.opening_statement')}
|
||||||
name="statement"
|
name="statement"
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
{source === 'workflow'
|
||||||
placeholder={t('common.pleaseEnter')}
|
? <Editor options={chatVariables as any} variant="outlined" />
|
||||||
/>
|
: <Input.TextArea
|
||||||
|
placeholder={t('common.pleaseEnter')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.List name="suggested_questions">
|
<Form.List name="suggested_questions">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 16:29:49
|
* @Date: 2026-02-03 16:29:49
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-31 15:45:17
|
* @Last Modified time: 2026-04-07 15:46:19
|
||||||
*/
|
*/
|
||||||
import type { KnowledgeConfig } from './components/Knowledge/types'
|
import type { KnowledgeConfig } from './components/Knowledge/types'
|
||||||
import type { Variable } from './components/VariableList/types'
|
import type { Variable } from './components/VariableList/types'
|
||||||
@@ -168,6 +168,7 @@ export interface WorkflowRef {
|
|||||||
chatVariables: ChatVariable[];
|
chatVariables: ChatVariable[];
|
||||||
config: WorkflowConfig | null;
|
config: WorkflowConfig | null;
|
||||||
features: WorkflowConfig['features'];
|
features: WorkflowConfig['features'];
|
||||||
|
handleFeaturesConfig?: () => void;
|
||||||
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
|
handleSaveFeaturesConfig?: (value: FeaturesConfigForm) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ const ConversationMemory: FC = () => {
|
|||||||
gap={12}
|
gap={12}
|
||||||
>
|
>
|
||||||
<div className={clsx("rb:size-8 rb:bg-cover", {
|
<div className={clsx("rb:size-8 rb:bg-cover", {
|
||||||
'rb:bg-[url(src/assets/images/conversation/user.png)]': item.role === 'user',
|
'rb:bg-[url(@/assets/images/conversation/user.png)]': item.role === 'user',
|
||||||
'rb:bg-[url(src/assets/images/conversation/ai.png)]': item.role === 'assistant',
|
'rb:bg-[url(@/assets/images/conversation/ai.png)]': item.role === 'assistant',
|
||||||
})}></div>
|
})}></div>
|
||||||
<div
|
<div
|
||||||
className="rb:flex-1"
|
className="rb:flex-1"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-06 21:10:56
|
* @Date: 2026-02-06 21:10:56
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 18:01:09
|
* @Last Modified time: 2026-04-07 17:06:02
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Workflow Chat Component
|
* Workflow Chat Component
|
||||||
@@ -40,6 +40,7 @@ import ChatToolbar from '@/components/Chat/ChatToolbar'
|
|||||||
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
import type { ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
||||||
import Runtime from './Runtime';
|
import Runtime from './Runtime';
|
||||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
||||||
|
import { replaceVariables } from '@/views/ApplicationConfig/Agent';
|
||||||
|
|
||||||
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({
|
const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: WorkflowConfig | null; features?: FeaturesConfigForm }>(({
|
||||||
appId, graphRef, features
|
appId, graphRef, features
|
||||||
@@ -419,6 +420,22 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef; data: Work
|
|||||||
handleClose
|
handleClose
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const opening_statement = features?.opening_statement
|
||||||
|
|
||||||
|
if (opening_statement?.enabled && opening_statement?.statement && opening_statement?.statement.trim() !== '') {
|
||||||
|
const assistantMsg: ChatItem = {
|
||||||
|
role: 'assistant',
|
||||||
|
content: replaceVariables(opening_statement.statement, variables as any),
|
||||||
|
meta_data: {
|
||||||
|
suggested_questions: opening_statement?.suggested_questions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('variables', assistantMsg)
|
||||||
|
setChatList(prev => [assistantMsg, ...prev.slice(1)])
|
||||||
|
}
|
||||||
|
}, [chatList.length, features?.opening_statement, variables])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbDrawer
|
<RbDrawer
|
||||||
title={<Flex align="center" gap={10}>
|
title={<Flex align="center" gap={10}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-24 17:57:08
|
* @Date: 2026-02-24 17:57:08
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-12 13:39:24
|
* @Last Modified time: 2026-04-07 14:05:50
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* Runtime Component
|
* Runtime Component
|
||||||
@@ -18,13 +18,15 @@
|
|||||||
import { type FC, useState } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Space, Button, Collapse, Flex } from 'antd'
|
import { App, Button, Collapse, Flex } from 'antd'
|
||||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
import styles from './chat.module.css'
|
import styles from './chat.module.css'
|
||||||
import type { ChatItem } from '@/components/Chat/types'
|
import type { ChatItem } from '@/components/Chat/types'
|
||||||
import Markdown from '@/components/Markdown'
|
import Markdown from '@/components/Markdown'
|
||||||
import CodeBlock from '@/components/Markdown/CodeBlock'
|
import CodeBlock from '@/components/Markdown/CodeBlock'
|
||||||
|
import RbAlert from '@/components/RbAlert'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime component props
|
* Runtime component props
|
||||||
@@ -36,10 +38,12 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
index
|
index
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { message } = App.useApp()
|
||||||
// Stores the currently selected detail view (for nested loop/iteration exploration)
|
// Stores the currently selected detail view (for nested loop/iteration exploration)
|
||||||
const [detail, setDetail] = useState<any>(null)
|
const [detail, setDetail] = useState<any>(null)
|
||||||
// Tracks whether the current detail view is for a loop (true) or iteration (false)
|
// Tracks whether the current detail view is for a loop (true) or iteration (false)
|
||||||
const [loop, setLoop] = useState<boolean | null>(null)
|
const [loop, setLoop] = useState<boolean | null>(null)
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles navigation into nested loop/iteration details
|
* Handles navigation into nested loop/iteration details
|
||||||
@@ -57,7 +61,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
* @returns Tailwind CSS class for appropriate color
|
* @returns Tailwind CSS class for appropriate color
|
||||||
*/
|
*/
|
||||||
const getStatus = (status?: string) => {
|
const getStatus = (status?: string) => {
|
||||||
return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]'
|
return status === 'completed' ? 'rb:text-[#369F21]!' : status === 'failed' ? 'rb:text-[#FF5D34]!' : 'rb:text-[#5B6167]!'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +80,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
<Flex gap={8} vertical>
|
||||||
{Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => {
|
{Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => {
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
@@ -92,7 +96,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +107,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
*/
|
*/
|
||||||
const renderChild = (list: any) => {
|
const renderChild = (list: any) => {
|
||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
return <Space size={8} direction="vertical" className="rb:w-full!">
|
return <Flex gap={8} vertical>
|
||||||
{list?.map(vo => {
|
{list?.map(vo => {
|
||||||
const isLoop = vo.node_type === 'loop';
|
const isLoop = vo.node_type === 'loop';
|
||||||
// Render cycle variables for loop nodes without node_name
|
// Render cycle variables for loop nodes without node_name
|
||||||
@@ -114,6 +118,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
size="small"
|
size="small"
|
||||||
|
onClick={() => handleCopy(typeof vo.content === 'object' && vo.content?.input ? JSON.stringify(vo.content.input, null, 2) : '{}')}
|
||||||
>{t('common.copy')}</Button>
|
>{t('common.copy')}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rb:max-h-40 rb:overflow-auto">
|
<div className="rb:max-h-40 rb:overflow-auto">
|
||||||
@@ -133,35 +138,44 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
key={vo.node_id}
|
key={vo.node_id}
|
||||||
|
bordered={false}
|
||||||
|
className="rb:bg-[#F6F6F6]"
|
||||||
items={[{
|
items={[{
|
||||||
key: vo.node_id,
|
key: vo.node_id,
|
||||||
label: <div className={clsx("rb:flex rb:justify-between rb:items-center", getStatus(vo.status))}>
|
label: <div className={clsx("rb:flex rb:justify-between rb:items-center")}>
|
||||||
<div className="rb:flex rb:items-center rb:gap-1 rb:flex-1">
|
<Flex gap={6} align="center" className="rb:flex-1!">
|
||||||
{vo.icon && <div className={`rb:size-4 rb:bg-cover ${vo.icon}`} />}
|
{vo.icon && <div className={`rb:size-6 rb:bg-cover ${vo.icon}`} />}
|
||||||
<div className="rb:wrap-break-word rb:line-clamp-1">{vo.node_name}</div>
|
<div className="rb:wrap-break-word rb:line-clamp-1 rb:font-medium">{vo.node_name}</div>
|
||||||
</div>
|
</Flex>
|
||||||
<span>
|
<Flex align="center" gap={8} className="rb:text-[12px]">
|
||||||
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
||||||
{vo.status === 'completed' ? <CheckCircleFilled className="rb:ml-1" /> : vo.status === 'failed' ? <CloseCircleFilled className="rb:ml-1" /> : <LoadingOutlined className="rb:ml-1" />}
|
{vo.status === 'completed'
|
||||||
</span>
|
? <CheckCircleFilled className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
: vo.status === 'failed'
|
||||||
|
? <CloseCircleFilled className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
: <LoadingOutlined className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
</div>,
|
</div>,
|
||||||
className: styles.collapseItem,
|
className: styles.collapseItem,
|
||||||
children: (
|
children: (
|
||||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
<Flex gap={8} vertical>
|
||||||
{/* Display error message for failed nodes */}
|
{/* Display error message for failed nodes */}
|
||||||
{vo.status === 'failed' &&
|
|
||||||
<div className={clsx("rb:bg-[#F0F3F8] rb:rounded-md", getStatus(vo.status))}>
|
{item.error &&
|
||||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
<RbAlert color="orange" className="rb:pb-0!">
|
||||||
{t(`workflow.error`)}
|
<Flex vertical className="rb:w-full!">
|
||||||
<Button
|
<Flex align="center" justify="space-between">
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
{t(`workflow.error`)}
|
||||||
size="small"
|
<Button
|
||||||
>{t('common.copy')}</Button>
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
</div>
|
size="small"
|
||||||
<div className="rb:pb-2 rb:px-3 rb:max-h-40 rb:overflow-auto">
|
onClick={() => handleCopy(vo.content?.error || '')}
|
||||||
|
>{t('common.copy')}</Button>
|
||||||
|
</Flex>
|
||||||
<Markdown content={vo.content?.error || ''} />
|
<Markdown content={vo.content?.error || ''} />
|
||||||
</div>
|
</Flex>
|
||||||
</div>
|
</RbAlert>
|
||||||
}
|
}
|
||||||
{/* Display navigation to nested cycles if subContent exists */}
|
{/* Display navigation to nested cycles if subContent exists */}
|
||||||
{vo.subContent?.length > 0 && (
|
{vo.subContent?.length > 0 && (
|
||||||
@@ -172,12 +186,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
)}
|
)}
|
||||||
{/* Display input and output data as JSON code blocks */}
|
{/* Display input and output data as JSON code blocks */}
|
||||||
{['input', 'output'].map(key => (
|
{['input', 'output'].map(key => (
|
||||||
<div key={key} className="rb:bg-[#F0F3F8] rb:rounded-md">
|
<div key={key} className="rb:bg-[#EBEBEB] rb:rounded-lg">
|
||||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||||
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}`)}
|
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}`)}
|
||||||
<Button
|
<Button
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
size="small"
|
size="small"
|
||||||
|
onClick={() => handleCopy(typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}')}
|
||||||
>{t('common.copy')}</Button>
|
>{t('common.copy')}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rb:max-h-40 rb:overflow-auto">
|
<div className="rb:max-h-40 rb:overflow-auto">
|
||||||
@@ -186,55 +201,80 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
||||||
needCopy={false}
|
needCopy={false}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
|
background="#EBEBEB"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Flex>
|
||||||
)
|
)
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Flex>
|
||||||
}
|
}
|
||||||
return <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
return <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
||||||
<Markdown content={list || ''} />
|
<Markdown content={list || ''} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Copy value to clipboard and show success message */
|
||||||
|
const handleCopy = (value: string) => {
|
||||||
|
copy(value)
|
||||||
|
message.success(t('common.copySuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="rb:min-w-100 rb:max-w-full rb:mb-2">
|
<div
|
||||||
<Collapse
|
key={index}
|
||||||
className={styles[item.status || 'default']}
|
className={clsx("rb:mb-4 rb-border rb:rounded-xl rb:px-4 rb:pt-3 rb:bg-white rb:max-w-full", {
|
||||||
items={[{
|
'rb:hover:bg-[#F6F6F6] rb:w-64': !expanded
|
||||||
key: 0,
|
})}
|
||||||
label: <div className={getStatus(item.status)}>
|
>
|
||||||
{item.status === 'completed' ? <CheckCircleFilled className="rb:mr-1" /> : item.status === 'failed' ? <CloseCircleFilled className="rb:mr-1" /> : <LoadingOutlined className="rb:mr-1" />}
|
<Flex align="center" justify="space-between" className="rb:font-medium rb:pb-3!">
|
||||||
{t('application.workflow')}
|
<span className="rb:font-medium rb:leading-5">
|
||||||
</div>,
|
{item.status === 'completed'
|
||||||
className: styles.collapseItem,
|
? <CheckCircleFilled className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
children: (
|
: item.status === 'failed'
|
||||||
detail
|
? <CloseCircleFilled className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
? (
|
: <LoadingOutlined className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
<div className="rb:bg-[#FBFDFF] rb:rounded-md">
|
}
|
||||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
{t('application.workflow')}
|
||||||
{t('common.return')}
|
</span>
|
||||||
</Button>
|
<Flex
|
||||||
{renderDetailChild(detail.subContent)}
|
align="center"
|
||||||
</div>
|
justify="center"
|
||||||
)
|
className={clsx("rb:size-6.5 rb:cursor-pointer rb-border rb:rounded-lg", {
|
||||||
: <>
|
'rb:hover:bg-[#F6F6F6]!': expanded
|
||||||
{item.error &&
|
})}
|
||||||
<div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mb-2 rb:-mt-4", getStatus('failed'))}>
|
onClick={() => { setExpanded(v => !v); setDetail(null) }}
|
||||||
<Markdown content={item.error} />
|
>
|
||||||
</div>
|
<div
|
||||||
}
|
className={clsx("rb:size-4 rb:bg-cover", {
|
||||||
{renderChild(item.subContent)}
|
'rb:bg-[url("@/assets/images/conversation/compress.svg")]': expanded,
|
||||||
</>
|
'rb:bg-[url("@/assets/images/conversation/expand.svg")]': !expanded
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{expanded && (
|
||||||
|
detail
|
||||||
|
? (
|
||||||
|
<div className="rb:bg-[#FBFDFF] rb:rounded-md rb:mb-4">
|
||||||
|
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
||||||
|
{t('common.return')}
|
||||||
|
</Button>
|
||||||
|
{renderDetailChild(detail.subContent)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}]}
|
: <div className="rb:mb-4">
|
||||||
/>
|
{item.error &&
|
||||||
|
<RbAlert color="orange" className="rb:pb-0! rb:mb-2!"><Markdown content={item.error} /></RbAlert>
|
||||||
|
}
|
||||||
|
{renderChild(item.subContent)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
||||||
padding: 8px 12px;
|
padding: 8px 16px 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -45,4 +47,7 @@
|
|||||||
}
|
}
|
||||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||||
padding: 0 4px 4px 4px;
|
padding: 0 4px 4px 4px;
|
||||||
|
}
|
||||||
|
:global(.ant-collapse-borderless)>.collapse-item:global(.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box) {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-04-02 15:15:36
|
* @Date: 2026-04-02 15:15:36
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 15:15:36
|
* @Last Modified time: 2026-04-07 14:48:00
|
||||||
*/
|
*/
|
||||||
import { type FC, useEffect, useMemo } from 'react';
|
import { type FC, useEffect, useMemo } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
@@ -12,7 +12,7 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
||||||
|
|
||||||
import { type Suggestion } from './plugin/AutocompletePlugin';
|
import { type Suggestion } from './plugin/AutocompletePlugin';
|
||||||
import CharacterCountPlugin from './plugin/CharacterCountPlugin';
|
import Jinjia2CharacterCountPlugin from './plugin/Jinjia2CharacterCountPlugin';
|
||||||
import Jinja2InitialValuePlugin from './plugin/Jinja2InitialValuePlugin';
|
import Jinja2InitialValuePlugin from './plugin/Jinja2InitialValuePlugin';
|
||||||
import Jinja2AutocompletePlugin from './plugin/Jinja2AutocompletePlugin';
|
import Jinja2AutocompletePlugin from './plugin/Jinja2AutocompletePlugin';
|
||||||
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
import Jinja2HighlightPlugin from './plugin/Jinja2HighlightPlugin';
|
||||||
@@ -89,7 +89,7 @@ export interface Jinja2EditorProps {
|
|||||||
|
|
||||||
const Jinja2Editor: FC<Jinja2EditorProps> = ({
|
const Jinja2Editor: FC<Jinja2EditorProps> = ({
|
||||||
placeholder = '请输入内容...',
|
placeholder = '请输入内容...',
|
||||||
value = '',
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
options = [],
|
options = [],
|
||||||
variant = 'borderless',
|
variant = 'borderless',
|
||||||
@@ -174,8 +174,8 @@ const Jinja2Editor: FC<Jinja2EditorProps> = ({
|
|||||||
<Jinja2HighlightPlugin />
|
<Jinja2HighlightPlugin />
|
||||||
<LineNumberPlugin />
|
<LineNumberPlugin />
|
||||||
<Jinja2AutocompletePlugin options={options} />
|
<Jinja2AutocompletePlugin options={options} />
|
||||||
<CharacterCountPlugin setCount={() => {}} onChange={onChange} waitForInit />
|
<Jinjia2CharacterCountPlugin setCount={() => {}} />
|
||||||
<Jinja2InitialValuePlugin value={value} />
|
<Jinja2InitialValuePlugin value={value} onChange={onChange} />
|
||||||
<Jinja2BlurPlugin />
|
<Jinja2BlurPlugin />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2025-12-23 16:22:51
|
* @Date: 2025-12-23 16:22:51
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-03 20:44:16
|
* @Last Modified time: 2026-04-07 16:29:36
|
||||||
*/
|
*/
|
||||||
import { type FC, useState, useMemo } from 'react';
|
import { type FC, useState, useMemo } from 'react';
|
||||||
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
||||||
@@ -57,7 +57,6 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
language = 'string',
|
language = 'string',
|
||||||
height,
|
height,
|
||||||
className,
|
className,
|
||||||
waitForInit = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
console.log('Editor value', value)
|
console.log('Editor value', value)
|
||||||
const [_count, setCount] = useState(0);
|
const [_count, setCount] = useState(0);
|
||||||
@@ -149,10 +148,10 @@ const Editor: FC<LexicalEditorProps> =({
|
|||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<CommandPlugin />
|
<CommandPlugin />
|
||||||
<AutocompletePlugin options={options} enableJinja2={false} />
|
<AutocompletePlugin options={options} />
|
||||||
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
|
<CharacterCountPlugin setCount={setCount} onChange={onChange} />
|
||||||
<InitialValuePlugin value={value} options={options} enableLineNumbers={false} />
|
<InitialValuePlugin value={value} options={options} />
|
||||||
<BlurPlugin enableJinja2={false} />
|
<BlurPlugin />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
|||||||
setSelected(!isSelected);
|
setSelected(!isSelected);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!data.nodeData?.name) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
onClick={handleClick}
|
||||||
|
className="rb:inline rb:cursor-pointer rb:text-[#171719]"
|
||||||
|
contentEditable={false}
|
||||||
|
>
|
||||||
|
{data.value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2025-12-23 16:22:51
|
* @Date: 2025-12-23 16:22:51
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 17:12:41
|
* @Last Modified time: 2026-04-07 16:51:04
|
||||||
*/
|
*/
|
||||||
import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
|
import { useEffect, useLayoutEffect, useState, useRef, type FC } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
@@ -168,7 +168,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
|||||||
// Group suggestions by node ID
|
// Group suggestions by node ID
|
||||||
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, suggestion) => {
|
const groupedSuggestions = options.reduce((groups: Record<string, Suggestion[]>, suggestion) => {
|
||||||
const { nodeData } = suggestion
|
const { nodeData } = suggestion
|
||||||
const nodeId = nodeData.id as string;
|
const nodeId = nodeData?.id as string;
|
||||||
if (!groups[nodeId]) {
|
if (!groups[nodeId]) {
|
||||||
groups[nodeId] = [];
|
groups[nodeId] = [];
|
||||||
}
|
}
|
||||||
@@ -291,67 +291,67 @@ const AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
|
<div className="rb:py-1 rb:min-w-70 rb:max-h-50 rb:overflow-y-auto">
|
||||||
<Flex vertical gap={12}>
|
<Flex vertical gap={12}>
|
||||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
|
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => {
|
||||||
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
const nodeName = nodeOptions[0]?.nodeData?.name || nodeId;
|
||||||
const nodeIcon = nodeOptions[0]?.nodeData?.icon;
|
const nodeIcon = nodeOptions[0]?.nodeData?.icon;
|
||||||
return (
|
return (
|
||||||
<div key={nodeId}>
|
<div key={nodeId}>
|
||||||
<Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
{nodeName !== 'undefined' && <Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
||||||
{nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />}
|
{nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />}
|
||||||
{nodeName}
|
{nodeName}
|
||||||
</Flex>
|
</Flex>}
|
||||||
{nodeOptions.map((option) => {
|
{nodeOptions.map((option) => {
|
||||||
const globalIndex = flatOptions.indexOf(option);
|
const globalIndex = flatOptions.indexOf(option);
|
||||||
const isExpanded = expandedParent?.key === option.key;
|
const isExpanded = expandedParent?.key === option.key;
|
||||||
const hasChildren = !!option.children?.length;
|
const hasChildren = !!option.children?.length;
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
key={option.key}
|
key={option.key}
|
||||||
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
ref={(el) => { if (el) itemRefs.current.set(option.key, el); }}
|
||||||
data-selected={selectedIndex === globalIndex}
|
data-selected={selectedIndex === globalIndex}
|
||||||
className="rb:pl-6! rb:pr-3! rb:py-2!"
|
className="rb:pl-6! rb:pr-3! rb:py-2!"
|
||||||
align="center"
|
align="center"
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
style={{
|
style={{
|
||||||
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
cursor: option.disabled ? 'not-allowed' : 'pointer',
|
||||||
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
|
background: (selectedIndex === globalIndex || isExpanded) ? '#f0f8ff' : 'white',
|
||||||
opacity: option.disabled ? 0.5 : 1,
|
opacity: option.disabled ? 0.5 : 1,
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (option.disabled) return;
|
if (option.disabled) return;
|
||||||
insertMention(option);
|
insertMention(option);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setSelectedIndex(globalIndex);
|
setSelectedIndex(globalIndex);
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
const el = itemRefs.current.get(option.key);
|
const el = itemRefs.current.get(option.key);
|
||||||
if (el && popupRef.current) {
|
if (el && popupRef.current) {
|
||||||
const elRect = el.getBoundingClientRect();
|
const elRect = el.getBoundingClientRect();
|
||||||
const popupRect = popupRef.current.getBoundingClientRect();
|
const popupRect = popupRef.current.getBoundingClientRect();
|
||||||
setChildPanelTop(calcChildPanelTop(elRect, popupRect));
|
setChildPanelTop(calcChildPanelTop(elRect, popupRect));
|
||||||
|
}
|
||||||
|
setExpandedParent(option);
|
||||||
|
} else {
|
||||||
|
setExpandedParent(null);
|
||||||
}
|
}
|
||||||
setExpandedParent(option);
|
}}
|
||||||
} else {
|
>
|
||||||
setExpandedParent(null);
|
{option.label && <Space size={4}>
|
||||||
}
|
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : `{x}`}</span>
|
||||||
}}
|
<span>{option.label}</span>
|
||||||
>
|
</Space>}
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<span className="rb:text-[#155EEF]">{option.isContext ? '📄' : `{x}`}</span>
|
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
|
||||||
<span>{option.label}</span>
|
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1">›</span>}
|
||||||
</Space>
|
</Space>
|
||||||
<Space size={4}>
|
</Flex>
|
||||||
{option.dataType && <span className="rb:text-[#5B6167]">{option.dataType}</span>}
|
);
|
||||||
{hasChildren && <span className="rb:text-[#5B6167] rb:ml-1">›</span>}
|
})}
|
||||||
</Space>
|
</div>
|
||||||
</Flex>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</Flex>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Child variables panel - floats to the left */}
|
{/* Child variables panel - floats to the left */}
|
||||||
{expandedParent?.children?.length && (
|
{expandedParent?.children?.length && (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-04-02 17:10:59
|
* @Date: 2026-04-02 17:10:59
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-04-02 17:10:59
|
* @Last Modified time: 2026-04-07 14:50:14
|
||||||
*/
|
*/
|
||||||
import { useEffect, useState, useRef, type FC } from 'react';
|
import { useEffect, useState, useRef, type FC } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
@@ -161,7 +161,7 @@ const Jinja2AutocompletePlugin: FC<{ options: Suggestion[] }> = ({ options }) =>
|
|||||||
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => (
|
{Object.entries(groupedSuggestions).map(([nodeId, nodeOptions]) => (
|
||||||
<div key={nodeId}>
|
<div key={nodeId}>
|
||||||
<Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
<Flex align="center" gap={4} className="rb:px-3! rb:text-[12px] rb:py-1.25! rb:font-medium rb:text-[#5B6167]">
|
||||||
{nodeOptions[0]?.nodeData?.icon && <img src={nodeOptions[0].nodeData.icon} className="rb:size-3" alt="" />}
|
{nodeOptions[0]?.nodeData?.icon && <div className={`rb:size-3 rb:bg-cover ${nodeOptions[0].nodeData.icon}`} />}
|
||||||
{nodeOptions[0]?.nodeData?.name || nodeId}
|
{nodeOptions[0]?.nodeData?.name || nodeId}
|
||||||
</Flex>
|
</Flex>
|
||||||
{nodeOptions.map((option) => {
|
{nodeOptions.map((option) => {
|
||||||
|
|||||||
@@ -6,53 +6,50 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import { $getRoot, $createParagraphNode, $createTextNode } from 'lexical';
|
import { $getRoot, $createParagraphNode, $createTextNode, $isParagraphNode } from 'lexical';
|
||||||
|
|
||||||
interface Jinja2InitialValuePluginProps {
|
interface Jinja2InitialValuePluginProps {
|
||||||
value: string;
|
value?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Jinja2InitialValuePlugin: React.FC<Jinja2InitialValuePluginProps> = ({ value }) => {
|
const Jinja2InitialValuePlugin: React.FC<Jinja2InitialValuePluginProps> = ({ value, onChange }) => {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext();
|
||||||
const prevValueRef = useRef<string>('');
|
const internalValueRef = useRef<string | undefined>(undefined);
|
||||||
const isUserInputRef = useRef(false);
|
const onChangeRef = useRef(onChange);
|
||||||
|
onChangeRef.current = onChange;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerUpdateListener(({ editorState, tags }) => {
|
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||||
if (tags.has('programmatic')) return;
|
if (tags.has('programmatic')) return;
|
||||||
|
if (internalValueRef.current === undefined) return;
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const textContent = $getRoot().getTextContent();
|
const paragraphs = $getRoot().getChildren()
|
||||||
if (textContent !== prevValueRef.current) {
|
.filter($isParagraphNode)
|
||||||
isUserInputRef.current = true;
|
.map(p => p.getChildren().map(n => n.getTextContent()).join(''));
|
||||||
prevValueRef.current = textContent;
|
const text = paragraphs.join('\n');
|
||||||
|
if (text !== internalValueRef.current) {
|
||||||
|
internalValueRef.current = text;
|
||||||
|
onChangeRef.current?.(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value === prevValueRef.current) return;
|
if (value === undefined) return;
|
||||||
|
if (value === internalValueRef.current) return;
|
||||||
|
|
||||||
if (isUserInputRef.current) {
|
internalValueRef.current = value;
|
||||||
prevValueRef.current = value;
|
editor.update(() => {
|
||||||
isUserInputRef.current = false;
|
const root = $getRoot();
|
||||||
return;
|
root.clear();
|
||||||
}
|
value.split('\n').forEach((line) => {
|
||||||
|
const paragraph = $createParagraphNode();
|
||||||
prevValueRef.current = value;
|
paragraph.append($createTextNode(line));
|
||||||
isUserInputRef.current = false;
|
root.append(paragraph);
|
||||||
|
});
|
||||||
queueMicrotask(() => {
|
}, { tag: 'programmatic' });
|
||||||
editor.update(() => {
|
|
||||||
const root = $getRoot();
|
|
||||||
root.clear();
|
|
||||||
value.split('\n').forEach((line) => {
|
|
||||||
const paragraph = $createParagraphNode();
|
|
||||||
paragraph.append($createTextNode(line));
|
|
||||||
root.append(paragraph);
|
|
||||||
});
|
|
||||||
}, { tag: 'programmatic' });
|
|
||||||
});
|
|
||||||
}, [value, editor]);
|
}, [value, editor]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { $getRoot, $isParagraphNode } from 'lexical';
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
|
|
||||||
|
const Jinjia2CharacterCountPlugin = ({ setCount }: { setCount: (count: number) => void }) => {
|
||||||
|
const [editor] = useLexicalComposerContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return editor.registerUpdateListener(({ editorState }) => {
|
||||||
|
editorState.read(() => {
|
||||||
|
const root = $getRoot();
|
||||||
|
const paragraphs = root.getChildren()
|
||||||
|
.filter($isParagraphNode)
|
||||||
|
.map(p => p.getChildren().map(n => n.getTextContent()).join(''));
|
||||||
|
setCount(paragraphs.join('\n').length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [editor, setCount]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Jinjia2CharacterCountPlugin
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type FC, useEffect, useState } from "react";
|
import { type FC, useEffect, useState, useMemo } from "react";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
||||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||||
@@ -163,6 +163,24 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
|||||||
|
|
||||||
form.setFieldsValue(inititalValue)
|
form.setFieldsValue(inititalValue)
|
||||||
}
|
}
|
||||||
|
const getNumberOptions = useMemo(() => {
|
||||||
|
const list: Suggestion[] = []
|
||||||
|
|
||||||
|
options.forEach(vo => {
|
||||||
|
if (vo.children && vo?.children?.length > 0) {
|
||||||
|
const filterChild = vo.children.filter(child => child.dataType === 'number')
|
||||||
|
|
||||||
|
if (filterChild.length > 0) {
|
||||||
|
list.push({ ...vo, children: filterChild })
|
||||||
|
} else {
|
||||||
|
list.push({ ...vo, children: [] })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.push({ ...vo })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
}, [options])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -202,15 +220,14 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
|||||||
: parameter.type === 'boolean'
|
: parameter.type === 'boolean'
|
||||||
? <Switch size="small" />
|
? <Switch size="small" />
|
||||||
: parameter.type === 'integer' || parameter.type === 'number'
|
: parameter.type === 'integer' || parameter.type === 'number'
|
||||||
? <InputNumber
|
? <Editor
|
||||||
min={parameter.minimum}
|
variant="outlined"
|
||||||
max={parameter.maximum}
|
type="input"
|
||||||
step={parameter.type === 'integer' ? 1 : 0.01}
|
size="small"
|
||||||
placeholder={t('common.pleaseEnter')}
|
height={28}
|
||||||
className="rb:w-full!"
|
options={getNumberOptions}
|
||||||
size="small"
|
placeholder={t('common.pleaseEnter')}
|
||||||
onChange={(value) => form.setFieldValue(['tool_parameters', parameter.name], value)}
|
/>
|
||||||
/>
|
|
||||||
: <Editor
|
: <Editor
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="input"
|
type="input"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-03 15:17:48
|
* @Date: 2026-02-03 15:17:48
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-31 11:13:23
|
* @Last Modified time: 2026-04-07 16:47:09
|
||||||
*/
|
*/
|
||||||
import { useRef, useEffect, useState } from 'react';
|
import { useRef, useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -18,6 +18,7 @@ import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
|||||||
import { useUser } from '@/store/user';
|
import { useUser } from '@/store/user';
|
||||||
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
||||||
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'
|
import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'
|
||||||
|
import type { Suggestion } from '../components/Editor/plugin/AutocompletePlugin';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for useWorkflowGraph hook
|
* Props for useWorkflowGraph hook
|
||||||
@@ -73,6 +74,8 @@ export interface UseWorkflowGraphReturn {
|
|||||||
handleAddNotes: () => void;
|
handleAddNotes: () => void;
|
||||||
handleSaveFeaturesConfig: (value: FeaturesConfigForm) => void;
|
handleSaveFeaturesConfig: (value: FeaturesConfigForm) => void;
|
||||||
features?: FeaturesConfigForm;
|
features?: FeaturesConfigForm;
|
||||||
|
/** Get start node output variable list (user-defined + system variables) */
|
||||||
|
getStartNodeVariables: () => Array<{ name: string; type: string; readonly?: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1363,9 +1366,49 @@ export const useWorkflowGraph = ({
|
|||||||
data: { ...cleanNodeData },
|
data: { ...cleanNodeData },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const getStartNodeVariables = (): Array<{ name: string; type: string; readonly?: boolean }> => {
|
||||||
|
const startNode = graphRef.current?.getNodes().find(n => n.getData()?.type === 'start')
|
||||||
|
if (!startNode) return []
|
||||||
|
const data = startNode.getData()
|
||||||
|
const userVars: Array<{ name: string; type: string; readonly?: boolean }> =
|
||||||
|
(data?.config?.variables?.defaultValue ?? []).map((v: any) => ({ name: v.name, type: v.type }))
|
||||||
|
return userVars
|
||||||
|
}
|
||||||
|
|
||||||
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
const handleSaveFeaturesConfig = (value?: FeaturesConfigForm) => {
|
||||||
|
const { statement = '' } = value?.opening_statement || {}
|
||||||
featuresRef.current = value
|
featuresRef.current = value
|
||||||
onFeaturesLoad?.(value)
|
onFeaturesLoad?.(value)
|
||||||
|
|
||||||
|
const usedVars = [...new Set([...(statement?.matchAll(/\{\{(\w+)\}\}/g) ?? [])].map(m => m[1]))]
|
||||||
|
const startVars = getStartNodeVariables()
|
||||||
|
const validNames = new Set(startVars.map(v => v.name))
|
||||||
|
const invalid = usedVars.filter(v => !validNames.has(v))
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
const newVars = invalid.map(name => ({
|
||||||
|
name,
|
||||||
|
description: name,
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
defaultValue: '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const startNode = graphRef.current?.getNodes().find(n => n.getData()?.type === 'start')
|
||||||
|
if (startNode) {
|
||||||
|
const data = startNode.getData()
|
||||||
|
console.log('startNode', [...startVars, ...newVars])
|
||||||
|
startNode.setData({
|
||||||
|
...data,
|
||||||
|
config: {
|
||||||
|
...data.config,
|
||||||
|
variables: {
|
||||||
|
...data.config.variables,
|
||||||
|
defaultValue: [...startVars, ...newVars],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1389,5 +1432,6 @@ export const useWorkflowGraph = ({
|
|||||||
handleAddNotes,
|
handleAddNotes,
|
||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features: featuresRef.current,
|
features: featuresRef.current,
|
||||||
|
getStartNodeVariables,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import Properties from './components/Properties';
|
|||||||
import CanvasToolbar from './components/CanvasToolbar';
|
import CanvasToolbar from './components/CanvasToolbar';
|
||||||
import PortClickHandler from './components/PortClickHandler';
|
import PortClickHandler from './components/PortClickHandler';
|
||||||
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
||||||
import type { WorkflowRef, FeaturesConfigForm } from '@/views/ApplicationConfig/types'
|
import type { WorkflowRef, FeaturesConfigForm, FeaturesConfigModalRef } from '@/views/ApplicationConfig/types'
|
||||||
import Chat from './components/Chat/Chat';
|
import Chat from './components/Chat/Chat';
|
||||||
import type { ChatRef, AddChatVariableRef } from './types'
|
import type { ChatRef, AddChatVariableRef } from './types'
|
||||||
import AddChatVariable from './components/AddChatVariable';
|
import AddChatVariable from './components/AddChatVariable';
|
||||||
|
import FeaturesConfigModal from '@/views/ApplicationConfig/components/FeaturesConfig/FeaturesConfigModal'
|
||||||
|
|
||||||
const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
|
const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesConfigForm | undefined) => void }>(({ onFeaturesLoad }, ref) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -35,7 +36,8 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
setChatVariables,
|
setChatVariables,
|
||||||
handleAddNotes,
|
handleAddNotes,
|
||||||
handleSaveFeaturesConfig,
|
handleSaveFeaturesConfig,
|
||||||
features
|
features,
|
||||||
|
getStartNodeVariables,
|
||||||
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
} = useWorkflowGraph({ containerRef, miniMapRef, onFeaturesLoad });
|
||||||
|
|
||||||
const onDragOver = (event: React.DragEvent) => {
|
const onDragOver = (event: React.DragEvent) => {
|
||||||
@@ -51,6 +53,15 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
addChatVariableRef.current?.handleOpen()
|
addChatVariableRef.current?.handleOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ref used to imperatively open the config modal
|
||||||
|
const funConfigModalRef = useRef<FeaturesConfigModalRef>(null)
|
||||||
|
|
||||||
|
/** Open the feature config modal pre-populated with the current values */
|
||||||
|
const handleFeaturesConfig = () => {
|
||||||
|
blankClick()
|
||||||
|
funConfigModalRef.current?.handleOpen(features as FeaturesConfigForm)
|
||||||
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleSave,
|
handleSave,
|
||||||
handleRun,
|
handleRun,
|
||||||
@@ -59,6 +70,7 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
chatVariables,
|
chatVariables,
|
||||||
config,
|
config,
|
||||||
features: features,
|
features: features,
|
||||||
|
handleFeaturesConfig,
|
||||||
handleSaveFeaturesConfig
|
handleSaveFeaturesConfig
|
||||||
}))
|
}))
|
||||||
return (
|
return (
|
||||||
@@ -112,6 +124,13 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC
|
|||||||
variables={chatVariables}
|
variables={chatVariables}
|
||||||
onChange={setChatVariables}
|
onChange={setChatVariables}
|
||||||
/>
|
/>
|
||||||
|
{/* Modal for editing feature settings; calls refresh on save */}
|
||||||
|
<FeaturesConfigModal
|
||||||
|
ref={funConfigModalRef}
|
||||||
|
refresh={handleSaveFeaturesConfig}
|
||||||
|
source="workflow"
|
||||||
|
chatVariables={getStartNodeVariables().map(v => ({ name: v.name, key: `start_${v.name}`, label: v.name, type: 'variable', dataType: v.type, value:`{{${v.name}}}` })) as any}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user