Merge pull request #798 from SuanmoSuanyangTechnology/feature/ui_upgrade_zy

Feature/UI upgrade zy
This commit is contained in:
yingzhao
2026-04-07 17:13:34 +08:00
committed by GitHub
21 changed files with 398 additions and 218 deletions

View File

@@ -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]!',

View File

@@ -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'

View File

@@ -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) => {

View File

@@ -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"

View File

@@ -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}
/> />

View File

@@ -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">

View File

@@ -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;
} }

View File

@@ -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"

View File

@@ -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}>

View File

@@ -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>
) )
} }

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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}

View File

@@ -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 && (

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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

View File

@@ -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"

View File

@@ -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,
}; };
}; };

View File

@@ -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>
); );
}); });