Fix Studio manual node switch state transitions so revisitable steps stay accessible after switching away.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
buildWorkflowVariableLabelMap,
|
||||
getWorkflowVariableLabel,
|
||||
} from './edit/workflowVariableLabels';
|
||||
import { resolveBoundCharacterName, canUndoRun, canRerollRun, canAdvanceNextStep, getRunControls, hasRunControl, shouldShowFirstExport, shouldShowSaveAppendOverwrite, canSaveWorldbookEntry, canSwitchToNode } from './studioRunUtils';
|
||||
import { resolveBoundCharacterName, canUndoRun, canRerollRun, canAdvanceNextStep, getRunControls, hasRunControl, shouldShowFirstExport, shouldShowSaveAppendOverwrite, canSaveWorldbookEntry, canSwitchToNode, isInitBindCompleted } from './studioRunUtils';
|
||||
import useCharacterStore from '../../Store/SideBarLeft/CharacterSlice';
|
||||
|
||||
import StudioContextBlockPopup from './StudioContextBlockPopup';
|
||||
@@ -584,9 +584,10 @@ function StudioRunPage() {
|
||||
);
|
||||
}, [currentRun]);
|
||||
|
||||
const isInitBindActive =
|
||||
const isInitBindPending =
|
||||
activeNodeState?.status === 'active' &&
|
||||
activeNodeState?.skillId === 'studio.init_bind';
|
||||
activeNodeState?.skillId === 'studio.init_bind' &&
|
||||
!isInitBindCompleted(activeNodeState);
|
||||
|
||||
const runControls = useMemo(
|
||||
() => getRunControls(activeNodeState?.skillId, skillTemplates),
|
||||
@@ -702,7 +703,7 @@ function StudioRunPage() {
|
||||
};
|
||||
|
||||
const handleChatSend = async (text) => {
|
||||
if (!text || isInitBindActive || !isWorldbookActive || runMessaging) return;
|
||||
if (!text || isInitBindPending || !isWorldbookActive || runMessaging) return;
|
||||
|
||||
setChatInput('');
|
||||
|
||||
@@ -774,13 +775,25 @@ function StudioRunPage() {
|
||||
|
||||
const handleGraphNodeSelect = useCallback(
|
||||
async (nodeId) => {
|
||||
if (!runSessionEntered || !currentRun || nodeId === currentRun.currentNodeId) return;
|
||||
if (!currentRun || nodeId === currentRun.currentNodeId) return;
|
||||
if (runLoading || runAdvancing || runMessaging) return;
|
||||
const nodeState = currentRun.nodeStates?.find((n) => n.nodeId === nodeId);
|
||||
if (!canSwitchToNode(nodeState)) return;
|
||||
clearConfirmPending();
|
||||
await switchRunNode(nodeId);
|
||||
const run = await switchRunNode(nodeId);
|
||||
if (run) {
|
||||
setChatInput('');
|
||||
setToolOptionIndex(0);
|
||||
}
|
||||
},
|
||||
[runSessionEntered, currentRun, switchRunNode, clearConfirmPending]
|
||||
[
|
||||
currentRun,
|
||||
runLoading,
|
||||
runAdvancing,
|
||||
runMessaging,
|
||||
switchRunNode,
|
||||
clearConfirmPending,
|
||||
]
|
||||
);
|
||||
|
||||
const handleDeleteRun = async (runId) => {
|
||||
@@ -887,7 +900,7 @@ function StudioRunPage() {
|
||||
nodeStates={graphNodeStates}
|
||||
currentNodeId={graphCurrentNodeId}
|
||||
variant="sidebar"
|
||||
onNodeSelect={runSessionEntered ? handleGraphNodeSelect : undefined}
|
||||
onNodeSelect={currentRun ? handleGraphNodeSelect : undefined}
|
||||
canSelectNode={canSwitchToNode}
|
||||
/>
|
||||
);
|
||||
@@ -915,6 +928,13 @@ function StudioRunPage() {
|
||||
activeNodeState={activeNodeState}
|
||||
isWorldbookActive={isWorldbookActive}
|
||||
/>
|
||||
<section className="studio-run-left-session__graph">
|
||||
<h2 className="studio-run-section-title">节点进度</h2>
|
||||
<p className="studio-run-hint studio-run-hint--inline">
|
||||
点击已完成、进行中或已有草稿的步骤可切换当前焦点。
|
||||
</p>
|
||||
{renderNodeProgress()}
|
||||
</section>
|
||||
<PromptBlocksDebug
|
||||
blocks={promptBlocks}
|
||||
onBlockClick={setContextBlockPopup}
|
||||
@@ -991,7 +1011,7 @@ function StudioRunPage() {
|
||||
};
|
||||
|
||||
const renderSessionCenter = () => {
|
||||
if (isInitBindActive && activePipelineNode) {
|
||||
if (isInitBindPending && activePipelineNode) {
|
||||
return (
|
||||
<div className="studio-run-chat-card">
|
||||
<InitBindGuidanceForm
|
||||
@@ -1004,6 +1024,18 @@ function StudioRunPage() {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
activeNodeState?.skillId === 'studio.init_bind' &&
|
||||
isInitBindCompleted(activeNodeState)
|
||||
) {
|
||||
return (
|
||||
<div className="studio-run-chat-empty">
|
||||
<p>创建并绑定步骤已完成。</p>
|
||||
<p className="studio-run-hint">绑定信息见左侧「目前产物」。</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="studio-run-chat-panel">
|
||||
<StudioRunChat
|
||||
@@ -1015,7 +1047,7 @@ function StudioRunPage() {
|
||||
onSend={handleChatSend}
|
||||
onInterrupt={canInterrupt ? interruptRun : undefined}
|
||||
canInterrupt={canInterrupt}
|
||||
disabled={!isWorldbookActive || runAdvancing || isInitBindActive}
|
||||
disabled={!isWorldbookActive || runAdvancing || isInitBindPending}
|
||||
sending={runMessaging}
|
||||
streamOutput={streamOutput}
|
||||
onStreamOutputChange={setStreamOutput}
|
||||
|
||||
@@ -85,10 +85,40 @@ export function canSaveWorldbookEntry(activeNodeState) {
|
||||
return Boolean(activeNodeState?.lastDraft?.entryContent?.trim());
|
||||
}
|
||||
|
||||
/** Whether init_bind step has finished creating character + worldbook. */
|
||||
export function isInitBindCompleted(nodeState) {
|
||||
const draft = nodeState?.lastDraft;
|
||||
return Boolean(draft?.characterId && draft?.worldbookId);
|
||||
}
|
||||
|
||||
/** Node has saved progress and may be revisited (even if status was reset to pending). */
|
||||
export function nodeHasRevisitableProgress(nodeState) {
|
||||
if (!nodeState) return false;
|
||||
if (nodeState.status === 'completed') return true;
|
||||
const draft = nodeState.lastDraft;
|
||||
if (nodeState.skillId === 'studio.worldbook_entry') {
|
||||
return Boolean(
|
||||
draft?.entryContent?.trim()
|
||||
|| draft?.writtenEntryUid
|
||||
|| (nodeState.stepMessages?.length ?? 0) > 0
|
||||
);
|
||||
}
|
||||
if (nodeState.skillId === 'studio.init_bind') {
|
||||
return isInitBindCompleted(nodeState) || Boolean(draft?.displayParams);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a pipeline node may be manually selected on the run graph. */
|
||||
export function canSwitchToNode(nodeState) {
|
||||
if (!nodeState || nodeState.skillId !== 'studio.worldbook_entry') return false;
|
||||
return nodeState.status === 'active' || nodeState.status === 'completed';
|
||||
if (!nodeState) return false;
|
||||
if (!['studio.worldbook_entry', 'studio.init_bind'].includes(nodeState.skillId)) {
|
||||
return false;
|
||||
}
|
||||
if (nodeState.status === 'active' || nodeState.status === 'completed') {
|
||||
return true;
|
||||
}
|
||||
return nodeState.status === 'pending' && nodeHasRevisitableProgress(nodeState);
|
||||
}
|
||||
|
||||
const RUN_CONTROL_ALIASES = {
|
||||
|
||||
Reference in New Issue
Block a user