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 (
);
}
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 ? (
) : (
<>
>
)}
);
}
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 ? (
暂无会话记录,点击「新开会话」开始
) : (
<>
{runs.map((run) => (
{
setRenamingId(null);
setRenameValue('');
}}
onStartRename={handleStartRename}
/>
))}
{runListNeedsCollapse && (
)}
>
)}
>
);
const renderNodeProgress = () => {
if (!graphPipeline) {
return 暂无流水线数据
;
}
return (
);
};
const renderLeftBrowseView = () => (
节点进度
基于各步骤上文引用(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 (
{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;