Codex/generation task reliability #20

Merged
stringadmin merged 20 commits from codex/generation-task-reliability into master 2026-06-08 05:56:38 +00:00
2 changed files with 122 additions and 169 deletions
Showing only changes of commit d68064f529 - Show all commits
+23 -115
View File
@@ -186,7 +186,7 @@ import {
} from "./canvasWorkflowDeserialize"; } from "./canvasWorkflowDeserialize";
import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents"; import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents";
import type { CanvasNodeToolbarAction } from "./canvasComponents"; import type { CanvasNodeToolbarAction } from "./canvasComponents";
import { CanvasTextPromptComposer } from "./CanvasTextPromptComposer"; import { CanvasPromptMentionTextarea, CanvasTextPromptComposer } from "./CanvasTextPromptComposer";
import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels"; import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels";
import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing"; import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing";
@@ -198,7 +198,6 @@ const canvasEnterpriseVideoModelOptions: CanvasOption[] = ENTERPRISE_VIDEO_MODEL
// --- Canvas generation keep-alive (survives page refresh / view switch) --- // --- Canvas generation keep-alive (survives page refresh / view switch) ---
const MENTION_TOKEN_RE = /@(?:图片|视频|文本)\d+/g; const MENTION_TOKEN_RE = /@(?:图片|视频|文本)\d+/g;
const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/;
function buildNodeMentionOptions( function buildNodeMentionOptions(
kind: CanvasNodeKind, kind: CanvasNodeKind,
@@ -4434,38 +4433,7 @@ function CanvasPage({
</button> </button>
</div> </div>
) : null} ) : null}
{imageNodeActive && !isCanvasNodeMoving && !imageNodeFocusActive ? (() => { {imageNodeActive && !isCanvasNodeMoving && !imageNodeFocusActive ? (
const imgMentionOptions = buildNodeMentionOptions("image", imageNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks);
const imgMentionState = textNodeMentionStates[imageNode.id] || { open: false, query: "", start: 0, caret: 0, activeIndex: 0 };
const imgFilteredMentions = imgMentionState.open
? imgMentionOptions.filter((o) => !imgMentionState.query || o.searchText.includes(imgMentionState.query.toLowerCase()))
: [];
const handleImagePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const caret = e.target.selectionStart || 0;
updateImageNodePrompt(imageNode.id, value);
const beforeCaret = value.slice(0, caret);
const atIdx = beforeCaret.lastIndexOf("@");
if (atIdx >= 0) {
const query = beforeCaret.slice(atIdx + 1);
if (!MENTION_BOUNDARY_RE.test(query) && !query.includes(" ")) {
setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { open: true, query, start: atIdx, caret, activeIndex: 0 } }));
return;
}
}
closeTextNodeMention(imageNode.id);
};
const handleImagePromptKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!imgMentionState.open || imgFilteredMentions.length === 0) return;
if (e.key === "ArrowDown") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { ...imgMentionState, activeIndex: (imgMentionState.activeIndex + 1) % imgFilteredMentions.length } })); }
else if (e.key === "ArrowUp") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { ...imgMentionState, activeIndex: (imgMentionState.activeIndex - 1 + imgFilteredMentions.length) % imgFilteredMentions.length } })); }
else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const opt = imgFilteredMentions[imgMentionState.activeIndex]; if (opt) insertTextNodeMention(imageNode.id, opt, e.currentTarget, "image"); }
else if (e.key === "Escape") { e.preventDefault(); closeTextNodeMention(imageNode.id); }
};
return (
<div className="studio-canvas-image-composer"> <div className="studio-canvas-image-composer">
<div className="studio-canvas-image-composer__tools"> <div className="studio-canvas-image-composer__tools">
<button <button
@@ -4586,28 +4554,18 @@ function CanvasPage({
</button> </button>
<button type="button" className="studio-canvas-image-composer__expand" aria-label="展开"></button> <button type="button" className="studio-canvas-image-composer__expand" aria-label="展开"></button>
</div> </div>
<div className="studio-canvas-text-composer__input-wrap"> <CanvasPromptMentionTextarea
<textarea nodeId={imageNode.id}
value={imageNode.prompt} value={imageNode.prompt}
onMouseDown={(event) => event.stopPropagation()}
onChange={handleImagePromptChange}
onKeyDown={handleImagePromptKeyDown}
placeholder="描述你想要生成的画面内容,按/呼出指令,@引用素材" placeholder="描述你想要生成的画面内容,按/呼出指令,@引用素材"
mentionOptions={buildNodeMentionOptions("image", imageNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks)}
mentionState={textNodeMentionStates[imageNode.id]}
mentionKind="image"
onPromptChange={updateImageNodePrompt}
onMentionStateChange={setTextNodeMentionStates}
onCloseMention={closeTextNodeMention}
onInsertMention={insertTextNodeMention}
/> />
{imgMentionState.open && (
<div className="studio-canvas-mention-panel">
{imgFilteredMentions.length > 0 ? imgFilteredMentions.map((opt, idx) => (
<button key={opt.token} type="button" className={`studio-canvas-mention-item${idx === imgMentionState.activeIndex ? " is-active" : ""}`} onMouseDown={(e) => { e.preventDefault(); insertTextNodeMention(imageNode.id, opt, e.currentTarget.closest(".studio-canvas-image-composer")?.querySelector("textarea") || null, "image"); }}>
<span className="studio-canvas-mention-thumb">{opt.kind === "image" && opt.previewUrl ? <img src={opt.previewUrl} alt="" /> : opt.kind === "image" ? "🖼" : opt.kind === "video" ? "🎬" : "📝"}</span>
<span className="studio-canvas-mention-label">{opt.nodeTitle}</span>
<span className="studio-canvas-mention-token">{opt.token}</span>
</button>
)) : (
<div className="studio-canvas-mention-item" style={{ opacity: 0.5, pointerEvents: "none" }}><span className="studio-canvas-mention-label"></span></div>
)}
</div>
)}
</div>
<div className="studio-canvas-image-composer__footer"> <div className="studio-canvas-image-composer__footer">
<CanvasSelectChip <CanvasSelectChip
ariaLabel="选择生图模型" ariaLabel="选择生图模型"
@@ -4675,7 +4633,7 @@ function CanvasPage({
</button> </button>
</div> </div>
</div> </div>
); })() : null} ) : null}
</div> </div>
</div> </div>
); );
@@ -4830,38 +4788,7 @@ function CanvasPage({
onMouseDown={(event) => handleNodeResizeStart(event, "video", videoNode.id, videoNode.size)} onMouseDown={(event) => handleNodeResizeStart(event, "video", videoNode.id, videoNode.size)}
/> />
</div> </div>
{videoNodeActive && !isCanvasNodeMoving ? (() => { {videoNodeActive && !isCanvasNodeMoving ? (
const vidMentionOptions = buildNodeMentionOptions("video", videoNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks);
const vidMentionState = textNodeMentionStates[videoNode.id] || { open: false, query: "", start: 0, caret: 0, activeIndex: 0 };
const vidFilteredMentions = vidMentionState.open
? vidMentionOptions.filter((o) => !vidMentionState.query || o.searchText.includes(vidMentionState.query.toLowerCase()))
: [];
const handleVideoPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
const caret = e.target.selectionStart || 0;
updateVideoNodePrompt(videoNode.id, value);
const beforeCaret = value.slice(0, caret);
const atIdx = beforeCaret.lastIndexOf("@");
if (atIdx >= 0) {
const query = beforeCaret.slice(atIdx + 1);
if (!MENTION_BOUNDARY_RE.test(query) && !query.includes(" ")) {
setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { open: true, query, start: atIdx, caret, activeIndex: 0 } }));
return;
}
}
closeTextNodeMention(videoNode.id);
};
const handleVideoPromptKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!vidMentionState.open || vidFilteredMentions.length === 0) return;
if (e.key === "ArrowDown") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { ...vidMentionState, activeIndex: (vidMentionState.activeIndex + 1) % vidFilteredMentions.length } })); }
else if (e.key === "ArrowUp") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { ...vidMentionState, activeIndex: (vidMentionState.activeIndex - 1 + vidFilteredMentions.length) % vidFilteredMentions.length } })); }
else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const opt = vidFilteredMentions[vidMentionState.activeIndex]; if (opt) insertTextNodeMention(videoNode.id, opt, e.currentTarget, "video"); }
else if (e.key === "Escape") { e.preventDefault(); closeTextNodeMention(videoNode.id); }
};
return (
<div className="studio-canvas-video-composer"> <div className="studio-canvas-video-composer">
<div className="studio-canvas-video-composer__tabs studio-canvas-video-composer__mode-tabs"> <div className="studio-canvas-video-composer__tabs studio-canvas-video-composer__mode-tabs">
<button <button
@@ -4983,37 +4910,18 @@ function CanvasPage({
<button type="button"></button> <button type="button"></button>
<button type="button" className="is-active"></button> <button type="button" className="is-active"></button>
</div> </div>
<div className="studio-canvas-text-composer__input-wrap"> <CanvasPromptMentionTextarea
<textarea nodeId={videoNode.id}
value={videoNode.prompt} value={videoNode.prompt}
onMouseDown={(event) => event.stopPropagation()}
onChange={handleVideoPromptChange}
onKeyDown={handleVideoPromptKeyDown}
placeholder="根据文字描述生成视频。" placeholder="根据文字描述生成视频。"
mentionOptions={buildNodeMentionOptions("video", videoNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks)}
mentionState={textNodeMentionStates[videoNode.id]}
mentionKind="video"
onPromptChange={updateVideoNodePrompt}
onMentionStateChange={setTextNodeMentionStates}
onCloseMention={closeTextNodeMention}
onInsertMention={insertTextNodeMention}
/> />
{vidMentionState.open ? (
<div className="studio-canvas-mention-panel">
{vidFilteredMentions.length > 0 ? vidFilteredMentions.map((opt, idx) => (
<button
key={opt.token}
type="button"
className={`studio-canvas-mention-item${idx === vidMentionState.activeIndex ? " is-active" : ""}`}
onMouseDown={(ev) => { ev.preventDefault(); insertTextNodeMention(videoNode.id, opt, ev.currentTarget.closest(".studio-canvas-text-composer__input-wrap")?.querySelector("textarea")!, "video"); }}
>
<span className={`studio-canvas-mention-icon studio-canvas-mention-icon--${opt.kind}`}>
{opt.kind === "image" ? "🖼" : opt.kind === "video" ? "🎬" : "📝"}
</span>
<span className="studio-canvas-mention-label">{opt.nodeTitle}</span>
<span className="studio-canvas-mention-token">{opt.token}</span>
</button>
)) : (
<div className="studio-canvas-mention-item" style={{ opacity: 0.5, pointerEvents: "none" }}>
<span className="studio-canvas-mention-label"></span>
</div>
)}
</div>
) : null}
</div>
<div className="studio-canvas-video-composer__footer studio-canvas-video-composer__settings"> <div className="studio-canvas-video-composer__footer studio-canvas-video-composer__settings">
<CanvasSelectChip <CanvasSelectChip
ariaLabel="选择视频模型" ariaLabel="选择视频模型"
@@ -5097,7 +5005,7 @@ function CanvasPage({
</button> </button>
</div> </div>
</div> </div>
); })() : null} ) : null}
</div> </div>
</div> </div>
); );
@@ -1,6 +1,6 @@
import { SendOutlined } from "@ant-design/icons"; import { SendOutlined } from "@ant-design/icons";
import type { CSSProperties, Dispatch, SetStateAction } from "react"; import { useRef, type CSSProperties, type Dispatch, type SetStateAction } from "react";
import type { CanvasPromptMentionOption, CanvasPromptMentionState } from "./canvasTypes"; import type { CanvasNodeKind, CanvasPromptMentionOption, CanvasPromptMentionState } from "./canvasTypes";
const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/; const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/;
const EMPTY_MENTION_STYLE: CSSProperties = { opacity: 0.5, pointerEvents: "none" }; const EMPTY_MENTION_STYLE: CSSProperties = { opacity: 0.5, pointerEvents: "none" };
@@ -12,11 +12,10 @@ const DEFAULT_MENTION_STATE: CanvasPromptMentionState = {
activeIndex: 0, activeIndex: 0,
}; };
interface CanvasTextPromptComposerProps { interface CanvasPromptMentionTextareaProps {
nodeId: string; nodeId: string;
prompt: string; value: string;
canGenerate: boolean; placeholder: string;
isGenerating: boolean;
mentionOptions: CanvasPromptMentionOption[]; mentionOptions: CanvasPromptMentionOption[];
mentionState?: CanvasPromptMentionState; mentionState?: CanvasPromptMentionState;
onPromptChange: (nodeId: string, prompt: string) => void; onPromptChange: (nodeId: string, prompt: string) => void;
@@ -26,23 +25,24 @@ interface CanvasTextPromptComposerProps {
nodeId: string, nodeId: string,
option: CanvasPromptMentionOption, option: CanvasPromptMentionOption,
textarea: HTMLTextAreaElement | null, textarea: HTMLTextAreaElement | null,
kind?: CanvasNodeKind,
) => void; ) => void;
onGenerate: (nodeId: string) => void | Promise<void>; mentionKind?: CanvasNodeKind;
} }
export function CanvasTextPromptComposer({ export function CanvasPromptMentionTextarea({
nodeId, nodeId,
prompt, value,
canGenerate, placeholder,
isGenerating,
mentionOptions, mentionOptions,
mentionState = DEFAULT_MENTION_STATE, mentionState = DEFAULT_MENTION_STATE,
onPromptChange, onPromptChange,
onMentionStateChange, onMentionStateChange,
onCloseMention, onCloseMention,
onInsertMention, onInsertMention,
onGenerate, mentionKind,
}: CanvasTextPromptComposerProps) { }: CanvasPromptMentionTextareaProps) {
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const filteredMentions = mentionState.open const filteredMentions = mentionState.open
? mentionOptions.filter((option) => !mentionState.query || option.searchText.includes(mentionState.query.toLowerCase())) ? mentionOptions.filter((option) => !mentionState.query || option.searchText.includes(mentionState.query.toLowerCase()))
: []; : [];
@@ -88,7 +88,7 @@ export function CanvasTextPromptComposer({
event.preventDefault(); event.preventDefault();
const option = filteredMentions[mentionState.activeIndex]; const option = filteredMentions[mentionState.activeIndex];
if (option) { if (option) {
onInsertMention(nodeId, option, event.currentTarget); onInsertMention(nodeId, option, event.currentTarget, mentionKind);
} }
} else if (event.key === "Escape") { } else if (event.key === "Escape") {
event.preventDefault(); event.preventDefault();
@@ -105,48 +105,93 @@ export function CanvasTextPromptComposer({
}); });
}; };
return (
<div className="studio-canvas-text-composer__input-wrap">
<textarea
ref={textareaRef}
value={value}
onMouseDown={(event) => event.stopPropagation()}
onChange={handlePromptChange}
onKeyDown={handlePromptKeyDown}
onSelect={handlePromptSelect}
placeholder={placeholder}
/>
{mentionState.open ? (
<div className="studio-canvas-mention-panel">
{filteredMentions.length > 0 ? filteredMentions.map((option, index) => (
<button
key={option.token}
type="button"
className={`studio-canvas-mention-item${index === mentionState.activeIndex ? " is-active" : ""}`}
onMouseDown={(event) => {
event.preventDefault();
onInsertMention(nodeId, option, textareaRef.current, mentionKind);
}}
>
<span className="studio-canvas-mention-thumb">
{option.kind === "image" && option.previewUrl ? (
<img src={option.previewUrl} alt="" />
) : option.kind === "image" ? "🖼" : option.kind === "video" ? "🎬" : "📝"}
</span>
<span className="studio-canvas-mention-label">{option.nodeTitle}</span>
<span className="studio-canvas-mention-token">{option.token}</span>
</button>
)) : (
<div className="studio-canvas-mention-item" style={EMPTY_MENTION_STYLE}>
<span className="studio-canvas-mention-label"></span>
</div>
)}
</div>
) : null}
</div>
);
}
interface CanvasTextPromptComposerProps {
nodeId: string;
prompt: string;
canGenerate: boolean;
isGenerating: boolean;
mentionOptions: CanvasPromptMentionOption[];
mentionState?: CanvasPromptMentionState;
onPromptChange: (nodeId: string, prompt: string) => void;
onMentionStateChange: Dispatch<SetStateAction<Record<string, CanvasPromptMentionState>>>;
onCloseMention: (nodeId: string) => void;
onInsertMention: (
nodeId: string,
option: CanvasPromptMentionOption,
textarea: HTMLTextAreaElement | null,
kind?: CanvasNodeKind,
) => void;
onGenerate: (nodeId: string) => void | Promise<void>;
}
export function CanvasTextPromptComposer({
nodeId,
prompt,
canGenerate,
isGenerating,
mentionOptions,
mentionState,
onPromptChange,
onMentionStateChange,
onCloseMention,
onInsertMention,
onGenerate,
}: CanvasTextPromptComposerProps) {
return ( return (
<div className="studio-canvas-text-composer"> <div className="studio-canvas-text-composer">
<div className="studio-canvas-text-composer__input-wrap"> <CanvasPromptMentionTextarea
<textarea nodeId={nodeId}
value={prompt} value={prompt}
onMouseDown={(event) => event.stopPropagation()} placeholder="写下你想讲的故事、场景或角色设定。@引用连接的节点"
onChange={handlePromptChange} mentionOptions={mentionOptions}
onKeyDown={handlePromptKeyDown} mentionState={mentionState}
onSelect={handlePromptSelect} onPromptChange={onPromptChange}
placeholder="写下你想讲的故事、场景或角色设定。@引用连接的节点" onMentionStateChange={onMentionStateChange}
/> onCloseMention={onCloseMention}
{mentionState.open ? ( onInsertMention={onInsertMention}
<div className="studio-canvas-mention-panel"> />
{filteredMentions.length > 0 ? filteredMentions.map((option, index) => (
<button
key={option.token}
type="button"
className={`studio-canvas-mention-item${index === mentionState.activeIndex ? " is-active" : ""}`}
onMouseDown={(event) => {
event.preventDefault();
const textarea = event.currentTarget
.closest(".studio-canvas-text-composer")
?.querySelector("textarea") ?? null;
onInsertMention(nodeId, option, textarea);
}}
>
<span className="studio-canvas-mention-thumb">
{option.kind === "image" && option.previewUrl ? (
<img src={option.previewUrl} alt="" />
) : option.kind === "image" ? "🖼" : option.kind === "video" ? "🎬" : "📝"}
</span>
<span className="studio-canvas-mention-label">{option.nodeTitle}</span>
<span className="studio-canvas-mention-token">{option.token}</span>
</button>
)) : (
<div className="studio-canvas-mention-item" style={EMPTY_MENTION_STYLE}>
<span className="studio-canvas-mention-label"></span>
</div>
)}
</div>
) : null}
</div>
<div className="studio-canvas-text-composer__footer"> <div className="studio-canvas-text-composer__footer">
<button <button
type="button" type="button"