From d3cd66fc6e78c603b9ef991dd64781f2c543e454 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Wed, 1 Apr 2026 12:03:56 +0800 Subject: [PATCH 1/6] feat(web): use bg replace img --- web/src/assets/images/common/return.svg | 17 ----- web/src/components/ButtonCheckbox/index.tsx | 4 +- web/src/components/ModelSelect/index.tsx | 4 +- web/src/components/RadioGroupCard/index.tsx | 2 +- web/src/components/RbModal/index.tsx | 2 +- web/src/components/Table/index.tsx | 2 +- web/src/views/ApplicationConfig/Agent.tsx | 2 +- .../components/ConfigHeader.tsx | 23 +++--- .../views/Home/components/QuickOperation.tsx | 17 ++--- .../views/Home/components/RecentActivity.tsx | 14 ++-- .../views/Index/components/QuickActions.tsx | 41 ++-------- web/src/views/Index/index.tsx | 2 +- .../views/Ontology/components/PageHeader.tsx | 2 +- web/src/views/UserMemoryDetail/Rag.tsx | 12 ++- .../components/PageHeader.tsx | 65 ---------------- .../UserMemoryDetail/pages/GraphDetail.tsx | 2 +- .../views/UserMemoryDetail/pages/index.tsx | 2 +- .../Workflow/components/Chat/Runtime.tsx | 2 +- .../components/Editor/nodes/VariableNode.tsx | 6 +- .../Editor/plugin/AutocompletePlugin.tsx | 6 +- .../views/Workflow/components/NodeLibrary.tsx | 4 +- .../Workflow/components/Nodes/AddNode.tsx | 2 +- .../components/Nodes/ConditionNode.tsx | 2 +- .../Workflow/components/Nodes/LoopNode.tsx | 2 +- .../Workflow/components/Nodes/NormalNode.tsx | 2 +- .../Workflow/components/PortClickHandler.tsx | 54 +++++--------- .../components/Properties/VariableSelect.tsx | 12 +-- .../Workflow/components/Properties/index.tsx | 2 +- web/src/views/Workflow/constant.ts | 74 +++++++------------ web/src/views/Workflow/index.tsx | 2 +- 30 files changed, 104 insertions(+), 279 deletions(-) delete mode 100644 web/src/assets/images/common/return.svg delete mode 100644 web/src/views/UserMemoryDetail/components/PageHeader.tsx diff --git a/web/src/assets/images/common/return.svg b/web/src/assets/images/common/return.svg deleted file mode 100644 index cb8166c0..00000000 --- a/web/src/assets/images/common/return.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - 退出 - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/src/components/ButtonCheckbox/index.tsx b/web/src/components/ButtonCheckbox/index.tsx index 8c52701b..0804a1b3 100644 --- a/web/src/components/ButtonCheckbox/index.tsx +++ b/web/src/components/ButtonCheckbox/index.tsx @@ -74,9 +74,9 @@ const ButtonCheckbox: FC = ({ onClick={handleChange} > {/* Display unchecked icon when not checked */} - {icon && !checked && } + {icon && !checked && {icon}} {/* Display checked icon when checked */} - {checkedIcon && checked && } + {checkedIcon && checked && {checkedIcon}} {children} ); diff --git a/web/src/components/ModelSelect/index.tsx b/web/src/components/ModelSelect/index.tsx index 3a6e42b6..8f9152fb 100644 --- a/web/src/components/ModelSelect/index.tsx +++ b/web/src/components/ModelSelect/index.tsx @@ -54,7 +54,7 @@ const ModelSelect: FC = ({ const logo = getListLogoUrl(item.provider, item.logo as string); return ( - {logo && } + {logo && {logo}}
{item.name}
); @@ -75,7 +75,7 @@ const ModelSelect: FC = ({ return ( - {logo && } + {logo && {logo}} {data.name as string} {data.capability?.length > 0 && ( diff --git a/web/src/components/RadioGroupCard/index.tsx b/web/src/components/RadioGroupCard/index.tsx index 49020b38..e346d7af 100644 --- a/web/src/components/RadioGroupCard/index.tsx +++ b/web/src/components/RadioGroupCard/index.tsx @@ -106,7 +106,7 @@ const RadioGroupCard: FC = ({ {/* Use custom render or default card layout */} {itemRender ? itemRender(option) : ( <> - {option.icon && {option.icon}}
diff --git a/web/src/components/RbModal/index.tsx b/web/src/components/RbModal/index.tsx index 199bfab5..84a57d8d 100644 --- a/web/src/components/RbModal/index.tsx +++ b/web/src/components/RbModal/index.tsx @@ -44,7 +44,7 @@ const RbModal: FC = ({ {...props} > {/* Scrollable content container */} -
+
{children}
diff --git a/web/src/components/Table/index.tsx b/web/src/components/Table/index.tsx index 62c9421a..08089499 100644 --- a/web/src/components/Table/index.tsx +++ b/web/src/components/Table/index.tsx @@ -91,7 +91,7 @@ const RbTable = forwardRef(, Q = Record {extra} diff --git a/web/src/views/UserMemoryDetail/Rag.tsx b/web/src/views/UserMemoryDetail/Rag.tsx index a11d4295..ff9069c7 100644 --- a/web/src/views/UserMemoryDetail/Rag.tsx +++ b/web/src/views/UserMemoryDetail/Rag.tsx @@ -16,8 +16,6 @@ import { Row, Col, Skeleton, Spin, Flex, Tooltip } from 'antd' import { LoadingOutlined } from '@ant-design/icons'; import { useParams } from 'react-router-dom' -import aboutUs from '@/assets/images/userMemory/aboutUs.svg' -import memoryInsight from '@/assets/images/userMemory/memoryInsight.svg' import RbCard from '@/components/RbCard/Card' import type { Data } from './types' import { @@ -34,12 +32,12 @@ import ConversationMemory from './components/ConversationMemory' */ interface TitleProps { title: string - icon: string + iconClassName: string } /** Collapsible section title */ -const Title: FC = ({ title, icon }) => ( +const Title: FC = ({ title, iconClassName }) => ( - +
{title} ) @@ -143,7 +141,7 @@ const Rag: FC = () => { <> <div className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2.5 rb:px-3 rb:mb-4"> {loading.summary @@ -160,7 +158,7 @@ const Rag: FC = () => { <> <Title title={t('userMemory.memoryInsight')} - icon={memoryInsight} + iconClassName="rb:bg-[url('@/assets/images/userMemory/memoryInsight.svg')]" /> <div className="rb:bg-[#F6F6F6] rb:rounded-lg rb:py-2.5 rb:px-3"> {loading.insight diff --git a/web/src/views/UserMemoryDetail/components/PageHeader.tsx b/web/src/views/UserMemoryDetail/components/PageHeader.tsx deleted file mode 100644 index 861bf0f5..00000000 --- a/web/src/views/UserMemoryDetail/components/PageHeader.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * @Author: ZhaoYing - * @Date: 2026-02-03 18:32:30 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 18:32:30 - */ -/** - * Page Header Component - * Header with navigation and operation buttons - */ - -import { type FC, type ReactNode } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { Layout, Button } from 'antd'; -import { useTranslation } from 'react-i18next'; - -import logoutIcon from '@/assets/images/logout_hover.svg' - -const { Header } = Layout; - -/** - * Component props - */ -interface ConfigHeaderProps { - name?: string; - operation?: ReactNode; - source?: 'detail' | 'node'; - extra?: ReactNode; -} -const PageHeader: FC<ConfigHeaderProps> = ({ - name, - operation, - source = 'detail', - extra -}) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - - /** Navigate back */ - const goBack = () => { - if (source === 'detail') { - navigate('/user-memory', { replace: true }) - } else { - navigate(-1) - } - } - return ( - <Header className="rb:w-full rb:h-16 rb:flex rb:justify-between rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8"> - <div className="rb:h-8 rb:flex rb:items-center rb:font-medium"> - {t('userMemory.memoryWindow', { name: name })} - {operation} - </div> - - <div className="rb:flex rb:items-center rb:gap-3"> - <Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={goBack}> - <img src={logoutIcon} className="rb:w-4 rb:h-4" /> - {t('common.return')} - </Button> - {extra} - </div> - </Header> - ); -}; - -export default PageHeader; \ No newline at end of file diff --git a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx index 19f49ff0..d2417e1c 100644 --- a/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx +++ b/web/src/views/UserMemoryDetail/pages/GraphDetail.tsx @@ -104,7 +104,7 @@ const GraphDetail = forwardRef<GraphDetailRef>((_props, ref) => { <Space size={12}> <Button className="rb:px-2! rb:gap-0.5!" - icon={<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/return.svg')]"></div>} + icon={<div className="rb:bg-[url('@/assets/images/workflow/return.svg')] rb:size-4 rb:bg-cover"></div>} onClick={() => navigate(-1)} > {t('common.return')} diff --git a/web/src/views/UserMemoryDetail/pages/index.tsx b/web/src/views/UserMemoryDetail/pages/index.tsx index f8ce00c3..8cc62ec0 100644 --- a/web/src/views/UserMemoryDetail/pages/index.tsx +++ b/web/src/views/UserMemoryDetail/pages/index.tsx @@ -106,7 +106,7 @@ const Detail: FC = () => { } <Button className="rb:px-2! rb:gap-0.5!" - icon={<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/common/return.svg')]"></div>} + icon={<div className="rb:bg-[url('@/assets/images/workflow/return.svg')] rb:size-4 rb:bg-cover"></div>} onClick={handleGoBack} > {t('common.return')} diff --git a/web/src/views/Workflow/components/Chat/Runtime.tsx b/web/src/views/Workflow/components/Chat/Runtime.tsx index 142b7e1d..7da550e0 100644 --- a/web/src/views/Workflow/components/Chat/Runtime.tsx +++ b/web/src/views/Workflow/components/Chat/Runtime.tsx @@ -137,7 +137,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({ key: vo.node_id, label: <div className={clsx("rb:flex rb:justify-between rb:items-center", getStatus(vo.status))}> <div className="rb:flex rb:items-center rb:gap-1 rb:flex-1"> - {vo.icon && <img src={vo.icon} className="rb:size-4" />} + {vo.icon && <div className={`rb:size-4 rb:bg-cover ${vo.icon}`} />} <div className="rb:wrap-break-word rb:line-clamp-1">{vo.node_name}</div> </div> <span> diff --git a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx index 6462e1ae..1a1a09c4 100644 --- a/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx +++ b/web/src/views/Workflow/components/Editor/nodes/VariableNode.tsx @@ -45,11 +45,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({ {data.isContext ? ( <span style={{ fontSize: '12px', marginRight: '4px' }}>📄</span> ) : data.group !== 'CONVERSATION' ? ( - <img - src={data.nodeData?.icon} - style={{ width: '12px', height: '12px', marginRight: '4px' }} - alt="" - /> + <div className={`rb:size-4 rb:mr-1 rb:bg-cover ${data.nodeData?.icon}`} /> ) : null} {!data.isContext && data.group !== 'CONVERSATION' && ( <> diff --git a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx index 863e5160..c78eac38 100644 --- a/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx +++ b/web/src/views/Workflow/components/Editor/plugin/AutocompletePlugin.tsx @@ -288,11 +288,7 @@ const AutocompletePlugin: FC<{ options: Suggestion[], enableJinja2?: boolean }> return ( <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]"> - {nodeIcon && <img - src={nodeIcon} - className="rb:size-3" - alt="" - />} + {nodeIcon && <div className={`rb:size-3 rb:bg-cover ${nodeIcon}`} />} {nodeName} </Flex> {nodeOptions.map((option) => { diff --git a/web/src/views/Workflow/components/NodeLibrary.tsx b/web/src/views/Workflow/components/NodeLibrary.tsx index a7b06fd1..e6190adb 100644 --- a/web/src/views/Workflow/components/NodeLibrary.tsx +++ b/web/src/views/Workflow/components/NodeLibrary.tsx @@ -49,7 +49,7 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col e.dataTransfer.setData('application/json', JSON.stringify(node)); }} > - <img src={node.icon} className="rb:size-6 rb:cursor-pointer" /> + <div className={`rb:size-6 rb:cursor-pointer rb:bg-cover ${node.icon}`} /> </div> </Tooltip> )) @@ -77,7 +77,7 @@ const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ col e.dataTransfer.setData('application/json', JSON.stringify(node)); }} > - <img src={node.icon} className="rb:size-6" /> + <div className={`rb:size-6 rb:bg-cover ${node.icon}`} /> <span className="rb:font-medium rb:text-[12px] rb:leading-4">{t(`workflow.${node.type}`)}</span> </Flex> ))} diff --git a/web/src/views/Workflow/components/Nodes/AddNode.tsx b/web/src/views/Workflow/components/Nodes/AddNode.tsx index dd0ab23d..5f1e7e65 100644 --- a/web/src/views/Workflow/components/Nodes/AddNode.tsx +++ b/web/src/views/Workflow/components/Nodes/AddNode.tsx @@ -151,7 +151,7 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => { e.currentTarget.style.background = 'white'; }} > - <img src={nodeType.icon} className="rb:w-4 rb:h-4" /> + <div className={`rb:size-4 rb:bg-cover ${nodeType.icon}`} /> <span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span> </div> ))} diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx index 516b5125..79e8352c 100644 --- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -52,7 +52,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { })}> <NodeTools node={node} /> <Flex align="center" gap={8} className="rb:flex-1"> - <img src={data.icon} className="rb:size-6" /> + <div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> </Flex> diff --git a/web/src/views/Workflow/components/Nodes/LoopNode.tsx b/web/src/views/Workflow/components/Nodes/LoopNode.tsx index 29c683cc..4a803246 100644 --- a/web/src/views/Workflow/components/Nodes/LoopNode.tsx +++ b/web/src/views/Workflow/components/Nodes/LoopNode.tsx @@ -126,7 +126,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => { })}> <NodeTools node={node} /> <Flex align="center" gap={8} className="rb:flex-1"> - <img src={data.icon} className="rb:size-6" /> + <div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> </Flex> <div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div> diff --git a/web/src/views/Workflow/components/Nodes/NormalNode.tsx b/web/src/views/Workflow/components/Nodes/NormalNode.tsx index 12e89cca..f947d004 100644 --- a/web/src/views/Workflow/components/Nodes/NormalNode.tsx +++ b/web/src/views/Workflow/components/Nodes/NormalNode.tsx @@ -16,7 +16,7 @@ const NormalNode: ReactShapeConfig['component'] = ({ node }) => { })}> <NodeTools node={node} /> <Flex align="center" gap={8} className="rb:flex-1"> - <img src={data.icon} className="rb:size-6" /> + <div className={`rb:size-6 rb:bg-cover ${data.icon}`} /> <div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div> </Flex> diff --git a/web/src/views/Workflow/components/PortClickHandler.tsx b/web/src/views/Workflow/components/PortClickHandler.tsx index 13ad6b98..31693722 100644 --- a/web/src/views/Workflow/components/PortClickHandler.tsx +++ b/web/src/views/Workflow/components/PortClickHandler.tsx @@ -5,7 +5,7 @@ * @Last Modified time: 2026-03-30 15:14:02 */ import { useEffect, useState } from 'react'; -import { Popover } from 'antd'; +import { Flex, Popover } from 'antd'; import { useTranslation } from 'react-i18next'; import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../constant'; @@ -286,21 +286,16 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => { }; const content = ( - <div style={{ maxHeight: '300px', overflowY: 'auto', minWidth: `${nodeWidth}px` }}> - {nodeLibrary.map((category, categoryIndex) => { + <Flex vertical gap={16} className="rb:max-h-[300px] rb:overflow-y-auto rb:p-3" style={{ minWidth: `${nodeWidth}px` }}> + {nodeLibrary.map((category) => { const sourceNodeData = sourceNode?.getData(); const isChildOfLoop = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'loop'); const isChildOfIteration = sourceNodeData?.cycle && graph?.getNodes().find((n: any) => n.getData()?.id === sourceNodeData.cycle && n.getData()?.type === 'iteration'); let filteredNodes; - if (isChildOfLoop) { - // Use same filtering as AddNode for child nodes of loop, but allow break - filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); - } else if (isChildOfIteration) { - // Filter out loop and iteration nodes for children of iteration nodes, but allow break + if (isChildOfLoop || isChildOfIteration) { filteredNodes = category.nodes.filter(nodeType => !['start', 'end', 'loop', 'cycle-start', 'iteration'].includes(nodeType.type)); } else { - // Original filtering for non-loop child nodes filteredNodes = category.nodes.filter(nodeType => nodeType.type !== 'start' && nodeType.type !== 'cycle-start' && nodeType.type !== 'break' ); @@ -310,36 +305,27 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => { return ( <div key={category.category}> - {categoryIndex > 0 && <div style={{ height: '1px', background: '#f0f0f0', margin: '4px 0' }} />} - <div style={{ padding: '4px 12px', fontSize: '12px', color: '#999', fontWeight: 'bold' }}> + <div className="rb:font-semibold rb:mb-2 rb:text-[12px] rb:leading-4.5 rb:pl-1"> {t(`workflow.${category.category}`)} </div> - {filteredNodes.map((nodeType) => ( - <div - key={nodeType.type} - style={{ - padding: '8px 12px', - cursor: 'pointer', - display: 'flex', - alignItems: 'center', - gap: '8px', - }} - onClick={() => handleNodeSelect(nodeType)} - onMouseEnter={(e) => { - e.currentTarget.style.background = '#f0f8ff'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = 'white'; - }} - > - <img src={nodeType.icon} className="rb:w-4 rb:h-4" /> - <span style={{ fontSize: '14px' }}>{t(`workflow.${nodeType.type}`)}</span> - </div> - ))} + <Flex gap={6} vertical> + {filteredNodes.map((nodeType) => ( + <Flex + key={nodeType.type} + align="center" + gap={8} + className="rb:rounded-xl rb:p-2! rb:border rb:border-[#EBEBEB] rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!" + onClick={() => handleNodeSelect(nodeType)} + > + <div className={`rb:size-6 rb:bg-cover ${nodeType.icon}`} /> + <span className="rb:font-medium rb:text-[12px] rb:leading-4">{t(`workflow.${nodeType.type}`)}</span> + </Flex> + ))} + </Flex> </div> ); })} - </div> + </Flex> ); if (!tempElement) return null; diff --git a/web/src/views/Workflow/components/Properties/VariableSelect.tsx b/web/src/views/Workflow/components/Properties/VariableSelect.tsx index 9170d065..51101736 100644 --- a/web/src/views/Workflow/components/Properties/VariableSelect.tsx +++ b/web/src/views/Workflow/components/Properties/VariableSelect.tsx @@ -73,11 +73,7 @@ const VariableSelect: FC<VariableSelectProps> = ({ > {filterOption.nodeData?.icon && filterOption.nodeData?.name && ( <> - <img - src={filterOption.nodeData.icon} - style={{ width: '12px', height: '12px', marginRight: '4px' }} - alt="" - /> + <div className={`rb:size-3 rb:mr-1 rb:bg-cover ${filterOption.nodeData.icon}`} /> {filterOption.nodeData.name} <span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span> </> @@ -111,11 +107,7 @@ const VariableSelect: FC<VariableSelectProps> = ({ */ const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({ label: <Flex align="center" gap={4}> - {suggestions[0].nodeData.icon && <img - src={suggestions[0].nodeData.icon} - className="rb:size-3" - alt="" - />} + {suggestions[0].nodeData.icon && <div className={`rb:size-3 ${suggestions[0].nodeData.icon}`} />} {suggestions[0].nodeData.name} </Flex>, options: suggestions.map(s => ({ diff --git a/web/src/views/Workflow/components/Properties/index.tsx b/web/src/views/Workflow/components/Properties/index.tsx index 66b59075..e38331db 100644 --- a/web/src/views/Workflow/components/Properties/index.tsx +++ b/web/src/views/Workflow/components/Properties/index.tsx @@ -474,7 +474,7 @@ const Properties: FC<PropertiesProps> = ({ label: t(`workflow.${category.category}`), options: category.nodes.filter(item => !['cycle-start', 'break'].includes(item.type)).map(node => ({ label: <div className="rb:flex rb:items-center rb:gap-2 rb:flex-1"> - <img src={node.icon} className="rb:size-3.5" /> + <div className={`rb:size-3.5 rb:bg-cover ${node.icon}`} /> <div className="rb:wrap-break-word rb:line-clamp-1">{t(`workflow.${node.type}`)}</div> </div>, value: node.type diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index 92773191..2de35fbb 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -13,28 +13,6 @@ import NoteNode from './components/Nodes/NoteNode'; import type { PortMetadata, GroupMetadata } from '@antv/x6/lib/model/port'; import type { ReactShapeConfig } from '@antv/x6-react-shape'; -// Import workflow icons -import startIcon from '@/assets/images/workflow/start.svg'; -import endIcon from '@/assets/images/workflow/end.svg'; -import llmIcon from '@/assets/images/workflow/llm.svg'; -import ragIcon from '@/assets/images/workflow/rag.svg'; -import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.svg'; -import conditionIcon from '@/assets/images/workflow/condition.svg'; -import iterationIcon from '@/assets/images/workflow/iteration.svg'; -import loopIcon from '@/assets/images/workflow/loop.svg'; -import aggregatorIcon from '@/assets/images/workflow/aggregator.svg'; -import httpRequestIcon from '@/assets/images/workflow/http_request.svg'; -import toolsIcon from '@/assets/images/workflow/tools.svg'; -import codeExecutionIcon from '@/assets/images/workflow/code_execution.svg'; -import templateRenderingIcon from '@/assets/images/workflow/template_rendering.svg'; -import questionClassifierIcon from '@/assets/images/workflow/question-classifier.svg' -import breakIcon from '@/assets/images/workflow/break.svg' -import assignerIcon from '@/assets/images/workflow/assigner.svg' -import memoryReadIcon from '@/assets/images/workflow/memory-read.svg' -import memoryWriteIcon from '@/assets/images/workflow/memory-write.svg' -import unknownIcon from '@/assets/images/workflow/unknown.svg' -import documentExtractorIcon from '@/assets/images/workflow/document-extractor.svg' - import { memoryConfigListUrl } from '@/api/memory' import type { NodeLibrary } from './types' @@ -46,7 +24,7 @@ export const nodeLibrary: NodeLibrary[] = [ { category: "coreNode", nodes: [ - { type: "start", icon: startIcon, + { type: "start", icon: 'rb:bg-[url("@/assets/images/workflow/start.svg")]', config: { variables: { type: 'define', @@ -87,7 +65,7 @@ export const nodeLibrary: NodeLibrary[] = [ } }, { - type: "end", icon: endIcon, + type: "end", icon: 'rb:bg-[url("@/assets/images/workflow/end.svg")]', config: { output: { type: 'editor' @@ -100,7 +78,7 @@ export const nodeLibrary: NodeLibrary[] = [ { category: "aiAndCognitiveProcessing", nodes: [ - { type: "llm", icon: llmIcon, + { type: "llm", icon: 'rb:bg-[url("@/assets/images/workflow/llm.svg")]', config: { model_id: { type: 'define', @@ -154,7 +132,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "knowledge-retrieval", icon: ragIcon, + { type: "knowledge-retrieval", icon: 'rb:bg-[url("@/assets/images/workflow/rag.svg")]', config: { query: { type: 'variableList', @@ -164,7 +142,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "parameter-extractor", icon: parameterExtractionIcon, + { type: "parameter-extractor", icon: 'rb:bg-[url("@/assets/images/workflow/parameter_extraction.svg")]', config: { model_id: { type: 'modelSelect', @@ -191,7 +169,7 @@ export const nodeLibrary: NodeLibrary[] = [ { category: "cognitiveUpgrading", nodes: [ - { type: "memory-read", icon: memoryReadIcon, + { type: "memory-read", icon: 'rb:bg-[url("@/assets/images/workflow/memory-read.svg")]', config: { message: { type: 'editor', @@ -214,7 +192,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "memory-write", icon: memoryWriteIcon, + { type: "memory-write", icon: 'rb:bg-[url("@/assets/images/workflow/memory-write.svg")]', config: { message: { type: 'editor', @@ -240,7 +218,7 @@ export const nodeLibrary: NodeLibrary[] = [ { category: "flowControl", nodes: [ - { type: "if-else", icon: conditionIcon, + { type: "if-else", icon: 'rb:bg-[url("@/assets/images/workflow/condition.svg")]', config: { cases: { type: 'caseList', @@ -253,7 +231,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "question-classifier", icon: questionClassifierIcon, + { type: "question-classifier", icon: 'rb:bg-[url("@/assets/images/workflow/question-classifier.svg")]', config: { model_id: { type: 'modelSelect', @@ -277,7 +255,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "iteration", icon: iterationIcon, + { type: "iteration", icon: 'rb:bg-[url("@/assets/images/workflow/iteration.svg")]', config: { input: { type: 'variableList', @@ -310,7 +288,7 @@ export const nodeLibrary: NodeLibrary[] = [ } }, }, - { type: "loop", icon: loopIcon, + { type: "loop", icon: 'rb:bg-[url("@/assets/images/workflow/loop.svg")]', config: { cycle_vars: { type: 'cycleVarsList', @@ -333,9 +311,10 @@ export const nodeLibrary: NodeLibrary[] = [ }, } }, - { type: "cycle-start", icon: startIcon }, - { type: "break", icon: breakIcon }, - { type: "var-aggregator", icon: aggregatorIcon, + { type: "cycle-start", icon: 'rb:bg-[url("@/assets/images/workflow/start.svg")]'}, + { type: "break", icon: 'rb:bg-[url("@/assets/images/workflow/break.svg")]'}, + { + type: "var-aggregator", icon: 'rb:bg-[url("@/assets/images/workflow/aggregator.svg")]', config: { group: { type: 'switch', @@ -350,7 +329,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "assigner", icon: assignerIcon, + { type: "assigner", icon: 'rb:bg-[url("@/assets/images/workflow/assigner.svg")]', config: { assignments: { type: 'assignmentList', @@ -363,7 +342,7 @@ export const nodeLibrary: NodeLibrary[] = [ { category: "externalInteraction", nodes: [ - { type: "http-request", icon: httpRequestIcon, + { type: "http-request", icon: 'rb:bg-[url("@/assets/images/workflow/http_request.svg")]', config: { method: { type: 'select', @@ -423,7 +402,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "tool", icon: toolsIcon, + { type: "tool", icon: 'rb:bg-[url("@/assets/images/workflow/tools.svg")]', config: { tool_id: { type: 'cascader' @@ -433,7 +412,7 @@ export const nodeLibrary: NodeLibrary[] = [ } } }, - { type: "code", icon: codeExecutionIcon, + { type: "code", icon: 'rb:bg-[url("@/assets/images/workflow/code_execution.svg")]', config: { input_variables: { type: 'inputList', @@ -459,7 +438,7 @@ export const nodeLibrary: NodeLibrary[] = [ }, } }, - { type: "jinja-render", icon: templateRenderingIcon, + { type: "jinja-render", icon: 'rb:bg-[url("@/assets/images/workflow/template_rendering.svg")]', config: { mapping: { type: 'mappingList', @@ -474,7 +453,7 @@ export const nodeLibrary: NodeLibrary[] = [ }, } }, - { type: "document-extractor", icon: documentExtractorIcon, + { type: "document-extractor", icon: 'rb:bg-[url("@/assets/images/workflow/document-extractor.svg")]', config: { file_selector: { type: 'variableList', @@ -527,7 +506,8 @@ export const THEME_MAP: Record<string, { outer: string; title: string; bg: strin } export const notesConfig = { - type: "notes", icon: templateRenderingIcon, + type: "notes", + icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]', config: { text: { type: 'define', @@ -555,11 +535,11 @@ export const notesConfig = { } export const unknownNode = { type: 'unknown', - icon: unknownIcon + icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]' } export const noteNode = { type: 'notes', - icon: unknownIcon + icon: 'rb:bg-[url("@/assets/images/workflow/unknown.svg")]' } export const nodeWidth = 240; @@ -702,7 +682,7 @@ const defaultPortGroup = { body: { width: 1, height: 8, - x: -1, + x: 0.75, magnet: true, stroke: port_color, strokeWidth: edge_width, @@ -738,7 +718,7 @@ const leftPortGroup = { body: { width: 1, height: 8, - x: -1, + x: -1.75, y: -4, magnet: true, stroke: port_color, diff --git a/web/src/views/Workflow/index.tsx b/web/src/views/Workflow/index.tsx index b698e857..34ef91a7 100644 --- a/web/src/views/Workflow/index.tsx +++ b/web/src/views/Workflow/index.tsx @@ -68,7 +68,7 @@ const Workflow = forwardRef<WorkflowRef, { onFeaturesLoad?: (features: FeaturesC {/* 右侧画布区域 */} <div - className={clsx(`rb:fixed rb:top-18.5 rb:bottom-2.5 rb:left-0 rb:right-0 rb:transition-all`)} + className={clsx(`rb:fixed rb:top-16 rb:bottom-0 rb:left-0 rb:right-0 rb:transition-all`)} onDrop={onDrop} onDragOver={onDragOver} > From e77a1a92fdabe8d8c3f76dbc104a6746cddcac86 Mon Sep 17 00:00:00 2001 From: zhaoying <zhaoyingyz@126.com> Date: Wed, 1 Apr 2026 13:33:16 +0800 Subject: [PATCH 2/6] feat(web): skill toolList add is_active --- web/src/components/Layout/BasicAuthLayout.tsx | 5 +- .../Skills/components/ToolList/ToolList.tsx | 63 ++++--- .../views/Skills/components/ToolList/types.ts | 1 + web/src/views/Skills/pages/SkillConfig.tsx | 177 ++++++++++-------- 4 files changed, 130 insertions(+), 116 deletions(-) diff --git a/web/src/components/Layout/BasicAuthLayout.tsx b/web/src/components/Layout/BasicAuthLayout.tsx index 2f40ac37..094493c5 100644 --- a/web/src/components/Layout/BasicAuthLayout.tsx +++ b/web/src/components/Layout/BasicAuthLayout.tsx @@ -19,6 +19,7 @@ import { Outlet } from 'react-router-dom'; import { useEffect, type FC } from 'react'; +import { Layout } from 'antd'; import { useUser } from '@/store/user'; @@ -35,10 +36,10 @@ const BasicAuthLayout: FC = () => { }, [getUserInfo]); return ( - <div className="rb:relative rb:min-h-screen rb:w-screen"> + <Layout className="rb:min-h-screen!"> {/* Render child routes without additional UI */} <Outlet /> - </div> + </Layout> ) }; diff --git a/web/src/views/Skills/components/ToolList/ToolList.tsx b/web/src/views/Skills/components/ToolList/ToolList.tsx index 785e0a54..0df4da58 100644 --- a/web/src/views/Skills/components/ToolList/ToolList.tsx +++ b/web/src/views/Skills/components/ToolList/ToolList.tsx @@ -12,7 +12,7 @@ import { type FC, useRef, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Space, Button, List } from 'antd' +import { Space, Button, Flex } from 'antd' import Card from '@/views/ApplicationConfig/components/Card' import type { @@ -22,6 +22,7 @@ import type { import Empty from '@/components/Empty' import ToolModal from './ToolModal' import { getToolMethods, getToolDetail } from '@/api/tools' +import Tag from '@/components/Tag' /** * Tool List Component Props @@ -61,6 +62,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { const mcpFilterItem = (methods as any[]).find(vo => vo.name === item.operation) return { ...item, + is_active: (toolDetail as any).is_active, label: mcpFilterItem?.description, method_id: mcpFilterItem?.method_id, value: mcpFilterItem?.name, @@ -74,6 +76,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { const builtinFilterItem = (methods as any[]).find(vo => vo.name === item.operation) return { ...item, + is_active: (toolDetail as any).is_active, label: builtinFilterItem?.description, method_id: builtinFilterItem?.method_id, value: builtinFilterItem?.name, @@ -84,6 +87,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { // Single method: Use first method return { ...item, + is_active: (toolDetail as any).is_active, label: (methods as any[])[0]?.description, method_id: (methods as any[])[0]?.method_id, value: (methods as any[])[0]?.name, @@ -96,6 +100,7 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { const customFilterItem = (methods as any[]).find(vo => vo.method_id === item.operation) return { ...item, + is_active: (toolDetail as any).is_active, label: customFilterItem?.name, method_id: customFilterItem?.method_id, value: customFilterItem?.name, @@ -129,7 +134,10 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { * @param tool - Tool to add */ const updateTools = (tool: ToolOption) => { - const list = [...toolList, tool] + const list = [...toolList, { + ...tool, + is_active: true, + }] setToolList(list) onChange && onChange(list) } @@ -146,42 +154,35 @@ const ToolList: FC<ToolListProps> = ({value, onChange}) => { } return ( - <Card + <Card title={t('application.toolConfiguration')} extra={ - <Button style={{ padding: '0 8px', height: '24px' }} onClick={handleAddTool}> - + {t('application.addTool')} - </Button> + <Button className="rb:h-6! rb:py-0! rb:px-2! rb:rounded-md! rb:text-[#21233" onClick={handleAddTool}>+ {t('application.addTool')}</Button> } > - {/* Show empty state or tool list */} {toolList.length === 0 - ? <Empty size={88} /> - : - <List - grid={{ gutter: 12, column: 1 }} - dataSource={toolList} - renderItem={(item, index) => ( - <List.Item> - {/* Tool card with delete button */} - <div key={index} className="rb:flex rb:items-center rb:justify-between rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg"> - {/* Tool label/description */} - <div className="rb:font-medium rb:leading-4"> - {item.label} - </div> - <Space size={12}> - {/* Delete button with hover effect */} - <div - className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]" - onClick={() => handleDeleteTool(index)} - ></div> - </Space> + ? <div className="rb-border rb:rounded-xl rb:pt-4 rb:pb-6"><Empty size={88} /></div> + : <Flex vertical gap={12}> + {toolList.map((item, index) => ( + <Flex key={index} align="center" justify="space-between" className="rb:py-2.5! rb:pl-4! rb:pr-3! rb-border rb:rounded-lg"> + <div> + <div className="rb:font-medium rb:leading-4"> + {item.label} </div> - </List.Item> - )} - /> + <Tag color={item.is_active ? 'success' : 'error'} className="rb:mt-1"> + {item.is_active ? t('common.enable') : t('common.deleted')} + </Tag> + </div> + <Space size={12}> + <div + className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]" + onClick={() => handleDeleteTool(index)} + ></div> + </Space> + </Flex> + ))} + </Flex> } - {/* Tool selection modal */} <ToolModal ref={toolModalRef} refresh={updateTools} diff --git a/web/src/views/Skills/components/ToolList/types.ts b/web/src/views/Skills/components/ToolList/types.ts index b0380f6a..4c7d908d 100644 --- a/web/src/views/Skills/components/ToolList/types.ts +++ b/web/src/views/Skills/components/ToolList/types.ts @@ -32,6 +32,7 @@ export interface ToolOption { tool_id?: string; /** Whether tool is enabled */ enabled?: boolean; + is_active?: boolean; } /** diff --git a/web/src/views/Skills/pages/SkillConfig.tsx b/web/src/views/Skills/pages/SkillConfig.tsx index f9f76dea..84c82378 100644 --- a/web/src/views/Skills/pages/SkillConfig.tsx +++ b/web/src/views/Skills/pages/SkillConfig.tsx @@ -7,17 +7,16 @@ import { type FC, useEffect, useRef, useState } from "react"; import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router-dom'; -import { Form, Input, Button, Space, Select, App } from 'antd' +import { Form, Input, Button, Space, Select, App, Flex } from 'antd' import Card from '@/views/ApplicationConfig/components/Card' -import aiPrompt from '@/assets/images/application/aiPrompt.png' import AiPromptModal from '@/views/ApplicationConfig/components/AiPromptModal' import ToolList from '../components/ToolList/ToolList' import type { AiPromptModalRef } from '@/views/ApplicationConfig/types' -import exitIcon from '@/assets/images/knowledgeBase/exit.png'; import type { SkillFormData } from '../types' import { getSkillDetail, createSkill, updateSkill } from '@/api/skill' import { stringRegExp } from '@/utils/validator'; +import PageHeader from '@/components/Layout/PageHeader' /** * Skill Configuration Page Component @@ -43,6 +42,7 @@ const SkillConfig: FC = () => { const { message } = App.useApp() const [loading, setLoading] = useState(false) const [form] = Form.useForm<SkillFormData>(); + const [data, setData] = useState<SkillFormData | null>(null) /** * Effect: Load skill data if editing existing skill @@ -70,6 +70,7 @@ const SkillConfig: FC = () => { getSkillDetail(id) .then(res => { form.setFieldsValue(res as SkillFormData) + setData(res as SkillFormData) }) .finally(() => { setLoading(false) @@ -131,93 +132,103 @@ const SkillConfig: FC = () => { } return ( - <div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto"> - {/* Back button */} - <div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}> - <img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' /> - <span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span> - </div> - - <Form form={form} layout="vertical"> - <Space size={16} direction="vertical" className="rb:w-full"> - {/* Manifest Section: Basic skill information */} - <Card title={t('skills.mainfest')}> - <Form.Item - name="name" - label={t('skills.name')} - rules={[ - { required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) }, - { max: 50 }, - { pattern: stringRegExp, message: t('common.nameInvalid') }, - ]} + <Flex vertical className="rb:h-screen!"> + <PageHeader + title={data?.name} + extra={ + <Flex gap={12} align="center"> + {/* Save button */} + <Button type="primary" className="rb:px-2! rb:gap-0.5!" disabled={loading} onClick={handleSave}>{t('skills.save')}</Button> + <Button + className="rb:px-2! rb:gap-0.5!" + icon={<div className="rb:bg-[url('@/assets/images/workflow/return.svg')] rb:size-4 rb:bg-cover"></div>} + onClick={handleBack} > - <Input placeholder={t('common.pleaseEnter')} /> - </Form.Item> - <Form.Item - name="description" - label={t('skills.description')} - rules={[{ max: 500 }]} - > - <Input.TextArea placeholder={t('skills.descriptionPlaceholder')} /> - </Form.Item> - <Form.Item - name={['config', 'keywords']} - label={t('skills.keywords')} - rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.keywords') }) }]} - > - <Select - mode="tags" - placeholder={t('common.pleaseEnter')} - /> - </Form.Item> - </Card> + {t('common.return')} + </Button> + </Flex> + } + /> + <div className="rb:w-250 rb:my-3 rb:mx-auto rb:flex-1 rb:overflow-y-auto"> + <Form form={form} layout="vertical"> + <Space size={16} direction="vertical" className="rb:w-full"> + {/* Manifest Section: Basic skill information */} + <Card title={t('skills.mainfest')}> + <Form.Item + name="name" + label={t('skills.name')} + rules={[ + { required: true, message: t('common.inputPlaceholder', { title: t('skills.name') }) }, + { max: 50 }, + { pattern: stringRegExp, message: t('common.nameInvalid') }, + ]} + > + <Input placeholder={t('common.pleaseEnter')} /> + </Form.Item> + <Form.Item + name="description" + label={t('skills.description')} + rules={[{ max: 500 }]} + > + <Input.TextArea placeholder={t('skills.descriptionPlaceholder')} /> + </Form.Item> + <Form.Item + name={['config', 'keywords']} + label={t('skills.keywords')} + rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('skills.keywords') }) }]} + > + <Select + mode="tags" + placeholder={t('common.pleaseEnter')} + /> + </Form.Item> + </Card> - {/* Prompt Configuration Section: AI instructions */} - <Card title={t('skills.promptConfiguration')} - extra={ - <Button style={{ padding: '0 8px', height: '24px' }} onClick={handlePrompt}> - <img src={aiPrompt} className="rb:size-5" /> - {t('skills.aiPrompt')} - </Button> - } - > + {/* Prompt Configuration Section: AI instructions */} + <Card title={t('skills.promptConfiguration')} + extra={ + <Button style={{ padding: '0 8px', height: '24px' }} onClick={handlePrompt}> + <div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/application/aiPrompt.png')] rb:mr-1!" /> + {t('skills.aiPrompt')} + </Button> + } + > + <Form.Item + name="prompt" + className="rb:mb-0!" + > + <Input.TextArea + placeholder={t('skills.promptPlaceholder')} + styles={{ + textarea: { + minHeight: '200px', + borderRadius: '8px' + }, + }} + /> + </Form.Item> + </Card> + + {/* Tool Configuration Section */} <Form.Item - name="prompt" + name="tools" + rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('skills.tools') }) }]} className="rb:mb-0!" > - <Input.TextArea - placeholder={t('skills.promptPlaceholder')} - styles={{ - textarea: { - minHeight: '200px', - borderRadius: '8px' - }, - }} - /> + <ToolList /> </Form.Item> - </Card> - {/* Tool Configuration Section */} - <Form.Item - name="tools" - rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('skills.tools') }) }]} - className="rb:mb-0!" - > - <ToolList /> - </Form.Item> - - {/* Save button */} - <Button type="primary" block disabled={loading} onClick={handleSave}>{t('skills.save')}</Button> - </Space> - </Form> - - {/* AI Prompt Generation Modal */} - <AiPromptModal - ref={aiPromptModalRef} - refresh={updatePrompt} - source="skills" - /> - </div> + </Space> + </Form> + + {/* AI Prompt Generation Modal */} + <AiPromptModal + ref={aiPromptModalRef} + refresh={updatePrompt} + source="skills" + /> + </div> + </Flex> ) } From ad4ddea97759b87ee11fd67fa525cb8d504d43e2 Mon Sep 17 00:00:00 2001 From: zhaoying <zhaoyingyz@126.com> Date: Wed, 1 Apr 2026 16:43:45 +0800 Subject: [PATCH 3/6] feat(web): ui upgrade --- .../assets/images/conversation/compress.svg | 18 ++ web/src/assets/images/conversation/expand.svg | 15 ++ web/src/components/BtnTabs/index.tsx | 8 +- web/src/components/Chat/AudioPlayer.tsx | 152 +++++++++++++ web/src/components/Chat/ChatContent.tsx | 180 +++++++++------- web/src/components/Chat/VideoPlayer.tsx | 62 ++++++ web/src/components/Markdown/index.tsx | 11 +- web/src/styles/antdThemeConfig.ts | 3 + web/src/styles/index.css | 11 + web/src/views/OrderHistory/index.tsx | 50 +++-- web/src/views/SelfReflectionEngine/index.tsx | 199 ++++++++++-------- .../UserMemoryDetail/pages/ExplicitDetail.tsx | 98 +++++++-- .../UserMemoryDetail/pages/WorkingDetail.tsx | 2 +- 13 files changed, 590 insertions(+), 219 deletions(-) create mode 100644 web/src/assets/images/conversation/compress.svg create mode 100644 web/src/assets/images/conversation/expand.svg create mode 100644 web/src/components/Chat/AudioPlayer.tsx create mode 100644 web/src/components/Chat/VideoPlayer.tsx diff --git a/web/src/assets/images/conversation/compress.svg b/web/src/assets/images/conversation/compress.svg new file mode 100644 index 00000000..640d80ba --- /dev/null +++ b/web/src/assets/images/conversation/compress.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <title>编组 35 + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/conversation/expand.svg b/web/src/assets/images/conversation/expand.svg new file mode 100644 index 00000000..8cc87d99 --- /dev/null +++ b/web/src/assets/images/conversation/expand.svg @@ -0,0 +1,15 @@ + + + 编组 36 + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/components/BtnTabs/index.tsx b/web/src/components/BtnTabs/index.tsx index 772a4c8d..8a6e670b 100644 --- a/web/src/components/BtnTabs/index.tsx +++ b/web/src/components/BtnTabs/index.tsx @@ -24,10 +24,11 @@ interface BtnTabsProps { onChange: (key: string) => void; /** Optional extra class name for the container */ className?: string; + variant?: 'outline' | 'borderless' } /** Button-style tab switcher — renders tabs as pill-shaped buttons with active highlight */ -const BtnTabs: FC = ({ items, activeKey, onChange, className }) => { +const BtnTabs: FC = ({ items, activeKey, onChange, className, variant = 'borderless' }) => { return ( {items.map((tab) => ( @@ -35,8 +36,9 @@ const BtnTabs: FC = ({ items, activeKey, onChange, className }) => key={tab.key} onClick={() => onChange(tab.key)} className={clsx('rb:px-2 rb:py-1 rb:rounded-[13px] rb:text-[12px] rb:leading-4.5 rb:cursor-pointer', { - 'rb:bg-[#F6F6F6]': activeKey !== tab.key, - 'rb:bg-[#171719] rb:text-white': activeKey === tab.key, + 'rb:bg-[#F6F6F6]': activeKey !== tab.key && variant === 'borderless', + 'rb-border rb:bg-white': activeKey !== tab.key && variant === 'outline', + 'rb:bg-[#171719] rb:text-white rb:border-[#171719]': activeKey === tab.key, })} > {tab.label} diff --git a/web/src/components/Chat/AudioPlayer.tsx b/web/src/components/Chat/AudioPlayer.tsx new file mode 100644 index 00000000..766c8deb --- /dev/null +++ b/web/src/components/Chat/AudioPlayer.tsx @@ -0,0 +1,152 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-03-16 15:00:07 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-27 15:23:14 + */ +import { type FC, useRef, useState, useEffect } from 'react' +import { Flex, Dropdown, type MenuProps, Slider } from 'antd' +import clsx from 'clsx' +import { useTranslation } from 'react-i18next' + +/** Available playback speed options. */ +const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] + +/** Format seconds into "MM:SS" display string. */ +const fmt = (s: number) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}` + +/** + * Props for the AudioPlayer component. + * @property src - Audio file URL to play. + * @property fileName - Display name shown beside the file icon. + * @property fileSize - Human-readable file size string (e.g. "3.2 MB"). + */ +interface AudioPlayerProps { + src: string + fileName?: string + fileSize?: string +} + +/** + * AudioPlayer – A compact inline audio player with playback controls. + * + * Displays file metadata (name & size), a play/pause toggle, a seekable + * progress slider, elapsed/total time, and a dropdown menu for downloading + * the file or changing playback speed. + * + * @example + * + */ +const AudioPlayer: FC = ({ src, fileName, fileSize }) => { + const { t } = useTranslation() + const audioRef = useRef(null) + const [playing, setPlaying] = useState(false) + const [current, setCurrent] = useState(0) + const [duration, setDuration] = useState(0) + const [speed, setSpeed] = useState(1) + + /* Bind native audio events to sync React state; re-binds when src changes. */ + useEffect(() => { + const audio = audioRef.current + if (!audio) return + const onTime = () => setCurrent(audio.currentTime) + const onMeta = () => setDuration(audio.duration) + const onEnd = () => setPlaying(false) + audio.addEventListener('timeupdate', onTime) + audio.addEventListener('loadedmetadata', onMeta) + audio.addEventListener('ended', onEnd) + return () => { + audio.removeEventListener('timeupdate', onTime) + audio.removeEventListener('loadedmetadata', onMeta) + audio.removeEventListener('ended', onEnd) + } + }, [src]) + + /** Toggle between play and pause. */ + const togglePlay = () => { + const audio = audioRef.current + if (!audio) return + if (playing) { audio.pause(); setPlaying(false) } + else { audio.play(); setPlaying(true) } + } + + /** Seek to a specific position (in seconds) on the audio timeline. */ + const handleSeek = (val: number) => { + if (audioRef.current) audioRef.current.currentTime = val + setCurrent(val) + } + + /** Update playback speed on both React state and the native audio element. */ + const setPlaybackSpeed = (s: number) => { + setSpeed(s) + if (audioRef.current) audioRef.current.playbackRate = s + } + + /** Open the audio source URL in a new tab to trigger download. */ + const handleDownload = () => window.open(src, '_blank') + + /** Dropdown menu items: download and playback speed sub-menu. */ + const mainMenu: MenuProps = { + items: [ + { + key: 'download', + icon:
, + label: t('common.download'), + onClick: handleDownload, + }, + { + key: 'speed', + icon:
, + label: t('perceptualDetail.playbackSpeed'), + children: SPEEDS.map(s => ({ + key: String(s), + label: {s === 1 ? 'normal' : s}, + onClick: () => setPlaybackSpeed(s), + })), + }, + ], + } + + return ( +
+