import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useAppLayoutStore from '../../Store/AppLayoutSlice'; import useStudioStore from '../../Store/Studio/StudioSlice'; import { buildWorkflowVariableLabelMap, getWorkflowVariableLabel, } from './edit/workflowVariableLabels'; import StudioContextBlockPopup from './StudioContextBlockPopup'; import StudioInsertionPopup from './StudioInsertionPopup'; import StudioRunChat from './StudioRunChat'; import StudioRunNodeGraph from './StudioRunNodeGraph'; import './StudioRunPage.css'; const RUN_STATUS_LABEL = { pending: '待开始', running: '运行中', paused: '已暂停', completed: '已完成', failed: '失败', cancelled: '已取消', }; const RUN_LIST_COLLAPSE_THRESHOLD = 4; const RUN_ITEM_HEIGHT = 56; function formatRunTime(iso) { if (!iso) return ''; try { const d = new Date(iso); return d.toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } catch { return iso; } } function runDisplayName(run) { if (run.title?.trim()) return run.title.trim(); return formatRunTime(run.createdAt) || '未命名运行'; } function InitBindGuidanceForm({ node, runAdvancing, onSubmit, compact = false }) { const displayParams = node?.displayParams || []; const [formValues, setFormValues] = useState({}); const [fieldErrors, setFieldErrors] = useState({}); useEffect(() => { const initial = {}; displayParams.forEach((dp) => { initial[dp.key] = ''; }); setFormValues(initial); setFieldErrors({}); }, [node?.nodeId, displayParams]); const handleChange = (key, value) => { setFormValues((prev) => ({ ...prev, [key]: value })); setFieldErrors((prev) => ({ ...prev, [key]: null })); }; const handleSubmit = (e) => { e.preventDefault(); const errors = {}; displayParams.forEach((dp) => { if (dp.required && !(formValues[dp.key] || '').trim()) { errors[dp.key] = `请填写${dp.label}`; } }); if (Object.keys(errors).length > 0) { setFieldErrors(errors); return; } onSubmit(formValues); }; if (displayParams.length === 0) { return
当前步骤未定义引导字段
; } return (

填写以下信息后将创建角色卡与世界书,并绑定到当前 Studio 项目。

{displayParams.map((dp) => ( ))}
); } function ToolOptionsCarousel({ options, index, onPrev, onNext, onSelect, runAdvancing }) { const current = options[index]; if (!options.length || !current) return null; return (
模型提问 {index + 1} / {options.length}

{current.question}

{(current.options || []).map((opt) => ( ))}
); } function PromptBlocksDebug({ blocks, onBlockClick }) { const [open, setOpen] = useState(false); if (!blocks?.length) return null; return (
{open && (
{blocks.map((block) => ( ))}
)}
); } function RunListItem({ run, selected, onSelect, onDelete, renamingId, renameValue, onRenameChange, onRenameSubmit, onRenameCancel, onStartRename, }) { const isRenaming = renamingId === run.id; return (
  • {isRenaming ? (
    { e.preventDefault(); onRenameSubmit(run.id); }} > onRenameChange(e.target.value)} autoFocus maxLength={120} />
    ) : ( <>
    )}
  • ); } function ProductPreview({ activeNodeState, isWorldbookActive, compact = false }) { let productPreview = '暂无产物'; if (activeNodeState?.lastDraft) { const draft = activeNodeState.lastDraft; for (const key of ['entryContent', 'content', 'text', 'body']) { if (typeof draft[key] === 'string' && draft[key].trim()) { productPreview = draft[key]; break; } } if (productPreview === '暂无产物') { productPreview = JSON.stringify(draft, null, 2); } } else if (isWorldbookActive) { productPreview = '(等待模型生成条目草稿…)'; } return (

    目前产物

    {activeNodeState?.displayName && ( {activeNodeState.displayName} )}
    {productPreview}
    ); } function WorkflowVariablesPanel({ variables, labelMap }) { const entries = Object.entries(variables || {}); if (entries.length === 0) return null; return (

    工作流变量

    {entries.map(([key, value]) => (
    {getWorkflowVariableLabel(key, labelMap)}
    {typeof value === 'string' ? value : JSON.stringify(value)}
    ))}
    ); } function CurrentNodePanel({ activeNodeState, graphCurrentNodeId, graphPipeline }) { const nodeName = activeNodeState?.displayName || graphPipeline?.nodes?.find((n) => n.id === graphCurrentNodeId)?.displayName || '—'; const nodeStatus = activeNodeState?.status; return (

    当前节点

    步骤
    {nodeName}
    {nodeStatus && (
    状态
    {RUN_STATUS_LABEL[nodeStatus] || nodeStatus}
    )} {activeNodeState?.skillId && (
    技能
    {activeNodeState.skillId}
    )}
    ); } function InsertionSidebar({ insertionPreview, onOpenEntry }) { if (!insertionPreview) { return (

    插入内容

    当前步骤无世界书插入配置

    ); } return (

    插入内容

    ); } function StudioRunPage() { const { projects, pipeline: projectPipeline, workflowVariables: workflowVariablesCatalog, runProjectId, runs, currentRunId, currentRun, runLoading, runCreating, runAdvancing, runError, initStudioRun, fetchProject, fetchWorkflowVariables, setRunProjectId, fetchRuns, createRun, selectRun, advanceRun, deleteRun, renameRun, } = useStudioStore(); const sidebarMode = useAppLayoutStore((s) => s.sidebarMode); const isSidebarHovered = useAppLayoutStore((s) => s.isSidebarHovered); const setSidebarHovered = useAppLayoutStore((s) => s.setSidebarHovered); const hoverTimeoutRef = useRef(null); const leaveTimeoutRef = useRef(null); const handleLeftMouseEnter = useCallback(() => { if (sidebarMode !== 'smart') return; if (leaveTimeoutRef.current) clearTimeout(leaveTimeoutRef.current); hoverTimeoutRef.current = setTimeout(() => { setSidebarHovered(true); }, 400); }, [sidebarMode, setSidebarHovered]); const handleLeftMouseLeave = useCallback(() => { if (sidebarMode !== 'smart') return; if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); leaveTimeoutRef.current = setTimeout(() => { setSidebarHovered(false); }, 250); }, [sidebarMode, setSidebarHovered]); useEffect(() => { return () => { if (hoverTimeoutRef.current) clearTimeout(hoverTimeoutRef.current); if (leaveTimeoutRef.current) clearTimeout(leaveTimeoutRef.current); }; }, []); const variableLabelMap = useMemo( () => buildWorkflowVariableLabelMap(workflowVariablesCatalog), [workflowVariablesCatalog] ); const [sessionEntered, setSessionEntered] = useState(false); const [insertionPopupOpen, setInsertionPopupOpen] = useState(false); const [insertionPopupExpanded, setInsertionPopupExpanded] = useState(true); const [contextBlockPopup, setContextBlockPopup] = useState(null); const [chatInput, setChatInput] = useState(''); const [toolOptionIndex, setToolOptionIndex] = useState(0); const [chatMessages, setChatMessages] = useState([]); const [runListExpanded, setRunListExpanded] = useState(false); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); useEffect(() => { initStudioRun(); }, [initStudioRun]); useEffect(() => { if (runProjectId) { fetchProject(runProjectId); } }, [runProjectId, fetchProject]); useEffect(() => { setSessionEntered(false); setInsertionPopupOpen(false); setContextBlockPopup(null); }, [currentRunId, runProjectId]); const activeNodeState = useMemo(() => { if (!currentRun?.currentNodeId) return null; return currentRun.nodeStates?.find((n) => n.nodeId === currentRun.currentNodeId); }, [currentRun]); const activePipelineNode = useMemo(() => { if (!currentRun?.currentNodeId) return null; return currentRun.pipelineSnapshot?.nodes?.find( (n) => n.id === currentRun.currentNodeId ); }, [currentRun]); const isInitBindActive = activeNodeState?.status === 'active' && activeNodeState?.skillId === 'studio.init_bind'; const isWorldbookActive = activeNodeState?.status === 'active' && activeNodeState?.skillId === 'studio.worldbook_entry'; const toolQuestions = useMemo( () => activeNodeState?.lastToolResponse?.questions || [], [activeNodeState] ); useEffect(() => { setToolOptionIndex(0); }, [currentRunId, activeNodeState?.nodeId, toolQuestions.length]); const graphPipeline = currentRun?.pipelineSnapshot || projectPipeline; const graphNodeStates = currentRun?.nodeStates || []; const graphCurrentNodeId = currentRun?.currentNodeId || null; const insertionPreview = useMemo(() => { const ins = activePipelineNode?.config?.insertion; if (!ins) return null; return { key: ins.key || '(未设置关键词)', comment: ins.comment || '', position: ins.position, activationType: ins.activationType, keysecondary: ins.keysecondary || '', }; }, [activePipelineNode]); const promptBlocks = currentRun?.lastPromptBlocks || []; const runWorkflowVariables = currentRun?.workflowVariables || {}; useEffect(() => { if (!currentRun || !sessionEntered) { setChatMessages([]); return; } const msgs = [ { id: 'sys-welcome', role: 'system', text: `已开始运行 · 项目 ${currentRun.projectId}`, }, ]; if (isInitBindActive) { msgs.push({ id: 'sys-init', role: 'assistant', text: '请先创建角色卡与世界书,完成后将进入创作步骤。', }); } else if (isWorldbookActive) { msgs.push({ id: 'sys-step', role: 'assistant', text: `当前步骤:${activeNodeState.displayName}。请在下方输入修改意见或与模型对话(R3 接入流式输出)。`, }); } else if (currentRun.status === 'completed') { msgs.push({ id: 'sys-done', role: 'assistant', text: '本次运行已全部完成。', }); } setChatMessages(msgs); }, [currentRun, currentRunId, isInitBindActive, isWorldbookActive, activeNodeState, sessionEntered]); const handleProjectChange = async (e) => { const projectId = e.target.value; setRunProjectId(projectId); setSessionEntered(false); await fetchWorkflowVariables(projectId); const list = await fetchRuns(projectId); if (list[0]?.id) { await selectRun(list[0].id); } }; const handleNewRun = async () => { if (!runProjectId) return; setSessionEntered(false); await createRun(runProjectId); }; const handleSelectRun = async (runId) => { setSessionEntered(false); await selectRun(runId); }; const handleEnterSession = () => { if (currentRun) { setSessionEntered(true); } }; const handleOpenInsertion = () => { setInsertionPopupExpanded(true); setInsertionPopupOpen(true); }; const handleInitBindSubmit = async (displayParams) => { await advanceRun(displayParams); }; const handleChatSend = (text) => { if (!text || isInitBindActive) return; setChatMessages((prev) => [ ...prev, { id: `user-${Date.now()}`, role: 'user', text }, { id: `stub-${Date.now()}`, role: 'assistant', text: '(R3 占位:模型回复与流式输出尚未接入)', }, ]); setChatInput(''); }; const handleNextStep = () => { window.alert('(R3 占位:确认当前产物后将进入下一步)'); }; const handleDeleteRun = async (runId) => { const name = runDisplayName(runs.find((r) => r.id === runId) || {}); if (!window.confirm(`确定删除运行「${name}」?此操作不可撤销。`)) return; if (currentRunId === runId) setSessionEntered(false); await deleteRun(runId); }; const handleStartRename = (run) => { setRenamingId(run.id); setRenameValue(run.title?.trim() || runDisplayName(run)); }; const handleRenameSubmit = async (runId) => { const title = renameValue.trim(); if (!title) return; const ok = await renameRun(runId, title); if (ok) { setRenamingId(null); setRenameValue(''); } }; const handleToolSelect = (option) => { setChatMessages((prev) => [ ...prev, { id: `tool-opt-${Date.now()}`, role: 'user', text: `[选项] ${option}`, }, ]); }; const runListNeedsCollapse = runs.length > RUN_LIST_COLLAPSE_THRESHOLD; const inSession = sessionEntered && !!currentRun; const showToolCarousel = inSession && isWorldbookActive && toolQuestions.length > 0; const showNextStep = inSession && isWorldbookActive; const renderRunList = () => ( <> {runLoading && runs.length === 0 ? (
    加载中…
    ) : runs.length === 0 ? (
    暂无会话记录,点击「新开会话」开始
    ) : ( <> {runListNeedsCollapse && ( )} )} ); const renderNodeProgress = () => { if (!graphPipeline) { return
    暂无流水线数据
    ; } return ( ); }; const renderLeftBrowseView = () => (

    会话列表

    {renderRunList()}

    节点进度

    基于各步骤上文引用(inputs.ref)生成的依赖顺序。

    {renderNodeProgress()}
    ); const renderLeftSessionView = () => (
    ); const renderRunPreviewCenter = () => { if (!runProjectId) { return
    请先选择项目
    ; } if (!currentRun) { return (

    在左侧选择或新建会话,此处将预览运行内容与节点状态。

    ); } return (

    {runDisplayName(currentRun)}

    {RUN_STATUS_LABEL[currentRun.status] || currentRun.status}
    创建 {formatRunTime(currentRun.createdAt)}
    {promptBlocks.length > 0 && (
    )}
    ); }; const renderSessionCenter = () => { if (isInitBindActive && activePipelineNode) { return (
    ); } return (
    ); }; const renderRightSidebar = () => ( <> {showToolCarousel && ( setToolOptionIndex((i) => (i - 1 + toolQuestions.length) % toolQuestions.length) } onNext={() => setToolOptionIndex((i) => (i + 1) % toolQuestions.length) } onSelect={handleToolSelect} runAdvancing={runAdvancing} /> )} {showNextStep && (

    确认左侧目前产物无误后进入下一节点

    )} ); return (

    创作运行

    {inSession && ( )}
    {runError && (
    {runError}
    )}
    {inSession ? renderSessionCenter() : renderRunPreviewCenter()}
    setContextBlockPopup(null)} /> setInsertionPopupOpen(false)} > {({ expanded }) => expanded ? (
                  {JSON.stringify(insertionPreview, null, 2)}
                
    ) : (
    关键词 {insertionPreview?.key} {insertionPreview?.comment ? ( <> 备注 {insertionPreview.comment} ) : null} 位置 / 激活 {insertionPreview?.position} · {insertionPreview?.activationType}
    ) }
    ); } export default StudioRunPage;