chore: frontend
parent
9d88021575
commit
ec9eb03ae1
@ -0,0 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import WorkflowRunContainer from '@/app/components/run'
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full overflow-x-auto'>
|
||||||
|
<WorkflowRunContainer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Page
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
export const anormalTypesOptions = [
|
||||||
|
{
|
||||||
|
value: 'unknown',
|
||||||
|
key: 0,
|
||||||
|
label: '未知',
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'app',
|
||||||
|
key: 1,
|
||||||
|
label: '应用接口异常类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'container',
|
||||||
|
key: 2,
|
||||||
|
label: '容器异常类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'infra',
|
||||||
|
key: 3,
|
||||||
|
label: '基础设施异常类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'network',
|
||||||
|
key: 4,
|
||||||
|
label: '网络异常类型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'error',
|
||||||
|
key: 5,
|
||||||
|
label: 'JAVA Exception',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'appInstance',
|
||||||
|
key: 6,
|
||||||
|
label: '应用实例异常类型',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
export const anormalTypes = ['app', 'container', 'infra', 'network', 'error', 'appInstance']
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
function escapeId(id: string) {
|
||||||
|
return id.replace(/[^a-zA-Z0-9-_]/g, '_')
|
||||||
|
}
|
||||||
|
function contactServiceEndpoint(service: string, endpoint: string) {
|
||||||
|
return escapeId(`${service}-${endpoint}`)
|
||||||
|
}
|
||||||
|
export const usePrepareTopologyData = (data) => {
|
||||||
|
console.log(data)
|
||||||
|
if (!data)
|
||||||
|
return { nodes: [], edges: [] }
|
||||||
|
|
||||||
|
const nodeSet = new Set() // 用于存储已添加节点的 ID
|
||||||
|
const edgeSet = new Set() // 用于存储已添加边的 ID
|
||||||
|
const current = data.current
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: contactServiceEndpoint(current.service, current.endpoint),
|
||||||
|
data: {
|
||||||
|
label: current.service,
|
||||||
|
isTraced: current.isTraced,
|
||||||
|
service: current.service,
|
||||||
|
endpoint: current.endpoint,
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'serviceNode',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
nodeSet.add(contactServiceEndpoint(current.service, current.endpoint))
|
||||||
|
const edges = []
|
||||||
|
data.parents?.forEach((parent) => {
|
||||||
|
const nodeId = contactServiceEndpoint(parent.service, parent.endpoint)
|
||||||
|
if (!nodeSet.has(nodeId)) {
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
data: {
|
||||||
|
label: parent.service,
|
||||||
|
isTraced: parent.isTraced,
|
||||||
|
service: parent.service,
|
||||||
|
endpoint: parent.endpoint,
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'serviceNode',
|
||||||
|
})
|
||||||
|
nodeSet.add(nodeId)
|
||||||
|
}
|
||||||
|
const edgeId = `${nodeId}-${contactServiceEndpoint(current.service, current.endpoint)}`
|
||||||
|
if (!edgeSet.has(edgeId)) {
|
||||||
|
const targetId = contactServiceEndpoint(current.service, current.endpoint)
|
||||||
|
edges.push({
|
||||||
|
id: edgeId,
|
||||||
|
source: nodeId,
|
||||||
|
target: targetId,
|
||||||
|
type: nodeId === targetId ? 'loop' : 'smart',
|
||||||
|
markerEnd: 'url(#arrowhead)',
|
||||||
|
})
|
||||||
|
edgeSet.add(edgeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
data.childRelations?.forEach((child, index) => {
|
||||||
|
const nodeId = contactServiceEndpoint(child.service, child.endpoint)
|
||||||
|
if (!nodeSet.has(nodeId)) {
|
||||||
|
nodes.push({
|
||||||
|
id: nodeId,
|
||||||
|
data: {
|
||||||
|
label: child.service,
|
||||||
|
isTraced: child.isTraced,
|
||||||
|
service: child.service,
|
||||||
|
endpoint: child.endpoint,
|
||||||
|
},
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
type: 'serviceNode',
|
||||||
|
})
|
||||||
|
nodeSet.add(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeId
|
||||||
|
= `${contactServiceEndpoint(child.parentService, child.parentEndpoint)}-${nodeId}`
|
||||||
|
if (!edgeSet.has(edgeId)) {
|
||||||
|
const sourceId = contactServiceEndpoint(child.parentService, child.parentEndpoint)
|
||||||
|
edges.push({
|
||||||
|
id: edgeId,
|
||||||
|
source: sourceId,
|
||||||
|
target: nodeId,
|
||||||
|
type: nodeId === sourceId ? 'loop' : 'smart',
|
||||||
|
})
|
||||||
|
edgeSet.add(edgeId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const resultNodes = [...nodes]
|
||||||
|
const result = resultNodes.reduce((acc, curr) => {
|
||||||
|
acc[curr.id] = 1
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
console.log(result)
|
||||||
|
// prepareDataForChartAndTopologyNode(result)
|
||||||
|
return { nodes: resultNodes, edges }
|
||||||
|
}
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
import ReactFlow, {
|
||||||
|
Background,
|
||||||
|
MarkerType,
|
||||||
|
ReactFlowProvider,
|
||||||
|
useEdgesState,
|
||||||
|
useNodesState,
|
||||||
|
useReactFlow,
|
||||||
|
} from 'reactflow'
|
||||||
|
import 'reactflow/dist/style.css'
|
||||||
|
import './index.css'
|
||||||
|
import ServiceNode from './service-node'
|
||||||
|
import CustomSelfLoopEdge from './loop-edges'
|
||||||
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { layout as dagreLayout, graphlib } from '@dagrejs/dagre'
|
||||||
|
const nodeWidth = 200
|
||||||
|
const defaultNodeTypes = {
|
||||||
|
serviceNode: ServiceNode,
|
||||||
|
// moreNode: MoreNode,
|
||||||
|
} // 定义在组件外部
|
||||||
|
const edgeTypes = {
|
||||||
|
// smart: SmartBezierEdge, // 或者使用 SmartBezierEdge 等
|
||||||
|
loop: CustomSelfLoopEdge,
|
||||||
|
}
|
||||||
|
const LayoutFlow = (props) => {
|
||||||
|
const { data, nodeHeight = 60, nodeTypes = {} } = props
|
||||||
|
// 所有链路
|
||||||
|
const reactFlowInstance = useRef(null)
|
||||||
|
// const [initialNodes, setInitialNodes] = useState([])
|
||||||
|
// const [initialEdges, setInitialEdges] = useState([])
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||||
|
|
||||||
|
const { fitView } = useReactFlow()
|
||||||
|
const markerEnd = {
|
||||||
|
type: MarkerType.ArrowClosed,
|
||||||
|
strokeWidth: 5,
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
color: '#6293ff',
|
||||||
|
}
|
||||||
|
const prepareData = () => {
|
||||||
|
const initialNodes = data?.nodes || []
|
||||||
|
const initialEdges = []
|
||||||
|
data.edges.forEach((edge) => {
|
||||||
|
initialEdges.push({
|
||||||
|
...edge,
|
||||||
|
markerEnd,
|
||||||
|
style: {
|
||||||
|
stroke: '#6293FF',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return { initialNodes, initialEdges }
|
||||||
|
}
|
||||||
|
|
||||||
|
const dagreGraph = new graphlib.Graph()
|
||||||
|
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||||
|
|
||||||
|
const getLayoutedElements = (nodes, edges) => {
|
||||||
|
dagreGraph.setGraph({ rankdir: 'LR', ranksep: 100, nodesep: 50 }) // 自上而下的布局
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight })
|
||||||
|
})
|
||||||
|
|
||||||
|
edges.forEach((edge) => {
|
||||||
|
dagreGraph.setEdge(edge.source, edge.target)
|
||||||
|
})
|
||||||
|
|
||||||
|
dagreLayout(dagreGraph)
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const nodeWithPosition = dagreGraph.node(node.id)
|
||||||
|
node.targetPosition = 'left'
|
||||||
|
node.sourcePosition = 'right'
|
||||||
|
|
||||||
|
node.position = {
|
||||||
|
x: nodeWithPosition.x - nodeWidth,
|
||||||
|
y: nodeWithPosition.y - nodeHeight / 2,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Calculate offsets to center the graph
|
||||||
|
const xMin = Math.min(...nodes.map(node => node.position.x))
|
||||||
|
const yMin = Math.min(...nodes.map(node => node.position.y))
|
||||||
|
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
node.position.x -= xMin - nodeWidth / 2
|
||||||
|
node.position.y -= yMin - nodeHeight / 2
|
||||||
|
})
|
||||||
|
edges.map((edge) => {
|
||||||
|
const sourceNode = nodes.find(node => node.id === edge.source)
|
||||||
|
const targetNode = nodes.find(node => node.id === edge.target)
|
||||||
|
if (
|
||||||
|
sourceNode.position.x > targetNode.position.x
|
||||||
|
&& Math.abs(sourceNode.position.y - targetNode.position.y) < nodeHeight
|
||||||
|
)
|
||||||
|
edge.type = 'loop'
|
||||||
|
})
|
||||||
|
return { nodes, edges }
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(data)
|
||||||
|
const { initialNodes, initialEdges } = prepareData()
|
||||||
|
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
|
||||||
|
initialNodes,
|
||||||
|
initialEdges,
|
||||||
|
)
|
||||||
|
setNodes([...layoutedNodes])
|
||||||
|
setEdges([...layoutedEdges])
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (reactFlowInstance.current) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
padding: layoutedNodes.length > 2 ? 0.1 : 0.2,
|
||||||
|
includeHiddenNodes: true,
|
||||||
|
})
|
||||||
|
}, 20)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [data])
|
||||||
|
const memoNodeTypes = useMemo(() => ({ ...nodeTypes, ...defaultNodeTypes }), [])
|
||||||
|
return (
|
||||||
|
|
||||||
|
<ReactFlow
|
||||||
|
fitView
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={memoNodeTypes}
|
||||||
|
ref={reactFlowInstance}
|
||||||
|
minZoom={0.1} // 设置最小缩放
|
||||||
|
maxZoom={2} // 设置最大缩放
|
||||||
|
>
|
||||||
|
<Background
|
||||||
|
gap={[14, 14]}
|
||||||
|
size={2}
|
||||||
|
className="bg-workflow-canvas-workflow-bg"
|
||||||
|
color='var(--color-workflow-canvas-workflow-dot-color)'
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
function FlowWithProvider(props) {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<svg style={{ position: 'absolute', top: 0, left: 0 }}>
|
||||||
|
<defs>
|
||||||
|
<marker
|
||||||
|
id="arrowhead"
|
||||||
|
viewBox="0 0 74.4539794921875 67"
|
||||||
|
refX="37.227"
|
||||||
|
refY="33.5"
|
||||||
|
markerWidth="16"
|
||||||
|
markerHeight="16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M45.4542 4.75L73.167 52.75C76.8236 59.0833 72.2529 67 64.9398 67L9.51418 67C2.20107 67 -2.36962 59.0833 1.28693 52.75L28.9997 4.75C32.6563 -1.58334 41.7977 -1.58334 45.4542 4.75Z"
|
||||||
|
fill="#6293ff"
|
||||||
|
/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<LayoutFlow {...props} />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default FlowWithProvider
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
const nodeWidth = 180
|
||||||
|
const nodeHeight = 60
|
||||||
|
const CustomSelfLoopEdge = ({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
style = {},
|
||||||
|
markerEndId,
|
||||||
|
}) => {
|
||||||
|
// 手动设置控制点,形成 3/4 圆的路径
|
||||||
|
const diff = Math.max(Math.trunc((sourceX - targetX) / nodeWidth), 1)
|
||||||
|
const controlX1 = sourceX + (sourceX - targetX) // 控制点1的位置
|
||||||
|
const controlY1 = sourceY - diff * nodeHeight * 2
|
||||||
|
const controlX2 = sourceX - (sourceX - targetX) * 2 // 控制点2的位置
|
||||||
|
const controlY2 = sourceY - diff * nodeHeight * 2
|
||||||
|
|
||||||
|
const edgePath = `M${sourceX},${sourceY} C${controlX1},${controlY1} ${controlX2},${controlY2} ${targetX},${targetY}`
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<path
|
||||||
|
id={id}
|
||||||
|
style={style}
|
||||||
|
className="react-flow__edge-path loop"
|
||||||
|
d={edgePath}
|
||||||
|
markerEnd="url(#arrowhead)"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomSelfLoopEdge
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { Handle, Position } from 'reactflow'
|
||||||
|
const handleStyle = { left: 10 }
|
||||||
|
const ServiceNode = React.memo(({ data, isConnectable }) => {
|
||||||
|
const onChange = useCallback((evt) => {}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-[15px]">
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="invisible"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="shadow-xs border-[2px] border-transparen w-[200px] min-h-[60px] p-2 rounded-[15px] overflow-hidden bg-[#fcfdff]/80"
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0">
|
||||||
|
</div>
|
||||||
|
<div className="text-center text-lg pt-2 px-2">
|
||||||
|
{data.label}
|
||||||
|
<div className="text-xs text-gray-500 text-left break-all">{data.endpoint}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
id="b"
|
||||||
|
isConnectable={isConnectable}
|
||||||
|
className="invisible"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ServiceNode
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import TextGeneration from '@/app/components/run/text-generation'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
|
import { fetchInstalledAppList } from '@/service/explore'
|
||||||
|
import type { InstalledApp } from '@/models/explore'
|
||||||
|
import useSWR from 'swr'
|
||||||
|
import { fetchApiKeysList } from '@/service/apps'
|
||||||
|
const TextGenerationApp = ({ installedApp }) => {
|
||||||
|
const { data: apiKeysList } = useSWR({ url: `/apps/${installedApp.app.id}/api-keys`, params: {} }, fetchApiKeysList)
|
||||||
|
return apiKeysList?.data?.length > 0 && <TextGeneration isWorkflow isInstalledApp installedAppInfo={installedApp} apiKey={apiKeysList.data[0].token}/>
|
||||||
|
}
|
||||||
|
const WorkflowRunContainer = () => {
|
||||||
|
const appDetail = useAppStore(state => state.appDetail)!
|
||||||
|
const [installedApp, setInstalledApp] = useState<InstalledApp>()
|
||||||
|
|
||||||
|
const getAppInfo = async () => {
|
||||||
|
const { installed_apps }: any = await fetchInstalledAppList(appDetail.id) || {}
|
||||||
|
setInstalledApp(installed_apps?.length > 0 ? installed_apps[0] : null)
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
getAppInfo()
|
||||||
|
}, [appDetail.id])
|
||||||
|
if (!installedApp) {
|
||||||
|
return (
|
||||||
|
<div className='flex h-full items-center'>
|
||||||
|
<Loading type='area' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-full py-2 pl-0 pr-2 sm:p-2'>
|
||||||
|
{installedApp.app.mode === 'workflow' && (
|
||||||
|
<TextGenerationApp installedApp={installedApp}/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(WorkflowRunContainer)
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Markdown } from '@/app/components/base/markdown'
|
||||||
|
type LLMOutputsProps = {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
const LLMOutputs = ({ text }: LLMOutputsProps) => {
|
||||||
|
return (
|
||||||
|
<div className="w-full overflow-hidden p-3">
|
||||||
|
<div className='px-4 py-3 bg-[#D1E9FF]/50 rounded-2xl text-sm text-gray-900'>
|
||||||
|
<Markdown content={text || ''} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default memo(LLMOutputs)
|
||||||
@ -0,0 +1,233 @@
|
|||||||
|
'use client'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
RiAlertFill,
|
||||||
|
RiArrowRightSLine,
|
||||||
|
RiCheckboxCircleFill,
|
||||||
|
RiErrorWarningLine,
|
||||||
|
RiLoader2Line,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import StatusContainer from '@/app/components/workflow/run/status-container'
|
||||||
|
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||||
|
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||||
|
import type {
|
||||||
|
AgentLogItemWithChildren,
|
||||||
|
IterationDurationMap,
|
||||||
|
NodeTracing,
|
||||||
|
} from '@/types/workflow'
|
||||||
|
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
|
||||||
|
import { hasRetryNode } from '@/app/components/workflow/utils'
|
||||||
|
import { BlockEnum } from '../../workflow/types'
|
||||||
|
import BlockIcon from '../../workflow/block-icon'
|
||||||
|
import { IterationLogTrigger } from '../../workflow/run/iteration-log'
|
||||||
|
import { RetryLogTrigger } from '../../workflow/run/retry-log'
|
||||||
|
import { AgentLogTrigger } from '../../workflow/run/agent-log'
|
||||||
|
import DataDisplay from '../../workflow/run/data-display'
|
||||||
|
import LLMOutputs from './llm-outputs'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
nodeInfo: NodeTracing
|
||||||
|
inMessage?: boolean
|
||||||
|
hideInfo?: boolean
|
||||||
|
hideProcessDetail?: boolean
|
||||||
|
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
|
||||||
|
onShowRetryDetail?: (detail: NodeTracing[]) => void
|
||||||
|
onShowAgentOrToolLog?: (detail?: AgentLogItemWithChildren) => void
|
||||||
|
notShowIterationNav?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodePanel: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
nodeInfo,
|
||||||
|
inMessage = false,
|
||||||
|
hideInfo = false,
|
||||||
|
hideProcessDetail,
|
||||||
|
onShowIterationDetail,
|
||||||
|
onShowRetryDetail,
|
||||||
|
onShowAgentOrToolLog,
|
||||||
|
notShowIterationNav,
|
||||||
|
}) => {
|
||||||
|
const [collapseState, doSetCollapseState] = useState<boolean>(true)
|
||||||
|
const setCollapseState = useCallback((state: boolean) => {
|
||||||
|
if (hideProcessDetail)
|
||||||
|
return
|
||||||
|
doSetCollapseState(state)
|
||||||
|
}, [hideProcessDetail])
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const getTime = (time: number) => {
|
||||||
|
if (time < 1)
|
||||||
|
return `${(time * 1000).toFixed(3)} ms`
|
||||||
|
if (time > 60)
|
||||||
|
return `${Number.parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
|
||||||
|
return `${time.toFixed(3)} s`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTokenCount = (tokens: number) => {
|
||||||
|
if (tokens < 1000)
|
||||||
|
return tokens
|
||||||
|
if (tokens >= 1000 && tokens < 1000000)
|
||||||
|
return `${Number.parseFloat((tokens / 1000).toFixed(3))}K`
|
||||||
|
if (tokens >= 1000000)
|
||||||
|
return `${Number.parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollapseState(!nodeInfo.expand)
|
||||||
|
}, [nodeInfo.expand, setCollapseState])
|
||||||
|
|
||||||
|
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration && !!nodeInfo.details?.length
|
||||||
|
const isRetryNode = hasRetryNode(nodeInfo.node_type) && !!nodeInfo.retryDetail?.length
|
||||||
|
const isAgentNode = nodeInfo.node_type === BlockEnum.Agent && !!nodeInfo.agentLog?.length
|
||||||
|
const isToolNode = nodeInfo.node_type === BlockEnum.Tool && !!nodeInfo.agentLog?.length
|
||||||
|
console.log(nodeInfo)
|
||||||
|
return (
|
||||||
|
<div className={cn('px-2 py-1', className)}>
|
||||||
|
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center pl-1 pr-3 cursor-pointer',
|
||||||
|
hideInfo ? 'py-2' : 'py-1.5',
|
||||||
|
!collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
|
||||||
|
)}
|
||||||
|
onClick={() => setCollapseState(!collapseState)}
|
||||||
|
>
|
||||||
|
{!hideProcessDetail && (
|
||||||
|
<RiArrowRightSLine
|
||||||
|
className={cn(
|
||||||
|
'shrink-0 w-4 h-4 mr-1 text-text-quaternary transition-all group-hover:text-text-tertiary',
|
||||||
|
!collapseState && 'rotate-90',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<BlockIcon size={inMessage ? 'xs' : 'sm'} className={cn('shrink-0 mr-2', inMessage && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||||
|
<div className={cn(
|
||||||
|
'grow text-text-secondary system-xs-semibold-uppercase truncate',
|
||||||
|
hideInfo && '!text-xs',
|
||||||
|
)} title={nodeInfo.title}>{nodeInfo.title}</div>
|
||||||
|
{nodeInfo.status !== 'running' && !hideInfo && (
|
||||||
|
<div className='shrink-0 text-text-tertiary system-xs-regular'>{nodeInfo.execution_metadata?.total_tokens ? `${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens · ` : ''}{`${getTime(nodeInfo.elapsed_time || 0)}`}</div>
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'succeeded' && (
|
||||||
|
<RiCheckboxCircleFill className='shrink-0 ml-2 w-3.5 h-3.5 text-text-success' />
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'failed' && (
|
||||||
|
<RiErrorWarningLine className='shrink-0 ml-2 w-3.5 h-3.5 text-text-warning' />
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'stopped' && (
|
||||||
|
<RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'exception' && (
|
||||||
|
<RiAlertFill className={cn('shrink-0 ml-2 w-4 h-4 text-text-warning-secondary', inMessage && 'w-3.5 h-3.5')} />
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'running' && (
|
||||||
|
<div className='shrink-0 flex items-center text-text-accent text-[13px] leading-[16px] font-medium'>
|
||||||
|
<span className='mr-2 text-xs font-normal'>Running</span>
|
||||||
|
<RiLoader2Line className='w-3.5 h-3.5 animate-spin' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
nodeInfo.node_type === BlockEnum.LLM && nodeInfo.outputs?.text && <LLMOutputs text={nodeInfo.outputs?.text} />
|
||||||
|
}
|
||||||
|
{nodeInfo.node_type === BlockEnum.Tool && (nodeInfo.outputs?.text)
|
||||||
|
&& <div className='px-4 py-2'><DataDisplay data={nodeInfo.outputs.text} /></div>}
|
||||||
|
{!collapseState && !hideProcessDetail && (
|
||||||
|
<div className='px-1 pb-1'>
|
||||||
|
{/* The nav to the iteration detail */}
|
||||||
|
{isIterationNode && !notShowIterationNav && onShowIterationDetail && (
|
||||||
|
<IterationLogTrigger
|
||||||
|
nodeInfo={nodeInfo}
|
||||||
|
onShowIterationResultList={onShowIterationDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isRetryNode && onShowRetryDetail && (
|
||||||
|
<RetryLogTrigger
|
||||||
|
nodeInfo={nodeInfo}
|
||||||
|
onShowRetryResultList={onShowRetryDetail}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
(isAgentNode || isToolNode) && onShowAgentOrToolLog && (
|
||||||
|
<AgentLogTrigger
|
||||||
|
nodeInfo={nodeInfo}
|
||||||
|
onShowAgentOrToolLog={onShowAgentOrToolLog}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
|
||||||
|
{(nodeInfo.status === 'stopped') && (
|
||||||
|
<StatusContainer status='stopped'>
|
||||||
|
{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}
|
||||||
|
</StatusContainer>
|
||||||
|
)}
|
||||||
|
{(nodeInfo.status === 'exception') && (
|
||||||
|
<StatusContainer status='stopped'>
|
||||||
|
{nodeInfo.error}
|
||||||
|
<a
|
||||||
|
href='https://docs.dify.ai/guides/workflow/error-handling/error-type'
|
||||||
|
target='_blank'
|
||||||
|
className='text-text-accent'
|
||||||
|
>
|
||||||
|
{t('workflow.common.learnMore')}
|
||||||
|
</a>
|
||||||
|
</StatusContainer>
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'failed' && (
|
||||||
|
<StatusContainer status='failed'>
|
||||||
|
{nodeInfo.error}
|
||||||
|
</StatusContainer>
|
||||||
|
)}
|
||||||
|
{nodeInfo.status === 'retry' && (
|
||||||
|
<StatusContainer status='failed'>
|
||||||
|
{nodeInfo.error}
|
||||||
|
</StatusContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{nodeInfo.inputs && (
|
||||||
|
<div className={cn('mb-1')}>
|
||||||
|
<CodeEditor
|
||||||
|
readOnly
|
||||||
|
title={<div>{t('workflow.common.input').toLocaleUpperCase()}</div>}
|
||||||
|
language={CodeLanguage.json}
|
||||||
|
value={nodeInfo.inputs}
|
||||||
|
isJSONStringifyBeauty
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nodeInfo.process_data && (
|
||||||
|
<div className={cn('mb-1')}>
|
||||||
|
<CodeEditor
|
||||||
|
readOnly
|
||||||
|
title={<div>{t('workflow.common.processData').toLocaleUpperCase()}</div>}
|
||||||
|
language={CodeLanguage.json}
|
||||||
|
value={nodeInfo.process_data}
|
||||||
|
isJSONStringifyBeauty
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nodeInfo.outputs && (
|
||||||
|
<div>
|
||||||
|
<CodeEditor
|
||||||
|
readOnly
|
||||||
|
title={<div>{t('workflow.common.output').toLocaleUpperCase()}</div>}
|
||||||
|
language={CodeLanguage.json}
|
||||||
|
value={nodeInfo.outputs}
|
||||||
|
isJSONStringifyBeauty
|
||||||
|
tip={<ErrorHandleTip type={nodeInfo.execution_metadata?.error_strategy} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NodePanel
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.66699 1.33366C3.66699 0.965469 3.36852 0.666992 3.00033 0.666992C2.63214 0.666992 2.33366 0.965469 2.33366 1.33366V2.33366H1.33366C0.965469 2.33366 0.666992 2.63214 0.666992 3.00033C0.666992 3.36852 0.965469 3.66699 1.33366 3.66699H2.33366V4.66699C2.33366 5.03518 2.63214 5.33366 3.00033 5.33366C3.36852 5.33366 3.66699 5.03518 3.66699 4.66699V3.66699H4.66699C5.03518 3.66699 5.33366 3.36852 5.33366 3.00033C5.33366 2.63214 5.03518 2.33366 4.66699 2.33366H3.66699V1.33366Z" fill="#444CE7"/>
|
||||||
|
<path d="M3.66699 11.3337C3.66699 10.9655 3.36852 10.667 3.00033 10.667C2.63214 10.667 2.33366 10.9655 2.33366 11.3337V12.3337H1.33366C0.965469 12.3337 0.666992 12.6321 0.666992 13.0003C0.666992 13.3685 0.965469 13.667 1.33366 13.667H2.33366V14.667C2.33366 15.0352 2.63214 15.3337 3.00033 15.3337C3.36852 15.3337 3.66699 15.0352 3.66699 14.667V13.667H4.66699C5.03518 13.667 5.33366 13.3685 5.33366 13.0003C5.33366 12.6321 5.03518 12.3337 4.66699 12.3337H3.66699V11.3337Z" fill="#444CE7"/>
|
||||||
|
<path d="M9.28922 1.76101C9.1902 1.50354 8.94284 1.33366 8.66699 1.33366C8.39114 1.33366 8.14378 1.50354 8.04476 1.76101L6.88864 4.76691C6.68837 5.28761 6.62544 5.43766 6.53936 5.55872C6.45299 5.68019 6.34686 5.78632 6.22539 5.87269C6.10432 5.95878 5.95428 6.02171 5.43358 6.22198L2.42767 7.37809C2.17021 7.47712 2.00033 7.72448 2.00033 8.00033C2.00033 8.27617 2.17021 8.52353 2.42767 8.62256L5.43358 9.77867C5.95428 9.97894 6.10432 10.0419 6.22539 10.128C6.34686 10.2143 6.45299 10.3205 6.53936 10.4419C6.62544 10.563 6.68837 10.713 6.88864 11.2337L8.04476 14.2396C8.14379 14.4971 8.39114 14.667 8.66699 14.667C8.94284 14.667 9.1902 14.4971 9.28922 14.2396L10.4453 11.2337C10.6456 10.713 10.7085 10.563 10.7946 10.4419C10.881 10.3205 10.9871 10.2143 11.1086 10.128C11.2297 10.0419 11.3797 9.97894 11.9004 9.77867L14.9063 8.62256C15.1638 8.52353 15.3337 8.27617 15.3337 8.00033C15.3337 7.72448 15.1638 7.47712 14.9063 7.37809L11.9004 6.22198C11.3797 6.02171 11.2297 5.95878 11.1086 5.87269C10.9871 5.78632 10.881 5.68019 10.7946 5.55872C10.7085 5.43766 10.6456 5.28761 10.4453 4.76691L9.28922 1.76101Z" fill="#444CE7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
@ -0,0 +1,679 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiErrorWarningFill,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import { useBoolean, useClickAway } from 'ahooks'
|
||||||
|
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import Button from '../../base/button'
|
||||||
|
import { checkOrSetAccessToken } from '../utils'
|
||||||
|
import s from './style.module.css'
|
||||||
|
import ResDownload from './run-batch/res-download'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
|
import RunOnce from '@/app/components/run/text-generation/run-once'
|
||||||
|
import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share'
|
||||||
|
import type { SiteInfo } from '@/models/share'
|
||||||
|
import type {
|
||||||
|
MoreLikeThisConfig,
|
||||||
|
PromptConfig,
|
||||||
|
SavedMessage,
|
||||||
|
TextToSpeechConfig,
|
||||||
|
} from '@/models/debug'
|
||||||
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
|
import { changeLanguage } from '@/i18n/i18next-config'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||||
|
import Res from '@/app/components/run/text-generation/result'
|
||||||
|
import type { InstalledApp } from '@/models/explore'
|
||||||
|
import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||||
|
import { Resolution, TransferMethod } from '@/types/app'
|
||||||
|
import { useAppFavicon } from '@/hooks/use-app-favicon'
|
||||||
|
|
||||||
|
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
|
||||||
|
enum TaskStatus {
|
||||||
|
pending = 'pending',
|
||||||
|
running = 'running',
|
||||||
|
completed = 'completed',
|
||||||
|
failed = 'failed',
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskParam = {
|
||||||
|
inputs: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task = {
|
||||||
|
id: number
|
||||||
|
status: TaskStatus
|
||||||
|
params: TaskParam
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IMainProps = {
|
||||||
|
isInstalledApp?: boolean
|
||||||
|
installedAppInfo?: InstalledApp
|
||||||
|
isWorkflow?: boolean
|
||||||
|
apiKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TextGeneration: FC<IMainProps> = ({
|
||||||
|
isInstalledApp = false,
|
||||||
|
installedAppInfo,
|
||||||
|
isWorkflow = false,
|
||||||
|
apiKey,
|
||||||
|
}) => {
|
||||||
|
const { notify } = Toast
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const media = useBreakpoints()
|
||||||
|
const isPC = media === MediaType.pc
|
||||||
|
const isTablet = media === MediaType.tablet
|
||||||
|
const isMobile = media === MediaType.mobile
|
||||||
|
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const mode = searchParams.get('mode') || 'create'
|
||||||
|
const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(searchParams)
|
||||||
|
if (params.has('mode')) {
|
||||||
|
params.delete('mode')
|
||||||
|
router.replace(`${pathname}?${params.toString()}`)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Notice this situation isCallBatchAPI but not in batch tab
|
||||||
|
const [isCallBatchAPI, setIsCallBatchAPI] = useState(false)
|
||||||
|
const isInBatchTab = currentTab === 'batch'
|
||||||
|
const [inputs, doSetInputs] = useState<Record<string, any>>({})
|
||||||
|
const inputsRef = useRef(inputs)
|
||||||
|
const setInputs = useCallback((newInputs: Record<string, any>) => {
|
||||||
|
doSetInputs(newInputs)
|
||||||
|
inputsRef.current = newInputs
|
||||||
|
}, [])
|
||||||
|
const [appId, setAppId] = useState<string>('')
|
||||||
|
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
|
||||||
|
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
|
||||||
|
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||||
|
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
|
||||||
|
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
|
||||||
|
|
||||||
|
// save message
|
||||||
|
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||||
|
const fetchSavedMessage = async () => {
|
||||||
|
const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id)
|
||||||
|
setSavedMessages(res.data)
|
||||||
|
}
|
||||||
|
const handleSaveMessage = async (messageId: string) => {
|
||||||
|
await saveMessage(messageId, isInstalledApp, installedAppInfo?.id)
|
||||||
|
notify({ type: 'success', message: t('common.api.saved') })
|
||||||
|
fetchSavedMessage()
|
||||||
|
}
|
||||||
|
const handleRemoveSavedMessage = async (messageId: string) => {
|
||||||
|
await removeMessage(messageId, isInstalledApp, installedAppInfo?.id)
|
||||||
|
notify({ type: 'success', message: t('common.api.remove') })
|
||||||
|
fetchSavedMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send message task
|
||||||
|
const [controlSend, setControlSend] = useState(0)
|
||||||
|
const [controlStopResponding, setControlStopResponding] = useState(0)
|
||||||
|
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
|
||||||
|
enabled: false,
|
||||||
|
number_limits: 2,
|
||||||
|
detail: Resolution.low,
|
||||||
|
transfer_methods: [TransferMethod.local_file],
|
||||||
|
})
|
||||||
|
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
setIsCallBatchAPI(false)
|
||||||
|
setControlSend(Date.now())
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-use-before-define
|
||||||
|
setAllTaskList([]) // clear batch task running status
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-use-before-define
|
||||||
|
showResSidebar()
|
||||||
|
}
|
||||||
|
|
||||||
|
const [controlRetry, setControlRetry] = useState(0)
|
||||||
|
const handleRetryAllFailedTask = () => {
|
||||||
|
setControlRetry(Date.now())
|
||||||
|
}
|
||||||
|
const [allTaskList, doSetAllTaskList] = useState<Task[]>([])
|
||||||
|
const allTaskListRef = useRef<Task[]>([])
|
||||||
|
const getLatestTaskList = () => allTaskListRef.current
|
||||||
|
const setAllTaskList = (taskList: Task[]) => {
|
||||||
|
doSetAllTaskList(taskList)
|
||||||
|
allTaskListRef.current = taskList
|
||||||
|
}
|
||||||
|
const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending)
|
||||||
|
const noPendingTask = pendingTaskList.length === 0
|
||||||
|
const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending)
|
||||||
|
const [currGroupNum, doSetCurrGroupNum] = useState(0)
|
||||||
|
const currGroupNumRef = useRef(0)
|
||||||
|
const setCurrGroupNum = (num: number) => {
|
||||||
|
doSetCurrGroupNum(num)
|
||||||
|
currGroupNumRef.current = num
|
||||||
|
}
|
||||||
|
const getCurrGroupNum = () => {
|
||||||
|
return currGroupNumRef.current
|
||||||
|
}
|
||||||
|
const allSuccessTaskList = allTaskList.filter(task => task.status === TaskStatus.completed)
|
||||||
|
const allFailedTaskList = allTaskList.filter(task => task.status === TaskStatus.failed)
|
||||||
|
const allTasksFinished = allTaskList.every(task => task.status === TaskStatus.completed)
|
||||||
|
const allTasksRun = allTaskList.every(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status))
|
||||||
|
const [batchCompletionRes, doSetBatchCompletionRes] = useState<Record<string, string>>({})
|
||||||
|
const batchCompletionResRef = useRef<Record<string, string>>({})
|
||||||
|
const setBatchCompletionRes = (res: Record<string, string>) => {
|
||||||
|
doSetBatchCompletionRes(res)
|
||||||
|
batchCompletionResRef.current = res
|
||||||
|
}
|
||||||
|
const getBatchCompletionRes = () => batchCompletionResRef.current
|
||||||
|
const exportRes = allTaskList.map((task) => {
|
||||||
|
const batchCompletionResLatest = getBatchCompletionRes()
|
||||||
|
const res: Record<string, string> = {}
|
||||||
|
const { inputs } = task.params
|
||||||
|
promptConfig?.prompt_variables.forEach((v) => {
|
||||||
|
res[v.name] = inputs[v.key]
|
||||||
|
})
|
||||||
|
let result = batchCompletionResLatest[task.id]
|
||||||
|
// task might return multiple fields, should marshal object to string
|
||||||
|
if (typeof batchCompletionResLatest[task.id] === 'object')
|
||||||
|
result = JSON.stringify(result)
|
||||||
|
|
||||||
|
res[t('share.generation.completionResult')] = result
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
const checkBatchInputs = (data: string[][]) => {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.empty') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const headerData = data[0]
|
||||||
|
let isMapVarName = true
|
||||||
|
promptConfig?.prompt_variables.forEach((item, index) => {
|
||||||
|
if (!isMapVarName)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (item.name !== headerData[index])
|
||||||
|
isMapVarName = false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isMapVarName) {
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.fileStructNotMatch') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let payloadData = data.slice(1)
|
||||||
|
if (payloadData.length === 0) {
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.atLeastOne') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check middle empty line
|
||||||
|
const allEmptyLineIndexes = payloadData.filter(item => item.every(i => i === '')).map(item => payloadData.indexOf(item))
|
||||||
|
if (allEmptyLineIndexes.length > 0) {
|
||||||
|
let hasMiddleEmptyLine = false
|
||||||
|
let startIndex = allEmptyLineIndexes[0] - 1
|
||||||
|
allEmptyLineIndexes.forEach((index) => {
|
||||||
|
if (hasMiddleEmptyLine)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (startIndex + 1 !== index) {
|
||||||
|
hasMiddleEmptyLine = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startIndex++
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasMiddleEmptyLine) {
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.emptyLine', { rowIndex: startIndex + 2 }) })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check row format
|
||||||
|
payloadData = payloadData.filter(item => !item.every(i => i === ''))
|
||||||
|
// after remove empty rows in the end, checked again
|
||||||
|
if (payloadData.length === 0) {
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.atLeastOne') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let errorRowIndex = 0
|
||||||
|
let requiredVarName = ''
|
||||||
|
let moreThanMaxLengthVarName = ''
|
||||||
|
let maxLength = 0
|
||||||
|
payloadData.forEach((item, index) => {
|
||||||
|
if (errorRowIndex !== 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
promptConfig?.prompt_variables.forEach((varItem, varIndex) => {
|
||||||
|
if (errorRowIndex !== 0)
|
||||||
|
return
|
||||||
|
if (varItem.type === 'string') {
|
||||||
|
const maxLen = varItem.max_length || DEFAULT_VALUE_MAX_LEN
|
||||||
|
if (item[varIndex].length > maxLen) {
|
||||||
|
moreThanMaxLengthVarName = varItem.name
|
||||||
|
maxLength = maxLen
|
||||||
|
errorRowIndex = index + 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!varItem.required)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (item[varIndex].trim() === '') {
|
||||||
|
requiredVarName = varItem.name
|
||||||
|
errorRowIndex = index + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (errorRowIndex !== 0) {
|
||||||
|
if (requiredVarName)
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.invalidLine', { rowIndex: errorRowIndex + 1, varName: requiredVarName }) })
|
||||||
|
|
||||||
|
if (moreThanMaxLengthVarName)
|
||||||
|
notify({ type: 'error', message: t('share.generation.errorMsg.moreThanMaxLengthLine', { rowIndex: errorRowIndex + 1, varName: moreThanMaxLengthVarName, maxLength }) })
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const handleRunBatch = (data: string[][]) => {
|
||||||
|
if (!checkBatchInputs(data))
|
||||||
|
return
|
||||||
|
if (!allTasksFinished) {
|
||||||
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForBatchResponse') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadData = data.filter(item => !item.every(i => i === '')).slice(1)
|
||||||
|
const varLen = promptConfig?.prompt_variables.length || 0
|
||||||
|
setIsCallBatchAPI(true)
|
||||||
|
const allTaskList: Task[] = payloadData.map((item, i) => {
|
||||||
|
const inputs: Record<string, string> = {}
|
||||||
|
if (varLen > 0) {
|
||||||
|
item.slice(0, varLen).forEach((input, index) => {
|
||||||
|
inputs[promptConfig?.prompt_variables[index].key as string] = input
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: i + 1,
|
||||||
|
status: i < GROUP_SIZE ? TaskStatus.running : TaskStatus.pending,
|
||||||
|
params: {
|
||||||
|
inputs,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setAllTaskList(allTaskList)
|
||||||
|
setCurrGroupNum(0)
|
||||||
|
setControlSend(Date.now())
|
||||||
|
// clear run once task status
|
||||||
|
setControlStopResponding(Date.now())
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-use-before-define
|
||||||
|
showResSidebar()
|
||||||
|
}
|
||||||
|
const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
|
||||||
|
const allTaskListLatest = getLatestTaskList()
|
||||||
|
const batchCompletionResLatest = getBatchCompletionRes()
|
||||||
|
const pendingTaskList = allTaskListLatest.filter(task => task.status === TaskStatus.pending)
|
||||||
|
const runTasksCount = 1 + allTaskListLatest.filter(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status)).length
|
||||||
|
const needToAddNextGroupTask = (getCurrGroupNum() !== runTasksCount) && pendingTaskList.length > 0 && (runTasksCount % GROUP_SIZE === 0 || (allTaskListLatest.length - runTasksCount < GROUP_SIZE))
|
||||||
|
// avoid add many task at the same time
|
||||||
|
if (needToAddNextGroupTask)
|
||||||
|
setCurrGroupNum(runTasksCount)
|
||||||
|
|
||||||
|
const nextPendingTaskIds = needToAddNextGroupTask ? pendingTaskList.slice(0, GROUP_SIZE).map(item => item.id) : []
|
||||||
|
const newAllTaskList = allTaskListLatest.map((item) => {
|
||||||
|
if (item.id === taskId) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
status: isSuccess ? TaskStatus.completed : TaskStatus.failed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needToAddNextGroupTask && nextPendingTaskIds.includes(item.id)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
status: TaskStatus.running,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
setAllTaskList(newAllTaskList)
|
||||||
|
if (taskId) {
|
||||||
|
setBatchCompletionRes({
|
||||||
|
...batchCompletionResLatest,
|
||||||
|
[`${taskId}`]: completionRes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchInitData = async () => {
|
||||||
|
if (!isInstalledApp)
|
||||||
|
await checkOrSetAccessToken()
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
isInstalledApp
|
||||||
|
? {
|
||||||
|
app_id: installedAppInfo?.id,
|
||||||
|
site: {
|
||||||
|
title: installedAppInfo?.app.name,
|
||||||
|
prompt_public: false,
|
||||||
|
copyright: '',
|
||||||
|
icon: installedAppInfo?.app.icon,
|
||||||
|
icon_background: installedAppInfo?.app.icon_background,
|
||||||
|
},
|
||||||
|
plan: 'basic',
|
||||||
|
}
|
||||||
|
: fetchAppInfo(),
|
||||||
|
fetchAppParams(isInstalledApp, installedAppInfo?.id),
|
||||||
|
!isWorkflow
|
||||||
|
? fetchSavedMessage()
|
||||||
|
: {},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const [appData, appParams]: any = await fetchInitData()
|
||||||
|
const { app_id: appId, site: siteInfo, can_replace_logo } = appData
|
||||||
|
setAppId(appId)
|
||||||
|
setSiteInfo(siteInfo as SiteInfo)
|
||||||
|
setCanReplaceLogo(can_replace_logo)
|
||||||
|
changeLanguage(siteInfo.default_language)
|
||||||
|
|
||||||
|
const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
|
||||||
|
setVisionConfig({
|
||||||
|
// legacy of image upload compatible
|
||||||
|
...file_upload,
|
||||||
|
transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods,
|
||||||
|
// legacy of image upload compatible
|
||||||
|
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
|
||||||
|
fileUploadConfig: appParams?.system_parameters,
|
||||||
|
})
|
||||||
|
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||||
|
setPromptConfig({
|
||||||
|
prompt_template: '', // placeholder for future
|
||||||
|
prompt_variables,
|
||||||
|
} as PromptConfig)
|
||||||
|
setMoreLikeThisConfig(more_like_this)
|
||||||
|
setTextToSpeechConfig(text_to_speech)
|
||||||
|
})()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
|
||||||
|
useEffect(() => {
|
||||||
|
if (siteInfo?.title) {
|
||||||
|
if (canReplaceLogo)
|
||||||
|
document.title = `${siteInfo.title}`
|
||||||
|
else
|
||||||
|
document.title = `${siteInfo.title} - Powered by Dify`
|
||||||
|
}
|
||||||
|
}, [siteInfo?.title, canReplaceLogo])
|
||||||
|
|
||||||
|
useAppFavicon({
|
||||||
|
enable: !isInstalledApp,
|
||||||
|
icon_type: siteInfo?.icon_type,
|
||||||
|
icon: siteInfo?.icon,
|
||||||
|
icon_background: siteInfo?.icon_background,
|
||||||
|
icon_url: siteInfo?.icon_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false)
|
||||||
|
const showResSidebar = () => {
|
||||||
|
// fix: useClickAway hideResSidebar will close sidebar
|
||||||
|
setTimeout(() => {
|
||||||
|
doShowResSidebar()
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
const resRef = useRef<HTMLDivElement>(null)
|
||||||
|
useClickAway(() => {
|
||||||
|
hideResSidebar()
|
||||||
|
}, resRef)
|
||||||
|
|
||||||
|
const renderRes = (task?: Task) => (<Res
|
||||||
|
key={task?.id}
|
||||||
|
isWorkflow={isWorkflow}
|
||||||
|
isCallBatchAPI={isCallBatchAPI}
|
||||||
|
isPC={isPC}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isInstalledApp={isInstalledApp}
|
||||||
|
installedAppInfo={installedAppInfo}
|
||||||
|
isError={task?.status === TaskStatus.failed}
|
||||||
|
promptConfig={promptConfig}
|
||||||
|
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
|
||||||
|
inputs={isCallBatchAPI ? (task as Task).params.inputs : inputs}
|
||||||
|
controlSend={controlSend}
|
||||||
|
controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0}
|
||||||
|
controlStopResponding={controlStopResponding}
|
||||||
|
onShowRes={showResSidebar}
|
||||||
|
handleSaveMessage={handleSaveMessage}
|
||||||
|
taskId={task?.id}
|
||||||
|
onCompleted={handleCompleted}
|
||||||
|
visionConfig={visionConfig}
|
||||||
|
completionFiles={completionFiles}
|
||||||
|
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
|
||||||
|
siteInfo={siteInfo}
|
||||||
|
apiKey={apiKey}
|
||||||
|
/>)
|
||||||
|
|
||||||
|
const renderBatchRes = () => {
|
||||||
|
return (showTaskList.map(task => renderRes(task)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const resWrapClassNames = (() => {
|
||||||
|
if (isPC)
|
||||||
|
return 'grow h-full'
|
||||||
|
|
||||||
|
if (!isShowResSidebar)
|
||||||
|
return 'none'
|
||||||
|
|
||||||
|
return cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')
|
||||||
|
})()
|
||||||
|
|
||||||
|
const renderResWrap = (
|
||||||
|
<div
|
||||||
|
ref={resRef}
|
||||||
|
className={
|
||||||
|
cn(
|
||||||
|
'flex flex-col h-full shrink-0',
|
||||||
|
isPC ? 'px-10 py-8' : 'bg-gray-50',
|
||||||
|
isTablet && 'p-6', isMobile && 'p-4')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div className='flex items-center justify-between shrink-0'>
|
||||||
|
<div className='flex items-center space-x-3'>
|
||||||
|
<div className={s.starIcon}></div>
|
||||||
|
<div className='text-lg font-semibold text-gray-800'>{t('share.generation.title')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
{allFailedTaskList.length > 0 && (
|
||||||
|
<div className='flex items-center'>
|
||||||
|
<RiErrorWarningFill className='w-4 h-4 text-[#D92D20]' />
|
||||||
|
<div className='ml-1 text-[#D92D20]'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
className='ml-2'
|
||||||
|
onClick={handleRetryAllFailedTask}
|
||||||
|
>{t('share.generation.batchFailed.retry')}</Button>
|
||||||
|
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{allSuccessTaskList.length > 0 && (
|
||||||
|
<ResDownload
|
||||||
|
isMobile={isMobile}
|
||||||
|
values={exportRes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isPC && (
|
||||||
|
<div
|
||||||
|
className='flex items-center justify-center cursor-pointer'
|
||||||
|
onClick={hideResSidebar}
|
||||||
|
>
|
||||||
|
<XMarkIcon className='w-4 h-4 text-gray-800' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='overflow-y-auto grow'>
|
||||||
|
{!isCallBatchAPI ? renderRes() : renderBatchRes()}
|
||||||
|
{!noPendingTask && (
|
||||||
|
<div className='mt-4'>
|
||||||
|
<Loading type='area' />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!appId || !siteInfo || !promptConfig) {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center h-screen'>
|
||||||
|
<Loading type='app' />
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={cn(
|
||||||
|
isPC && 'flex',
|
||||||
|
isInstalledApp ? s.installedApp : 'h-screen',
|
||||||
|
'bg-gray-50',
|
||||||
|
)}>
|
||||||
|
{/* Left */}
|
||||||
|
<div className={cn(
|
||||||
|
isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4',
|
||||||
|
isInstalledApp && 'rounded-l-2xl',
|
||||||
|
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
|
||||||
|
)}>
|
||||||
|
<div className='mb-6'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='flex items-center space-x-3'>
|
||||||
|
<AppIcon
|
||||||
|
size="small"
|
||||||
|
iconType={siteInfo.icon_type}
|
||||||
|
icon={siteInfo.icon}
|
||||||
|
background={siteInfo.icon_background || appDefaultIconBackground}
|
||||||
|
imageUrl={siteInfo.icon_url}
|
||||||
|
/>
|
||||||
|
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
|
||||||
|
</div>
|
||||||
|
{!isPC && (
|
||||||
|
<Button
|
||||||
|
className='shrink-0 ml-2'
|
||||||
|
onClick={showResSidebar}
|
||||||
|
>
|
||||||
|
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
|
||||||
|
<div className={s.starIcon}></div>
|
||||||
|
<span>{t('share.generation.title')}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{siteInfo.description && (
|
||||||
|
<div className='mt-2 text-xs text-gray-500'>{siteInfo.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* <TabHeader
|
||||||
|
items={[
|
||||||
|
{ id: 'create', name: t('share.generation.tabs.create') },
|
||||||
|
{ id: 'batch', name: t('share.generation.tabs.batch') },
|
||||||
|
...(!isWorkflow
|
||||||
|
? [{
|
||||||
|
id: 'saved',
|
||||||
|
name: t('share.generation.tabs.saved'),
|
||||||
|
isRight: true,
|
||||||
|
extra: savedMessages.length > 0
|
||||||
|
? (
|
||||||
|
<div className='ml-1 flex items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'>
|
||||||
|
{savedMessages.length}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
value={currentTab}
|
||||||
|
onChange={setCurrentTab}
|
||||||
|
/> */}
|
||||||
|
<div className='h-20 overflow-y-auto grow'>
|
||||||
|
{/* <div className={cn(currentTab === 'create' ? 'block' : 'hidden')}> */}
|
||||||
|
<RunOnce
|
||||||
|
siteInfo={siteInfo}
|
||||||
|
inputs={inputs}
|
||||||
|
inputsRef={inputsRef}
|
||||||
|
onInputsChange={setInputs}
|
||||||
|
promptConfig={promptConfig}
|
||||||
|
onSend={handleSend}
|
||||||
|
visionConfig={visionConfig}
|
||||||
|
onVisionFilesChange={setCompletionFiles}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
{/* <div className={cn(isInBatchTab ? 'block' : 'hidden')}>
|
||||||
|
<RunBatch
|
||||||
|
vars={promptConfig.prompt_variables}
|
||||||
|
onSend={handleRunBatch}
|
||||||
|
isAllFinished={allTasksRun}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{currentTab === 'saved' && (
|
||||||
|
<SavedItems
|
||||||
|
className='mt-4'
|
||||||
|
isShowTextToSpeech={textToSpeechConfig?.enabled}
|
||||||
|
list={savedMessages}
|
||||||
|
onRemove={handleRemoveSavedMessage}
|
||||||
|
onStartCreateContent={() => setCurrentTab('create')}
|
||||||
|
/>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* copyright */}
|
||||||
|
<div className={cn(
|
||||||
|
isInstalledApp ? 'left-[248px]' : 'left-8',
|
||||||
|
'fixed bottom-4 flex space-x-2 text-gray-400 font-normal text-xs',
|
||||||
|
)}>
|
||||||
|
<div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div>
|
||||||
|
{siteInfo.privacy_policy && (
|
||||||
|
<>
|
||||||
|
<div>·</div>
|
||||||
|
<div>{t('share.chat.privacyPolicyLeft')}
|
||||||
|
<a
|
||||||
|
className='text-gray-500 px-1'
|
||||||
|
href={siteInfo.privacy_policy}
|
||||||
|
target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
|
||||||
|
{t('share.chat.privacyPolicyRight')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Result */}
|
||||||
|
<div
|
||||||
|
className={resWrapClassNames}
|
||||||
|
style={{
|
||||||
|
background: (!isPC && isShowResSidebar) ? 'rgba(35, 56, 118, 0.2)' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderResWrap}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextGeneration
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const StarIcon = (
|
||||||
|
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.50033 48.3337V36.667M7.50033 13.3337V1.66699M1.66699 7.50033H13.3337M1.66699 42.5003H13.3337M27.3337 4.00032L23.2872 14.521C22.6292 16.2319 22.3002 17.0873 21.7886 17.8069C21.3351 18.4446 20.7779 19.0018 20.1402 19.4552C19.4206 19.9669 18.5652 20.2959 16.8543 20.9539L6.33366 25.0003L16.8543 29.0467C18.5652 29.7048 19.4206 30.0338 20.1402 30.5454C20.7779 30.9989 21.3351 31.5561 21.7886 32.1938C22.3002 32.9133 22.6292 33.7688 23.2872 35.4796L27.3337 46.0003L31.3801 35.4796C32.0381 33.7688 32.3671 32.9133 32.8788 32.1938C33.3322 31.5561 33.8894 30.9989 34.5271 30.5454C35.2467 30.0338 36.1021 29.7048 37.813 29.0467L48.3337 25.0003L37.813 20.9539C36.1021 20.2959 35.2467 19.9669 34.5271 19.4552C33.8894 19.0018 33.3322 18.4446 32.8788 17.8069C32.3671 17.0873 32.0381 16.2319 31.3801 14.521L27.3337 4.00032Z" stroke="#EAECF0" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
export type INoDataProps = {}
|
||||||
|
const NoData: FC<INoDataProps> = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col h-full w-full justify-center items-center'>
|
||||||
|
{StarIcon}
|
||||||
|
<div
|
||||||
|
className='mt-3 text-gray-300 text-xs leading-3'
|
||||||
|
>
|
||||||
|
{t('share.generation.noData')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(NoData)
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import Header from './header'
|
||||||
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
|
import { format } from '@/service/base'
|
||||||
|
|
||||||
|
export type IResultProps = {
|
||||||
|
content: string
|
||||||
|
showFeedback: boolean
|
||||||
|
feedback: FeedbackType
|
||||||
|
onFeedback: (feedback: FeedbackType) => void
|
||||||
|
}
|
||||||
|
const Result: FC<IResultProps> = ({
|
||||||
|
content,
|
||||||
|
showFeedback,
|
||||||
|
feedback,
|
||||||
|
onFeedback,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className='basis-3/4 h-max'>
|
||||||
|
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
|
||||||
|
<div
|
||||||
|
className='mt-4 w-full flex text-sm leading-5 overflow-scroll font-normal text-gray-900'
|
||||||
|
style={{
|
||||||
|
maxHeight: '70vh',
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: format(content),
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(Result)
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
|
||||||
|
type IResultHeaderProps = {
|
||||||
|
result: string
|
||||||
|
showFeedback: boolean
|
||||||
|
feedback: FeedbackType
|
||||||
|
onFeedback: (feedback: FeedbackType) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Header: FC<IResultHeaderProps> = ({
|
||||||
|
feedback,
|
||||||
|
showFeedback,
|
||||||
|
onFeedback,
|
||||||
|
result,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<div className='flex w-full justify-between items-center '>
|
||||||
|
<div className='text-gray-800 text-2xl leading-4 font-normal'>{t('share.generation.resultTitle')}</div>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
<Button
|
||||||
|
className='h-7 p-[2px] pr-2'
|
||||||
|
onClick={() => {
|
||||||
|
copy(result)
|
||||||
|
Toast.notify({ type: 'success', message: 'copied' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<ClipboardDocumentIcon className='text-gray-500 w-4 h-3 mr-1' />
|
||||||
|
<span className='text-gray-500 text-xs leading-3'>{t('share.generation.copy')}</span>
|
||||||
|
</>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showFeedback && feedback.rating && feedback.rating === 'like' && (
|
||||||
|
<Tooltip
|
||||||
|
popupContent="Undo Great Rating"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
onFeedback({
|
||||||
|
rating: null,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||||
|
<HandThumbUpIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
|
||||||
|
<Tooltip
|
||||||
|
popupContent="Undo Undesirable Response"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
onFeedback({
|
||||||
|
rating: null,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||||
|
<HandThumbDownIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showFeedback && !feedback.rating && (
|
||||||
|
<div className='flex rounded-lg border border-gray-200 p-[1px] space-x-1'>
|
||||||
|
<Tooltip
|
||||||
|
popupContent="Great Rating"
|
||||||
|
needsDelay={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
onFeedback({
|
||||||
|
rating: 'like',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||||
|
<HandThumbUpIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
popupContent="Undesirable Response"
|
||||||
|
needsDelay={false}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
onFeedback({
|
||||||
|
rating: 'dislike',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||||
|
<HandThumbDownIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Header)
|
||||||
@ -0,0 +1,401 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import produce from 'immer'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import NoData from '@/app/components/share/text-generation/no-data'
|
||||||
|
import Toast from '@/app/components/base/toast'
|
||||||
|
import { sendWorkflowRun, updateFeedback } from '@/service/share'
|
||||||
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import type { PromptConfig } from '@/models/debug'
|
||||||
|
import type { InstalledApp } from '@/models/explore'
|
||||||
|
import type { ModerationService } from '@/models/common'
|
||||||
|
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||||
|
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||||
|
import type { WorkflowProcess } from '@/app/components/base/chat/types'
|
||||||
|
import { sleep } from '@/utils'
|
||||||
|
import type { SiteInfo } from '@/models/share'
|
||||||
|
import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
|
||||||
|
import {
|
||||||
|
getFilesInLogs,
|
||||||
|
} from '@/app/components/base/file-uploader/utils'
|
||||||
|
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
|
||||||
|
import { useStore } from '@/app/components/app/store'
|
||||||
|
|
||||||
|
export type IResultProps = {
|
||||||
|
isWorkflow: boolean
|
||||||
|
isCallBatchAPI: boolean
|
||||||
|
isPC: boolean
|
||||||
|
isMobile: boolean
|
||||||
|
isInstalledApp: boolean
|
||||||
|
installedAppInfo?: InstalledApp
|
||||||
|
isError: boolean
|
||||||
|
isShowTextToSpeech: boolean
|
||||||
|
promptConfig: PromptConfig | null
|
||||||
|
moreLikeThisEnabled: boolean
|
||||||
|
inputs: Record<string, any>
|
||||||
|
controlSend?: number
|
||||||
|
controlRetry?: number
|
||||||
|
controlStopResponding?: number
|
||||||
|
onShowRes: () => void
|
||||||
|
handleSaveMessage: (messageId: string) => void
|
||||||
|
taskId?: number
|
||||||
|
onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
|
||||||
|
enableModeration?: boolean
|
||||||
|
moderationService?: (text: string) => ReturnType<ModerationService>
|
||||||
|
visionConfig: VisionSettings
|
||||||
|
completionFiles: VisionFile[]
|
||||||
|
siteInfo: SiteInfo | null
|
||||||
|
apiKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Result: FC<IResultProps> = ({
|
||||||
|
isWorkflow,
|
||||||
|
isCallBatchAPI,
|
||||||
|
isPC,
|
||||||
|
isMobile,
|
||||||
|
isInstalledApp,
|
||||||
|
installedAppInfo,
|
||||||
|
isError,
|
||||||
|
isShowTextToSpeech,
|
||||||
|
promptConfig,
|
||||||
|
moreLikeThisEnabled,
|
||||||
|
inputs,
|
||||||
|
controlSend,
|
||||||
|
controlRetry,
|
||||||
|
controlStopResponding,
|
||||||
|
onShowRes,
|
||||||
|
handleSaveMessage,
|
||||||
|
taskId,
|
||||||
|
onCompleted,
|
||||||
|
visionConfig,
|
||||||
|
completionFiles,
|
||||||
|
siteInfo,
|
||||||
|
apiKey,
|
||||||
|
}) => {
|
||||||
|
const appDetail = useStore(state => state.appDetail)!
|
||||||
|
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlStopResponding)
|
||||||
|
setRespondingFalse()
|
||||||
|
}, [controlStopResponding])
|
||||||
|
|
||||||
|
const [completionRes, doSetCompletionRes] = useState<any>('')
|
||||||
|
const completionResRef = useRef<any>()
|
||||||
|
const setCompletionRes = (res: any) => {
|
||||||
|
completionResRef.current = res
|
||||||
|
doSetCompletionRes(res)
|
||||||
|
}
|
||||||
|
const getCompletionRes = () => completionResRef.current
|
||||||
|
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
|
||||||
|
const workflowProcessDataRef = useRef<WorkflowProcess>()
|
||||||
|
const setWorkflowProcessData = (data: WorkflowProcess) => {
|
||||||
|
workflowProcessDataRef.current = data
|
||||||
|
doSetWorkflowProcessData(data)
|
||||||
|
}
|
||||||
|
const getWorkflowProcessData = () => workflowProcessDataRef.current
|
||||||
|
|
||||||
|
const { notify } = Toast
|
||||||
|
const isNoData = !completionRes
|
||||||
|
|
||||||
|
const [messageId, setMessageId] = useState<string | null>(null)
|
||||||
|
const [feedback, setFeedback] = useState<FeedbackType>({
|
||||||
|
rating: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleFeedback = async (feedback: FeedbackType) => {
|
||||||
|
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
|
||||||
|
setFeedback(feedback)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logError = (message: string) => {
|
||||||
|
notify({ type: 'error', message })
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkCanSend = () => {
|
||||||
|
// batch will check outer
|
||||||
|
if (isCallBatchAPI)
|
||||||
|
return true
|
||||||
|
|
||||||
|
const prompt_variables = promptConfig?.prompt_variables
|
||||||
|
if (!prompt_variables || prompt_variables?.length === 0) {
|
||||||
|
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||||
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasEmptyInput = ''
|
||||||
|
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
|
||||||
|
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||||
|
return res
|
||||||
|
}) || [] // compatible with old version
|
||||||
|
requiredVars.forEach(({ key, name }) => {
|
||||||
|
if (hasEmptyInput)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!inputs[key])
|
||||||
|
hasEmptyInput = name
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasEmptyInput) {
|
||||||
|
logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
|
||||||
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !hasEmptyInput
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (isResponding) {
|
||||||
|
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkCanSend())
|
||||||
|
return
|
||||||
|
|
||||||
|
const data: Record<string, any> = {
|
||||||
|
inputs,
|
||||||
|
}
|
||||||
|
if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
|
||||||
|
data.files = completionFiles.map((item) => {
|
||||||
|
if (item.transfer_method === TransferMethod.local_file) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
url: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageId(null)
|
||||||
|
setFeedback({
|
||||||
|
rating: null,
|
||||||
|
})
|
||||||
|
setCompletionRes('')
|
||||||
|
|
||||||
|
const res: string[] = []
|
||||||
|
let tempMessageId = ''
|
||||||
|
|
||||||
|
if (!isPC)
|
||||||
|
onShowRes()
|
||||||
|
|
||||||
|
setRespondingTrue()
|
||||||
|
let isEnd = false
|
||||||
|
let isTimeout = false;
|
||||||
|
(async () => {
|
||||||
|
await sleep(TEXT_GENERATION_TIMEOUT_MS)
|
||||||
|
if (!isEnd) {
|
||||||
|
setRespondingFalse()
|
||||||
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
|
isTimeout = true
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
data.user = 'test'
|
||||||
|
if (isWorkflow) {
|
||||||
|
sendWorkflowRun(
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
onWorkflowStarted: ({ workflow_run_id }) => {
|
||||||
|
tempMessageId = workflow_run_id
|
||||||
|
setWorkflowProcessData({
|
||||||
|
status: WorkflowRunningStatus.Running,
|
||||||
|
tracing: [],
|
||||||
|
expand: false,
|
||||||
|
resultText: '',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onIterationStart: ({ data }) => {
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.expand = true
|
||||||
|
draft.tracing!.push({
|
||||||
|
...data,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
|
expand: true,
|
||||||
|
} as any)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onIterationNext: () => {
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.expand = true
|
||||||
|
const iterations = draft.tracing.find(item => item.node_id === data.node_id
|
||||||
|
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||||
|
iterations?.details!.push([])
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onIterationFinish: ({ data }) => {
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.expand = true
|
||||||
|
const iterationsIndex = draft.tracing.findIndex(item => item.node_id === data.node_id
|
||||||
|
&& (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
|
||||||
|
draft.tracing[iterationsIndex] = {
|
||||||
|
...data,
|
||||||
|
expand: !!data.error,
|
||||||
|
} as any
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onNodeStarted: ({ data }) => {
|
||||||
|
if (data.iteration_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.expand = true
|
||||||
|
draft.tracing!.push({
|
||||||
|
...data,
|
||||||
|
status: NodeRunningStatus.Running,
|
||||||
|
expand: true,
|
||||||
|
} as any)
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onNodeFinished: ({ data }) => {
|
||||||
|
if (data.iteration_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id
|
||||||
|
&& (trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || trace.parallel_id === data.execution_metadata?.parallel_id))
|
||||||
|
if (currentIndex > -1 && draft.tracing) {
|
||||||
|
draft.tracing[currentIndex] = {
|
||||||
|
...(draft.tracing[currentIndex].extras
|
||||||
|
? { extras: draft.tracing[currentIndex].extras }
|
||||||
|
: {}),
|
||||||
|
...data,
|
||||||
|
expand: !!data.error,
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onWorkflowFinished: ({ data }) => {
|
||||||
|
if (isTimeout) {
|
||||||
|
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (data.error) {
|
||||||
|
notify({ type: 'error', message: data.error })
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.status = WorkflowRunningStatus.Failed
|
||||||
|
}))
|
||||||
|
setRespondingFalse()
|
||||||
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
|
isEnd = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.status = WorkflowRunningStatus.Succeeded
|
||||||
|
draft.files = getFilesInLogs(data.outputs || []) as any[]
|
||||||
|
}))
|
||||||
|
if (!data.outputs) {
|
||||||
|
setCompletionRes('')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setCompletionRes(data.outputs)
|
||||||
|
const isStringOutput = Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
|
||||||
|
if (isStringOutput) {
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRespondingFalse()
|
||||||
|
setMessageId(tempMessageId)
|
||||||
|
onCompleted(getCompletionRes(), taskId, true)
|
||||||
|
isEnd = true
|
||||||
|
},
|
||||||
|
onTextChunk: (params) => {
|
||||||
|
const { data: { text } } = params
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.resultText += text
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
onTextReplace: (params) => {
|
||||||
|
const { data: { text } } = params
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.resultText = text
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isInstalledApp,
|
||||||
|
apiKey,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [controlClearMoreLikeThis, setControlClearMoreLikeThis] = useState(0)
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlSend) {
|
||||||
|
handleSend()
|
||||||
|
setControlClearMoreLikeThis(Date.now())
|
||||||
|
}
|
||||||
|
}, [controlSend])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlRetry)
|
||||||
|
handleSend()
|
||||||
|
}, [controlRetry])
|
||||||
|
|
||||||
|
const renderTextGenerationRes = () => (
|
||||||
|
<TracingPanel
|
||||||
|
className='bg-background-section-burn'
|
||||||
|
list={workflowProcessData?.tracing || []}
|
||||||
|
ifForRun={true}
|
||||||
|
/>
|
||||||
|
// <TextGenerationRes
|
||||||
|
// isWorkflow={isWorkflow}
|
||||||
|
// workflowProcessData={workflowProcessData}
|
||||||
|
// className='mt-3'
|
||||||
|
// isError={isError}
|
||||||
|
// onRetry={handleSend}
|
||||||
|
// content={completionRes}
|
||||||
|
// messageId={messageId}
|
||||||
|
// isInWebApp
|
||||||
|
// moreLikeThis={moreLikeThisEnabled}
|
||||||
|
// onFeedback={handleFeedback}
|
||||||
|
// feedback={feedback}
|
||||||
|
// onSave={handleSaveMessage}
|
||||||
|
// isMobile={isMobile}
|
||||||
|
// isInstalledApp={isInstalledApp}
|
||||||
|
// installedAppId={installedAppInfo?.id}
|
||||||
|
// isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||||
|
// taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||||
|
// controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||||
|
// isShowTextToSpeech={isShowTextToSpeech}
|
||||||
|
// hideProcessDetail
|
||||||
|
// siteInfo={siteInfo}
|
||||||
|
// />
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(isNoData && !isCallBatchAPI && 'h-full')}>
|
||||||
|
|
||||||
|
{
|
||||||
|
!isCallBatchAPI && isWorkflow && (
|
||||||
|
(isResponding && !workflowProcessData)
|
||||||
|
? (
|
||||||
|
<div className='flex h-full w-full justify-center items-center'>
|
||||||
|
<Loading type='area' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: !workflowProcessData
|
||||||
|
? <NoData />
|
||||||
|
: renderTextGenerationRes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{isCallBatchAPI && (
|
||||||
|
<div className='mt-2'>
|
||||||
|
{renderTextGenerationRes()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(Result)
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
useCSVDownloader,
|
||||||
|
} from 'react-papaparse'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||||
|
|
||||||
|
export type ICSVDownloadProps = {
|
||||||
|
vars: { name: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const CSVDownload: FC<ICSVDownloadProps> = ({
|
||||||
|
vars,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { CSVDownloader, Type } = useCSVDownloader()
|
||||||
|
const addQueryContentVars = [...vars]
|
||||||
|
const template = (() => {
|
||||||
|
const res: Record<string, string> = {}
|
||||||
|
addQueryContentVars.forEach((item) => {
|
||||||
|
res[item.name] = ''
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
})()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
|
||||||
|
<div className='mt-2 max-h-[500px] overflow-auto'>
|
||||||
|
<table className='w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||||
|
<thead className='text-gray-500'>
|
||||||
|
<tr>
|
||||||
|
{addQueryContentVars.map((item, i) => (
|
||||||
|
<td key={i} className='h-9 pl-4 border-b border-gray-200'>{item.name}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className='text-gray-300'>
|
||||||
|
<tr>
|
||||||
|
{addQueryContentVars.map((item, i) => (
|
||||||
|
<td key={i} className='h-9 pl-4'>{item.name} {t('share.generation.field')}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<CSVDownloader
|
||||||
|
className="block mt-2 cursor-pointer"
|
||||||
|
type={Type.Link}
|
||||||
|
filename={'template'}
|
||||||
|
bom={true}
|
||||||
|
config={{
|
||||||
|
// delimiter: ';',
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
template,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
|
||||||
|
<DownloadIcon className='w-3 h-3' />
|
||||||
|
<span>{t('share.generation.downloadTemplate')}</span>
|
||||||
|
</div>
|
||||||
|
</CSVDownloader>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(CSVDownload)
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
useCSVReader,
|
||||||
|
} from 'react-papaparse'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import s from './style.module.css'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
onParsed: (data: string[][]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CSVReader: FC<Props> = ({
|
||||||
|
onParsed,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { CSVReader } = useCSVReader()
|
||||||
|
const [zoneHover, setZoneHover] = useState(false)
|
||||||
|
return (
|
||||||
|
<CSVReader
|
||||||
|
onUploadAccepted={(results: any) => {
|
||||||
|
onParsed(results.data)
|
||||||
|
setZoneHover(false)
|
||||||
|
}}
|
||||||
|
onDragOver={(event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setZoneHover(true)
|
||||||
|
}}
|
||||||
|
onDragLeave={(event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setZoneHover(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
getRootProps,
|
||||||
|
acceptedFile,
|
||||||
|
}: any) => (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={cn(s.zone, zoneHover && s.zoneHover, acceptedFile ? 'px-6' : 'justify-center border-dashed text-gray-500')}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
acceptedFile
|
||||||
|
? (
|
||||||
|
<div className='w-full flex items-center space-x-2'>
|
||||||
|
<CSVIcon className="shrink-0" />
|
||||||
|
<div className='flex w-0 grow'>
|
||||||
|
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{acceptedFile.name.replace(/.csv$/, '')}</span>
|
||||||
|
<span className='shrink-0 text-gray-500'>.csv</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<div className='flex items-center justify-center space-x-2'>
|
||||||
|
<CSVIcon className="shrink-0" />
|
||||||
|
<div className='text-gray-500'>{t('share.generation.csvUploadTitle')}<span className='text-primary-400'>{t('share.generation.browse')}</span></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CSVReader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(CSVReader)
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
.zone {
|
||||||
|
@apply flex items-center h-20 rounded-xl bg-gray-50 border border-gray-200 cursor-pointer text-sm font-normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoneHover {
|
||||||
|
@apply border-solid bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
@apply text-gray-800 text-sm;
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
PlayIcon,
|
||||||
|
} from '@heroicons/react/24/solid'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
RiLoader2Line,
|
||||||
|
} from '@remixicon/react'
|
||||||
|
import CSVReader from './csv-reader'
|
||||||
|
import CSVDownload from './csv-download'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
export type IRunBatchProps = {
|
||||||
|
vars: { name: string }[]
|
||||||
|
onSend: (data: string[][]) => void
|
||||||
|
isAllFinished: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunBatch: FC<IRunBatchProps> = ({
|
||||||
|
vars,
|
||||||
|
onSend,
|
||||||
|
isAllFinished,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [csvData, setCsvData] = React.useState<string[][]>([])
|
||||||
|
const [isParsed, setIsParsed] = React.useState(false)
|
||||||
|
const handleParsed = (data: string[][]) => {
|
||||||
|
setCsvData(data)
|
||||||
|
// console.log(data)
|
||||||
|
setIsParsed(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
onSend(csvData)
|
||||||
|
}
|
||||||
|
const Icon = isAllFinished ? PlayIcon : RiLoader2Line
|
||||||
|
return (
|
||||||
|
<div className='pt-4'>
|
||||||
|
<CSVReader onParsed={handleParsed} />
|
||||||
|
<CSVDownload vars={vars} />
|
||||||
|
<div className='mt-4 h-[1px] bg-gray-100'></div>
|
||||||
|
<div className='flex justify-end'>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className='mt-4 pl-3 pr-4'
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!isParsed || !isAllFinished}
|
||||||
|
>
|
||||||
|
<Icon className={cn(!isAllFinished && 'animate-spin', 'shrink-0 w-4 h-4 mr-1')} aria-hidden="true" />
|
||||||
|
<span className='uppercase text-[13px]'>{t('share.generation.run')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(RunBatch)
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
useCSVDownloader,
|
||||||
|
} from 'react-papaparse'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
export type IResDownloadProps = {
|
||||||
|
isMobile: boolean
|
||||||
|
values: Record<string, string>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResDownload: FC<IResDownloadProps> = ({
|
||||||
|
isMobile,
|
||||||
|
values,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { CSVDownloader, Type } = useCSVDownloader()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CSVDownloader
|
||||||
|
className="block cursor-pointer"
|
||||||
|
type={Type.Link}
|
||||||
|
filename={'result'}
|
||||||
|
bom={true}
|
||||||
|
config={{
|
||||||
|
// delimiter: ';',
|
||||||
|
}}
|
||||||
|
data={values}
|
||||||
|
>
|
||||||
|
<Button className={cn('space-x-2 bg-white', isMobile ? '!p-0 !w-8 justify-center' : '')}>
|
||||||
|
<DownloadIcon className='w-4 h-4 text-[#155EEF]' />
|
||||||
|
{!isMobile && <span className='text-[#155EEF]'>{t('common.operation.download')}</span>}
|
||||||
|
</Button>
|
||||||
|
</CSVDownloader>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(ResDownload)
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
import type { FC, FormEvent } from 'react'
|
||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import {
|
||||||
|
PlayIcon,
|
||||||
|
} from '@heroicons/react/24/solid'
|
||||||
|
import Select from '@/app/components/base/select'
|
||||||
|
import type { SiteInfo } from '@/models/share'
|
||||||
|
import type { PromptConfig } from '@/models/debug'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import Textarea from '@/app/components/base/textarea'
|
||||||
|
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||||
|
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
|
||||||
|
import type { VisionFile, VisionSettings } from '@/types/app'
|
||||||
|
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||||
|
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||||
|
|
||||||
|
export type IRunOnceProps = {
|
||||||
|
siteInfo: SiteInfo
|
||||||
|
promptConfig: PromptConfig
|
||||||
|
inputs: Record<string, any>
|
||||||
|
inputsRef: React.MutableRefObject<Record<string, any>>
|
||||||
|
onInputsChange: (inputs: Record<string, any>) => void
|
||||||
|
onSend: () => void
|
||||||
|
visionConfig: VisionSettings
|
||||||
|
onVisionFilesChange: (files: VisionFile[]) => void
|
||||||
|
}
|
||||||
|
const RunOnce: FC<IRunOnceProps> = ({
|
||||||
|
promptConfig,
|
||||||
|
inputs,
|
||||||
|
inputsRef,
|
||||||
|
onInputsChange,
|
||||||
|
onSend,
|
||||||
|
visionConfig,
|
||||||
|
onVisionFilesChange,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const onClear = () => {
|
||||||
|
const newInputs: Record<string, any> = {}
|
||||||
|
promptConfig.prompt_variables.forEach((item) => {
|
||||||
|
newInputs[item.key] = ''
|
||||||
|
})
|
||||||
|
onInputsChange(newInputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSend()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputsChange = useCallback((newInputs: Record<string, any>) => {
|
||||||
|
onInputsChange(newInputs)
|
||||||
|
inputsRef.current = newInputs
|
||||||
|
}, [onInputsChange, inputsRef])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<section>
|
||||||
|
{/* input form */}
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
{promptConfig.prompt_variables.map(item => (
|
||||||
|
<div className='w-full mt-4' key={item.key}>
|
||||||
|
<label className='text-gray-900 text-sm font-medium'>{item.name}</label>
|
||||||
|
<div className='mt-2'>
|
||||||
|
{item.type === 'select' && (
|
||||||
|
<Select
|
||||||
|
className='w-full'
|
||||||
|
defaultValue={inputs[item.key]}
|
||||||
|
onSelect={(i) => { handleInputsChange({ ...inputsRef.current, [item.key]: i.value }) }}
|
||||||
|
items={(item.options || []).map(i => ({ name: i, value: i }))}
|
||||||
|
allowSearch={false}
|
||||||
|
bgClassName='bg-gray-50'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === 'string' && (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
|
||||||
|
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||||
|
value={inputs[item.key]}
|
||||||
|
onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||||
|
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === 'paragraph' && (
|
||||||
|
<Textarea
|
||||||
|
className='h-[104px] sm:text-xs'
|
||||||
|
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||||
|
value={inputs[item.key]}
|
||||||
|
onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === 'number' && (
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
|
||||||
|
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||||
|
value={inputs[item.key]}
|
||||||
|
onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === 'file' && (
|
||||||
|
<FileUploaderInAttachmentWrapper
|
||||||
|
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: getProcessedFiles(files)[0] }) }}
|
||||||
|
fileConfig={{
|
||||||
|
...item.config,
|
||||||
|
fileUploadConfig: (visionConfig as any).fileUploadConfig,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{item.type === 'file-list' && (
|
||||||
|
<FileUploaderInAttachmentWrapper
|
||||||
|
onChange={(files) => { handleInputsChange({ ...inputsRef.current, [item.key]: getProcessedFiles(files) }) }}
|
||||||
|
fileConfig={{
|
||||||
|
...item.config,
|
||||||
|
fileUploadConfig: (visionConfig as any).fileUploadConfig,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{
|
||||||
|
visionConfig?.enabled && (
|
||||||
|
<div className="w-full mt-4">
|
||||||
|
<div className="text-gray-900 text-sm font-medium">{t('common.imageUploader.imageUpload')}</div>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<TextGenerationImageUploader
|
||||||
|
settings={visionConfig}
|
||||||
|
onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||||
|
type: 'image',
|
||||||
|
transfer_method: fileItem.type,
|
||||||
|
url: fileItem.url,
|
||||||
|
upload_file_id: fileItem.fileId,
|
||||||
|
})))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{promptConfig.prompt_variables.length > 0 && (
|
||||||
|
<div className='mt-4 h-[1px] bg-gray-100'></div>
|
||||||
|
)}
|
||||||
|
<div className='w-full mt-4'>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
onClick={onClear}
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<span className='text-[13px]'>{t('common.operation.clear')}</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
variant="primary"
|
||||||
|
disabled={false}
|
||||||
|
>
|
||||||
|
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
|
||||||
|
<span className='text-[13px]'>{t('share.generation.run')}</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default React.memo(RunOnce)
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
.installedApp {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.starIcon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: url(./icons/star.svg) center center no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { CONVERSATION_ID_INFO } from '../base/chat/constants'
|
||||||
|
import { fetchAccessToken } from '@/service/share'
|
||||||
|
|
||||||
|
export const checkOrSetAccessToken = async () => {
|
||||||
|
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
|
||||||
|
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
|
||||||
|
let accessTokenJson = { [sharedToken]: '' }
|
||||||
|
try {
|
||||||
|
accessTokenJson = JSON.parse(accessToken)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
if (!accessTokenJson[sharedToken]) {
|
||||||
|
const res = await fetchAccessToken(sharedToken)
|
||||||
|
accessTokenJson[sharedToken] = res.access_token
|
||||||
|
localStorage.setItem('token', JSON.stringify(accessTokenJson))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setAccessToken = async (sharedToken: string, token: string) => {
|
||||||
|
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
|
||||||
|
let accessTokenJson = { [sharedToken]: '' }
|
||||||
|
try {
|
||||||
|
accessTokenJson = JSON.parse(accessToken)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||||
|
|
||||||
|
accessTokenJson[sharedToken] = token
|
||||||
|
localStorage.setItem('token', JSON.stringify(accessTokenJson))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeAccessToken = () => {
|
||||||
|
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0]
|
||||||
|
|
||||||
|
const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' })
|
||||||
|
let accessTokenJson = { [sharedToken]: '' }
|
||||||
|
try {
|
||||||
|
accessTokenJson = JSON.parse(accessToken)
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(CONVERSATION_ID_INFO)
|
||||||
|
|
||||||
|
delete accessTokenJson[sharedToken]
|
||||||
|
localStorage.setItem('token', JSON.stringify(accessTokenJson))
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
|
import { fetchApoTools } from '@/service/tools'
|
||||||
|
import Conversation from './conversation'
|
||||||
|
import { initApoToolsEntry } from './constant'
|
||||||
|
import type { ApoToolTypeInfo } from './types'
|
||||||
|
import { ApoDisplayDataType } from './types'
|
||||||
|
import Input from '../../base/input'
|
||||||
|
import type { BlockEnum } from '../types'
|
||||||
|
import type { ToolDefaultValue } from '../block-selector/types'
|
||||||
|
import { Portal } from '@headlessui/react'
|
||||||
|
type AINodeRecommendProps = {
|
||||||
|
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||||
|
apoToolType: ApoToolTypeInfo | null,
|
||||||
|
showRecommendModal: boolean
|
||||||
|
closeModal: () => void
|
||||||
|
}
|
||||||
|
const AINodeRecommend = ({
|
||||||
|
onSelect,
|
||||||
|
apoToolType,
|
||||||
|
showRecommendModal,
|
||||||
|
closeModal,
|
||||||
|
}: AINodeRecommendProps) => {
|
||||||
|
const [conversation, setConversation] = useState<any[]>([])
|
||||||
|
const [queryText, setQueryText] = useState('')
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const getAllTools = async () => {
|
||||||
|
const apoTools = await fetchApoTools(apoToolType, queryText)
|
||||||
|
setConversation(prev => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
role: 'ai',
|
||||||
|
text: '以下是推荐的工具',
|
||||||
|
data: apoTools[0],
|
||||||
|
type: ApoDisplayDataType.tool,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
setQueryText('')
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if(apoToolType && showRecommendModal) {
|
||||||
|
setConversation([
|
||||||
|
{
|
||||||
|
role: 'human',
|
||||||
|
text: `请给出${initApoToolsEntry[apoToolType].label}推荐工具`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
getAllTools()
|
||||||
|
}
|
||||||
|
}, [apoToolType, showRecommendModal])
|
||||||
|
|
||||||
|
const handleKeyDown = async (event) => {
|
||||||
|
if (event.key === 'Enter' && !loading) {
|
||||||
|
setLoading(true)
|
||||||
|
setConversation(prev => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
role: 'human',
|
||||||
|
text: queryText,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
getAllTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return showRecommendModal
|
||||||
|
&& <Portal><div className={cn('flex flex-col z-[10000] fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-components-panel-bg shadow-lg border-[0.5px] border-components-panel-border rounded-2xl overflow-y-auto w-1/2 h-3/4')}>
|
||||||
|
{/* header */}
|
||||||
|
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
|
||||||
|
节点推荐
|
||||||
|
<div className='p-1 cursor-pointer' onClick={() => closeModal()}>
|
||||||
|
<RiCloseLine className='w-4 h-4 text-gray-500' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 h-0 flex flex-col overflow-hidden'>
|
||||||
|
<div className='flex-1 overflow-auto'>
|
||||||
|
<Conversation conversation={conversation} onSelect={onSelect} closeModal={closeModal} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className='grow-0 shrink-0 flex items-center justify-center p-4'>
|
||||||
|
<Input
|
||||||
|
showClearIcon
|
||||||
|
wrapperClassName='w-full'
|
||||||
|
value={queryText}
|
||||||
|
onChange={e => setQueryText(e.target.value)}
|
||||||
|
onClear={() => setQueryText('')}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div></Portal>
|
||||||
|
}
|
||||||
|
export default React.memo(AINodeRecommend)
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import type { ApoToolTypeInfo } from './types'
|
||||||
|
|
||||||
|
export const initApoToolsEntry: Record<ApoToolTypeInfo, any> = {
|
||||||
|
select: {
|
||||||
|
label: 'APO异常检测',
|
||||||
|
description: 'APO异常检测分析描述',
|
||||||
|
icon: '',
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
analysis: {
|
||||||
|
label: 'APO查询检测',
|
||||||
|
description: 'APO查询检测',
|
||||||
|
icon: '',
|
||||||
|
type: 'analysis',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useRef,
|
||||||
|
} from 'react'
|
||||||
|
import { createApoStore } from './store'
|
||||||
|
|
||||||
|
type ApoStore = ReturnType<typeof createApoStore>
|
||||||
|
export const ApoContext = createContext<ApoStore | null>(null)
|
||||||
|
|
||||||
|
type ApoProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
export const ApoContextProvider = ({ children }: ApoProviderProps) => {
|
||||||
|
const storeRef = useRef<ApoStore>()
|
||||||
|
|
||||||
|
if (!storeRef.current)
|
||||||
|
storeRef.current = createApoStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApoContext.Provider value={storeRef.current}>
|
||||||
|
{children}
|
||||||
|
</ApoContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { Markdown } from '@/app/components/base/markdown'
|
||||||
|
import { ApoDisplayDataType } from './types'
|
||||||
|
import RecommendTools from './recommend-tools'
|
||||||
|
import type { ToolDefaultValue } from '../block-selector/types'
|
||||||
|
import type { BlockEnum } from '../types'
|
||||||
|
|
||||||
|
type ConversationProps = {
|
||||||
|
conversation: any[]
|
||||||
|
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||||
|
closeModal: () => void
|
||||||
|
|
||||||
|
}
|
||||||
|
const Conversation = ({ conversation, onSelect, closeModal }: ConversationProps) => {
|
||||||
|
return <div className='p-2'>{
|
||||||
|
conversation?.map((con, index) => (
|
||||||
|
<div key={index} className={cn('relative flex ', con.role === 'human' ? 'justify-end' : '')}>
|
||||||
|
|
||||||
|
<div className={cn('rounded p-2 max-w-[80%]')} style={{ backgroundColor: 'rgba(0, 0, 0, 0.04)', color: 'rgba(0, 0, 0, 0.85)' }}>
|
||||||
|
<Markdown content={con.text} />
|
||||||
|
{/* data */}
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
con.type === ApoDisplayDataType.tool && con.data && <>
|
||||||
|
<RecommendTools provider={con.data} onSelect={onSelect} closeModal={closeModal}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* data */}
|
||||||
|
{/* <div>
|
||||||
|
{
|
||||||
|
con.type === ApoDisplayDataType.tool && <>
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}</div>
|
||||||
|
}
|
||||||
|
export default React.memo(Conversation)
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
|
||||||
|
import Tooltip from '../../base/tooltip'
|
||||||
|
import type { OnSelectBlock } from '../types'
|
||||||
|
import { BlockEnum } from '../types'
|
||||||
|
import type { ToolDefaultValue } from '../block-selector/types'
|
||||||
|
|
||||||
|
type RecommendToolsPrps = {
|
||||||
|
provider: any[]
|
||||||
|
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||||
|
closeModal: () => void
|
||||||
|
}
|
||||||
|
const RecommendTools = ({ provider, onSelect, closeModal }: RecommendToolsPrps) => {
|
||||||
|
const language = useLanguage()
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
onSelect(type, toolDefaultValue)
|
||||||
|
closeModal()
|
||||||
|
}, [])
|
||||||
|
return <div>
|
||||||
|
{provider.tools.map(tool => (
|
||||||
|
<Tooltip
|
||||||
|
key={tool.name}
|
||||||
|
position='right'
|
||||||
|
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||||
|
popupContent={(
|
||||||
|
<div>
|
||||||
|
{/* <BlockIcon
|
||||||
|
size='md'
|
||||||
|
className='mb-2'
|
||||||
|
type={BlockEnum.Tool}
|
||||||
|
toolIcon={provider.icon}
|
||||||
|
/> */}
|
||||||
|
<div className='mb-1 text-sm leading-5 text-gray-900'>{tool.label[language]}</div>
|
||||||
|
<div className='text-xs text-gray-700 leading-[18px]'>{tool.description[language]}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
key={tool.name}
|
||||||
|
className='rounded-lg pl-[21px] hover:bg-state-base-hover cursor-pointer'
|
||||||
|
onClick={(e) => {
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
if (tool.parameters) {
|
||||||
|
tool.parameters.forEach((item) => {
|
||||||
|
params[item.name] = ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
e.stopPropagation()
|
||||||
|
handleSelect(BlockEnum.Tool, {
|
||||||
|
provider_id: provider.id,
|
||||||
|
provider_type: provider.type,
|
||||||
|
provider_name: provider.name,
|
||||||
|
tool_name: tool.name,
|
||||||
|
tool_label: tool.label[language],
|
||||||
|
title: tool.label[language],
|
||||||
|
is_team_authorization: provider.is_team_authorization,
|
||||||
|
output_schema: tool.output_schema,
|
||||||
|
paramSchemas: tool.parameters,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='h-8 leading-8 border-l-2 border-divider-subtle pl-4 truncate text-text-secondary system-sm-medium'>{tool.label[language]}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip >
|
||||||
|
))}</div>
|
||||||
|
}
|
||||||
|
export default React.memo(RecommendTools)
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
const OperatorSider = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||||
|
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
} = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||||
|
const { newNode } = generateNewNode({
|
||||||
|
data: {
|
||||||
|
...NODES_INITIAL_DATA[type],
|
||||||
|
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||||
|
...(toolDefaultValue || {}),
|
||||||
|
_isCandidate: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
workflowStore.setState({
|
||||||
|
candidateNode: newNode,
|
||||||
|
})
|
||||||
|
}, [store, workflowStore, t])
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { useContext } from 'react'
|
||||||
|
import {
|
||||||
|
useStore as useZustandStore,
|
||||||
|
} from 'zustand'
|
||||||
|
import { createStore } from 'zustand/vanilla'
|
||||||
|
import { ApoContext } from './context'
|
||||||
|
|
||||||
|
type Shape = {
|
||||||
|
showRecommendModal: boolean
|
||||||
|
setShowRecommendModal: (showRecommendModal: boolean,) => void
|
||||||
|
apoToolType: string
|
||||||
|
setApoToolType: (apoToolType: any[],) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createApoStore = () => {
|
||||||
|
return createStore<Shape>(set => ({
|
||||||
|
showRecommendModal: false,
|
||||||
|
setShowRecommendModal: (show) => {
|
||||||
|
set({ showRecommendModal: show })
|
||||||
|
},
|
||||||
|
apoToolType: '',
|
||||||
|
setApoToolType: apoToolType => set(() => ({ apoToolType })),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStore<T>(selector: (state: Shape) => T): T {
|
||||||
|
const store = useContext(ApoContext)
|
||||||
|
if (!store)
|
||||||
|
throw new Error('Missing ApoContext.Provider in the tree')
|
||||||
|
|
||||||
|
return useZustandStore(store, selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useApoStore = () => {
|
||||||
|
return useContext(ApoContext)!
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export type ApoToolTypeInfo = 'select' | 'analysis' | 'rule'
|
||||||
|
export enum ApoDisplayDataType {
|
||||||
|
metric = 'metric',
|
||||||
|
toplogy = 'toplogy',
|
||||||
|
alert = 'alert',
|
||||||
|
log = 'log',
|
||||||
|
tool = 'tool',
|
||||||
|
}
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
|
import BlockIcon from '../block-icon'
|
||||||
|
import { BlockEnum } from '../types'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
import { initApoToolsEntry } from '../apo/constant'
|
||||||
|
import AINodeRecommend from '@/app/components/workflow/apo/ai-node-recommend'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import type { ToolDefaultValue } from './types'
|
||||||
|
type APOToolsProps = {
|
||||||
|
searchText: string;
|
||||||
|
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
|
||||||
|
}
|
||||||
|
const APOTools = ({ searchText, onSelect }: APOToolsProps) => {
|
||||||
|
const [apoToolType, setApoToolType] = useState()
|
||||||
|
const [showRecommendModal, setShowRecommendModal] = useState()
|
||||||
|
const handleSelect = (type: BlockEnum) => {
|
||||||
|
onSelect(type)
|
||||||
|
setShowRecommendModal(false)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-1 last-of-type:mb-0">
|
||||||
|
{Object.entries(initApoToolsEntry).map(([key, tool]) => (
|
||||||
|
<Tooltip
|
||||||
|
key={key}
|
||||||
|
position="right"
|
||||||
|
popupClassName="w-[200px]"
|
||||||
|
popupContent={
|
||||||
|
<div>
|
||||||
|
<BlockIcon
|
||||||
|
size="md"
|
||||||
|
className="mb-2"
|
||||||
|
type={BlockEnum.Tool}
|
||||||
|
toolIcon={tool.icon}
|
||||||
|
/>
|
||||||
|
<div className={cn('grow text-sm text-gray-900 truncate')}>
|
||||||
|
{tool.label}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-700 leading-[18px]">
|
||||||
|
{tool.description}
|
||||||
|
</div>
|
||||||
|
{/* <div className={cn('grow text-sm text-gray-900 truncate')}>{tool.label[language]}</div>
|
||||||
|
<div className='text-xs text-gray-700 leading-[18px]'>{tool.description[language]}</div> */}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
key={tool.type}
|
||||||
|
className="flex items-center px-3 w-full h-8 rounded-lg hover:bg-state-base-hover cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setApoToolType(tool.type)
|
||||||
|
setShowRecommendModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlockIcon
|
||||||
|
className="mr-2 shrink-0"
|
||||||
|
type={BlockEnum.Tool}
|
||||||
|
toolIcon={tool.icon}
|
||||||
|
/>
|
||||||
|
<div className={cn('grow text-sm text-gray-900 truncate')}>
|
||||||
|
{tool.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<AINodeRecommend
|
||||||
|
onSelect={handleSelect}
|
||||||
|
apoToolType={apoToolType}
|
||||||
|
showRecommendModal={showRecommendModal}
|
||||||
|
closeModal={() => {
|
||||||
|
setApoToolType(null)
|
||||||
|
setShowRecommendModal(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default APOTools
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { BlockEnum, type OnSelectBlock } from '../types'
|
||||||
|
import BlockSelector from '../block-selector'
|
||||||
|
import { NODES_INITIAL_DATA } from '../constants'
|
||||||
|
import { useAvailableBlocks } from '../hooks'
|
||||||
|
import { useWorkflowStore } from '../store'
|
||||||
|
import { generateNewNode } from '../utils'
|
||||||
|
|
||||||
|
const OperatorSider = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||||
|
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
} = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||||
|
const { newNode } = generateNewNode({
|
||||||
|
data: {
|
||||||
|
...NODES_INITIAL_DATA[type],
|
||||||
|
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||||
|
...(toolDefaultValue || {}),
|
||||||
|
_isCandidate: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log(newNode)
|
||||||
|
workflowStore.setState({
|
||||||
|
candidateNode: newNode,
|
||||||
|
})
|
||||||
|
}, [store, workflowStore, t])
|
||||||
|
return <div className="shrink-0 flex flex-col w-[326px] border-r border-divider-burn transition-all h-full relative">
|
||||||
|
|
||||||
|
{/* <div className="shrink-0 flex flex-col w-[1px] border-r border-divider-burn transition-all h-full relative"> */}
|
||||||
|
<BlockSelector
|
||||||
|
open={true}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
placement="left-end"
|
||||||
|
popupClassName='!min-w-[326px] h-full'
|
||||||
|
availableBlocksTypes={availableNextBlocks}
|
||||||
|
absolute={false}
|
||||||
|
/>
|
||||||
|
{/* </div> */}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
export default OperatorSider
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import LineChart from '../../base/line-chart'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Topology from '../../base/topology'
|
||||||
|
import { usePrepareTopologyData } from '../../base/topology/hook'
|
||||||
|
import AlertList from '../../base/alert'
|
||||||
|
import LogContent from '../../base/log/LogContent'
|
||||||
|
|
||||||
|
type DataDisplayProps = {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
const TopologyCom = ({ data }) => {
|
||||||
|
const topologyData = usePrepareTopologyData(data)
|
||||||
|
|
||||||
|
return <div className=' w-full h-[400px] relative'><Topology data={topologyData} /></div>
|
||||||
|
}
|
||||||
|
const DataDisplay = ({ data }: DataDisplayProps) => {
|
||||||
|
// const [chartData,setChartData] = useState<any>(null)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
let chartData: any = null
|
||||||
|
try {
|
||||||
|
chartData = JSON.parse(data)
|
||||||
|
if(chartData.data?.message) chartData = { type: 'error', data: chartData.data?.message }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('JSON 解析失败:', error)
|
||||||
|
chartData = {} // 返回一个空对象或其他默认值
|
||||||
|
}
|
||||||
|
console.log(chartData)
|
||||||
|
return (
|
||||||
|
<div className="relative w-full">
|
||||||
|
{
|
||||||
|
chartData?.type
|
||||||
|
&& <>
|
||||||
|
<div className="h-6 py-1 text-text-tertiary system-xs-medium-uppercase">
|
||||||
|
{t('apo.chart.chartTitle')}
|
||||||
|
</div>
|
||||||
|
<div className='px-1'>
|
||||||
|
{['cpu', 'network', 'memory'].includes(chartData?.type) && (
|
||||||
|
<LineChart type={chartData?.type} data={chartData?.data} />
|
||||||
|
)}
|
||||||
|
{chartData?.type === 'topology' && (
|
||||||
|
<TopologyCom data={chartData?.data} />
|
||||||
|
)}
|
||||||
|
{chartData?.type === 'alert' && (
|
||||||
|
<AlertList {...chartData?.data} />
|
||||||
|
)}
|
||||||
|
{chartData?.type === 'log' && (
|
||||||
|
<LogContent {...chartData?.data} />
|
||||||
|
)}
|
||||||
|
{
|
||||||
|
chartData?.type === 'error' && <span>{chartData?.data}</span>
|
||||||
|
}
|
||||||
|
{/* {
|
||||||
|
!chartData?.type && <>获取数据失败</>
|
||||||
|
} */}
|
||||||
|
</div></>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default memo(DataDisplay)
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { memo, useCallback } from 'react'
|
||||||
|
import BlockSelector from './block-selector'
|
||||||
|
import type { OnSelectBlock } from './types'
|
||||||
|
import { BlockEnum } from './types'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
import { useWorkflowStore } from './store'
|
||||||
|
import { useAvailableBlocks } from './hooks'
|
||||||
|
import { generateNewNode } from './utils'
|
||||||
|
import { NODES_INITIAL_DATA } from './constants'
|
||||||
|
|
||||||
|
const ToolSider = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
} = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||||
|
const { newNode } = generateNewNode({
|
||||||
|
data: {
|
||||||
|
...NODES_INITIAL_DATA[type],
|
||||||
|
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||||
|
...(toolDefaultValue || {}),
|
||||||
|
_isCandidate: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
workflowStore.setState({
|
||||||
|
candidateNode: newNode,
|
||||||
|
})
|
||||||
|
}, [store, workflowStore, t])
|
||||||
|
return <div className='w-[328px] h-full'>
|
||||||
|
<BlockSelector
|
||||||
|
open={true}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
placement='top'
|
||||||
|
offset={0}
|
||||||
|
absolute={false}
|
||||||
|
popupClassName='!w-[328px]'
|
||||||
|
availableBlocksTypes={availableNextBlocks}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
export default memo(ToolSider)
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
const translation = {
|
||||||
|
toolType: {
|
||||||
|
select: 'APO Select',
|
||||||
|
analysis: 'APO Analysis',
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
chartTitle: 'Chart Display',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
const translation = {
|
||||||
|
toolType: {
|
||||||
|
select: 'APO查询数据',
|
||||||
|
analysis: 'APO异常检测',
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
chartTitle: '图表渲染',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
const translation = {}
|
||||||
|
|
||||||
|
export default translation
|
||||||
Loading…
Reference in New Issue