refactor(core): 任务与模型协议治理 - 拆分服务职责并禁止已发起任务取消
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import { Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Square, Video } from "lucide-react";
|
||||
import { Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Settings2, Video } from "lucide-react";
|
||||
import { Button, InputNumber, Segmented, Select } from "antd";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -23,7 +23,6 @@ type CanvasConfigNodePanelProps = {
|
||||
inputSummary: { textCount: number; imageCount: number; videoCount: number; audioCount: number; characterCount: number };
|
||||
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeMetadata>) => void;
|
||||
onGenerate: (nodeId: string) => void;
|
||||
onStop: (nodeId: string) => void;
|
||||
onComposerToggle: () => void;
|
||||
workspaceMode?: CanvasWorkspaceMode;
|
||||
};
|
||||
@@ -40,7 +39,7 @@ const videoOperationOptions: Array<{ label: string; value: CanvasVideoEditOperat
|
||||
{ label: "版本对比", value: "compare_versions" },
|
||||
];
|
||||
|
||||
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onStop, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) {
|
||||
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) {
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
|
||||
@@ -188,17 +187,15 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
<Button
|
||||
type="primary"
|
||||
className="mt-auto !h-9 !w-full !cursor-pointer !rounded-lg"
|
||||
danger={isRunning}
|
||||
disabled={!isRunning && !canGenerate}
|
||||
disabled={isRunning || !canGenerate}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={() => (isRunning ? onStop(node.id) : onGenerate(node.id))}
|
||||
onClick={() => onGenerate(node.id)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{isRunning ? (
|
||||
<>
|
||||
<LoaderCircle className="size-4 animate-spin" />
|
||||
<Square className="size-3.5 fill-current" />
|
||||
<span>停止</span>
|
||||
<span>生成中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react";
|
||||
import { AlertCircle, BookOpenCheck, Clock3, FileText, Image as ImageIcon, LoaderCircle, Music2, Pencil, Play, RefreshCw, Square, Video } from "lucide-react";
|
||||
import { AlertCircle, BookOpenCheck, Clock3, FileText, Image as ImageIcon, LoaderCircle, Music2, Pencil, Play, RefreshCw, Video } from "lucide-react";
|
||||
|
||||
import { VideoPlayer } from "@/components/video-player";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error";
|
||||
@@ -34,7 +34,6 @@ type CanvasNodeContentProps = {
|
||||
onStopEditing: () => void;
|
||||
mentionReferences: CanvasResourceReference[];
|
||||
onRetry?: (node: CanvasNodeData) => void;
|
||||
onCancelTask?: (node: CanvasNodeData) => void;
|
||||
onOpenTaskDetails?: (node: CanvasNodeData) => void;
|
||||
onToggleBatch?: () => void;
|
||||
reduceMediaEffects?: boolean;
|
||||
@@ -49,7 +48,7 @@ export function CanvasNodeContent(props: CanvasNodeContentProps) {
|
||||
|| (props.node.metadata?.workflowKind === "styleboard" && !props.node.metadata.content);
|
||||
if (hasCustomContent && props.renderNodeContent) return props.renderNodeContent(props.node);
|
||||
if (props.isBatchRoot) return <ImageNodeContent {...props} />;
|
||||
if (props.node.metadata?.status === "loading") return <LoadingContent node={props.node} theme={props.theme} onCancelTask={props.onCancelTask} onOpenTaskDetails={props.onOpenTaskDetails} />;
|
||||
if (props.node.metadata?.status === "loading") return <LoadingContent node={props.node} theme={props.theme} onOpenTaskDetails={props.onOpenTaskDetails} />;
|
||||
if (props.node.metadata?.status === "error") return <ErrorContent node={props.node} theme={props.theme} onRetry={props.onRetry} />;
|
||||
|
||||
const Renderer = nodeContentRenderers[props.node.type];
|
||||
@@ -117,7 +116,7 @@ function DrawingContent({ node, theme, drawingProjectId }: CanvasNodeContentProp
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick<CanvasNodeContentProps, "node" | "theme" | "onCancelTask" | "onOpenTaskDetails">) {
|
||||
function LoadingContent({ node, theme, onOpenTaskDetails }: Pick<CanvasNodeContentProps, "node" | "theme" | "onOpenTaskDetails">) {
|
||||
const taskId = node.metadata?.taskId;
|
||||
const displayTask = {
|
||||
provider: node.metadata?.taskProvider,
|
||||
@@ -152,9 +151,6 @@ function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick<C
|
||||
</div>
|
||||
<div className="mt-0.5 flex items-center gap-1.5">
|
||||
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-[var(--r-sm)] px-2 text-[var(--fs-tiny)] font-medium transition-colors" style={{ background: theme.toolbar.itemHover, color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpenTaskDetails?.(node); }}><FileText className="size-3" />详情</button>
|
||||
{!submissionUncertain ? (
|
||||
<button type="button" className="inline-flex h-7 items-center gap-1 rounded-[var(--r-sm)] px-2 text-[var(--fs-tiny)] font-medium transition-colors" style={{ background: `${theme.accent.danger}16`, color: theme.accent.danger }} onMouseDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCancelTask?.(node); }}><Square className="size-2.5 fill-current" />取消</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { ArrowUp, AtSign, Boxes, ChevronDown, FileText, ImageIcon, ImagePlus, Maximize2, Music2, Pencil, SlidersHorizontal, Square, UserRound, Video } from "lucide-react";
|
||||
import { ArrowUp, AtSign, Boxes, ChevronDown, FileText, ImageIcon, ImagePlus, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video } from "lucide-react";
|
||||
import { Button, Image as AntImage, InputNumber, Modal, Tooltip } from "antd";
|
||||
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -30,7 +30,6 @@ type CanvasNodePromptPanelProps = {
|
||||
onPromptChange: (nodeId: string, prompt: string) => void;
|
||||
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeMetadata>) => void;
|
||||
onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void;
|
||||
onStop: (nodeId: string) => void;
|
||||
mentionReferences?: CanvasResourceReference[];
|
||||
onImageSettingsOpenChange?: (open: boolean) => void;
|
||||
workspaceMode?: CanvasWorkspaceMode;
|
||||
@@ -47,7 +46,7 @@ const PROMPT_EDITOR_VERTICAL_PADDING = 12;
|
||||
const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20;
|
||||
const PROMPT_EDITOR_MAX_LINES = 8;
|
||||
|
||||
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, onStop, mentionReferences = [], onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
|
||||
export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const themeName = useThemeStore((state) => state.theme);
|
||||
const theme = canvasThemes[themeName];
|
||||
@@ -204,13 +203,12 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
const renderSubmitButton = (expanded: boolean) => {
|
||||
const showCost = creditsEnabled && credits !== null;
|
||||
const formattedCredits = credits?.toLocaleString();
|
||||
const actionLabel = isRunning ? "停止生成" : showCost ? `预计消耗 ${formattedCredits} 积分,生成` : "生成";
|
||||
const actionLabel = isRunning ? "生成中" : showCost ? `预计消耗 ${formattedCredits} 积分,生成` : "生成";
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
className={`canvas-node-composer-submit ${showCost ? "has-cost" : ""}`}
|
||||
danger={isRunning}
|
||||
disabled={isSubmitDisabled}
|
||||
disabled={isRunning || isSubmitDisabled}
|
||||
style={
|
||||
{
|
||||
color: isSubmitDisabled ? theme.node.faint : theme.node.text,
|
||||
@@ -218,7 +216,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
"--canvas-composer-submit-action-fg": isSubmitDisabled ? theme.node.faint : theme.canvas.background,
|
||||
} as CSSProperties
|
||||
}
|
||||
onClick={() => (isRunning ? onStop(node.id) : expanded ? submitExpandedPrompt() : submit())}
|
||||
onClick={() => (expanded ? submitExpandedPrompt() : submit())}
|
||||
aria-label={actionLabel}
|
||||
title={actionLabel}
|
||||
>
|
||||
@@ -229,7 +227,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
</span>
|
||||
) : null}
|
||||
<span className="canvas-node-composer-submit-action" aria-hidden>
|
||||
{isRunning ? <Square className="size-2.5 fill-current" /> : <ArrowUp className="size-3" />}
|
||||
{isRunning ? <span className="size-2.5 animate-pulse" aria-hidden="true" /> : <ArrowUp className="size-3" />}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,6 @@ type CanvasNodeProps = {
|
||||
onToggleBatch?: (nodeId: string) => void;
|
||||
onSetBatchPrimary?: (node: CanvasNodeData) => void;
|
||||
onRetry?: (node: CanvasNodeData) => void;
|
||||
onCancelTask?: (node: CanvasNodeData) => void;
|
||||
onOpenTaskDetails?: (node: CanvasNodeData) => void;
|
||||
onOpenVersions?: (node: CanvasNodeData) => void;
|
||||
onViewImage?: (node: CanvasNodeData) => void;
|
||||
@@ -93,7 +92,6 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
onToggleBatch,
|
||||
onSetBatchPrimary,
|
||||
onRetry,
|
||||
onCancelTask,
|
||||
onOpenTaskDetails,
|
||||
onOpenVersions,
|
||||
onViewImage,
|
||||
@@ -360,7 +358,6 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
onContentChange={onContentChange}
|
||||
onStopEditing={() => setIsEditingContent(false)}
|
||||
onRetry={onRetry}
|
||||
onCancelTask={onCancelTask}
|
||||
onOpenTaskDetails={onOpenTaskDetails}
|
||||
onToggleBatch={() => onToggleBatch?.(data.id)}
|
||||
reduceMediaEffects={reduceMediaEffects}
|
||||
@@ -515,7 +512,6 @@ function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProp
|
||||
previous.onToggleBatch === next.onToggleBatch &&
|
||||
previous.onSetBatchPrimary === next.onSetBatchPrimary &&
|
||||
previous.onRetry === next.onRetry &&
|
||||
previous.onCancelTask === next.onCancelTask &&
|
||||
previous.onOpenTaskDetails === next.onOpenTaskDetails &&
|
||||
previous.onOpenVersions === next.onOpenVersions &&
|
||||
previous.onViewImage === next.onViewImage &&
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, use
|
||||
import { Button, Checkbox, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table, Tooltip } from "antd";
|
||||
import type { MenuProps } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { ChevronDown, ChevronUp, Clapperboard, Copy, Expand, Film, Grid3X3, Image as ImageIcon, ListTree, Merge, Minus, MoreHorizontal, Plus, RefreshCw, Send, Square, Trash2, Video, X } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, Clapperboard, Copy, Expand, Film, Grid3X3, Image as ImageIcon, ListTree, Merge, Minus, MoreHorizontal, Plus, RefreshCw, Send, Square, Trash2, Video } from "lucide-react";
|
||||
|
||||
import { CanvasResourceMentionTextarea } from "@/components/canvas/canvas-resource-mention-textarea";
|
||||
import { ModelPicker } from "@/components/model-picker";
|
||||
@@ -74,7 +74,6 @@ export function CanvasScriptNodeContent({
|
||||
onRetryBatch,
|
||||
onRetryBatchItem,
|
||||
onStopBatch,
|
||||
onCancelBatchItem,
|
||||
onAddRow,
|
||||
onRemoveRow,
|
||||
onUpdateRow,
|
||||
@@ -104,7 +103,6 @@ export function CanvasScriptNodeContent({
|
||||
onRetryBatch: (batchId: string) => void;
|
||||
onRetryBatchItem: (batchId: string, itemId: string) => void;
|
||||
onStopBatch: (batchId: string) => void;
|
||||
onCancelBatchItem: (batchId: string, itemId: string) => void;
|
||||
onAddRow: () => void;
|
||||
onRemoveRow: (rowId: string) => void;
|
||||
onUpdateRow: (rowId: string, patch: Partial<StoryboardRow>) => void;
|
||||
@@ -270,7 +268,7 @@ export function CanvasScriptNodeContent({
|
||||
</div>
|
||||
{batch ? (
|
||||
<Modal title="批次详情" open={batchDetailsOpen} onCancel={() => setBatchDetailsOpen(false)} footer={null} width={560} centered destroyOnHidden>
|
||||
<GenerationBatchDetails batch={batch} rows={rows} onRetryItem={(itemId) => onRetryBatchItem(batch.id, itemId)} onCancelItem={(itemId) => onCancelBatchItem(batch.id, itemId)} />
|
||||
<GenerationBatchDetails batch={batch} rows={rows} onRetryItem={(itemId) => onRetryBatchItem(batch.id, itemId)} />
|
||||
</Modal>
|
||||
) : null}
|
||||
<StoryboardMiniPipeline pipeline={pipeline} theme={theme} rows={rows} />
|
||||
@@ -525,7 +523,7 @@ function StoryboardMiniPipeline({ pipeline, theme, rows }: { pipeline: CanvasSto
|
||||
);
|
||||
}
|
||||
|
||||
function GenerationBatchDetails({ batch, rows, onRetryItem, onCancelItem }: { batch: CanvasGenerationBatch; rows: StoryboardRow[]; onRetryItem: (itemId: string) => void; onCancelItem: (itemId: string) => void }) {
|
||||
function GenerationBatchDetails({ batch, rows, onRetryItem }: { batch: CanvasGenerationBatch; rows: StoryboardRow[]; onRetryItem: (itemId: string) => void }) {
|
||||
const shotByRowId = new Map(rows.map((row) => [row.id, row.shotNumber]));
|
||||
return (
|
||||
<div className="w-80" onMouseDown={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}>
|
||||
@@ -535,7 +533,6 @@ function GenerationBatchDetails({ batch, rows, onRetryItem, onCancelItem }: { ba
|
||||
</div>
|
||||
<div className="thin-scrollbar max-h-72 overflow-y-auto">
|
||||
{batch.items.map((item) => {
|
||||
const cancellable = Boolean(item.taskId && (item.status === "queued" || item.status === "running"));
|
||||
const requiresPromptChange = isContentModerationError(item.errorDetails);
|
||||
return (
|
||||
<div key={item.id} className="flex min-h-9 items-center gap-2 border-t border-foreground/10 py-1.5 first:border-t-0">
|
||||
@@ -556,18 +553,6 @@ function GenerationBatchDetails({ batch, rows, onRetryItem, onCancelItem }: { ba
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{cancellable ? (
|
||||
<Tooltip title="取消这个后台任务">
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded outline-none transition hover:bg-red-500/10 focus-visible:ring-2"
|
||||
onClick={() => onCancelItem(item.id)}
|
||||
aria-label={`取消镜头 ${shotByRowId.get(item.rowId) || ""} 任务`}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user