Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

View File

@@ -0,0 +1,428 @@
import { RectangleClient } from '@plait/core';
import type { WorkflowMessageData } from '../types/chat.types';
import { workflowCompletionService } from '../services/workflow-completion-service';
import {
TaskExecutionPhase,
TaskStatus,
TaskType,
type Task,
} from '../types/task.types';
import type {
ImageGenerationAnchorBatchSlot,
ImageGenerationAnchorPhase,
ImageGenerationAnchorViewModel,
PlaitImageGenerationAnchor,
} from '../types/image-generation-anchor.types';
import {
getImageGenerationBatchExpectedCount,
getTaskResultPreviewUrls,
resolveImageGenerationBatchDisplayProgress,
} from './image-generation-anchor-batch';
import {
getImageTaskProgressStatusText,
resolveImageTaskDisplayProgress,
} from './image-task-progress';
const PHASE_LABELS: Record<ImageGenerationAnchorPhase, string> = {
submitted: '已提交',
queued: '排队中',
generating: '生成中',
developing: '显影中',
inserting: '插入中',
completed: '已完成',
failed: '失败',
};
const PHASE_SUBTITLES: Record<ImageGenerationAnchorPhase, string> = {
submitted: '正在创建生成锚点',
queued: '请求已受理,等待执行',
generating: '图片正在生成,请稍候',
developing: '结果已返回,正在准备显影',
inserting: '正在放入画布',
completed: '图片已稳定落位',
failed: '生成失败,可从当前位置重试',
};
const BATCH_STATUS_LABELS: Record<ImageGenerationAnchorPhase, string> = {
submitted: '等待执行',
queued: '等待执行',
generating: '生成中...',
developing: '正在显影',
inserting: '正在显现',
completed: '',
failed: '生成失败',
};
const clampProgress = (value: number | null | undefined): number | null => {
if (typeof value !== 'number' || Number.isNaN(value)) {
return null;
}
return Math.max(0, Math.min(100, value));
};
function sortBatchTasks(tasks: Task[]): Task[] {
return [...tasks].sort((left, right) => {
if (left.createdAt !== right.createdAt) {
return left.createdAt - right.createdAt;
}
return left.id.localeCompare(right.id);
});
}
function derivePlaceholderSlotStatus(
phase: ImageGenerationAnchorPhase
): ImageGenerationAnchorBatchSlot['status'] {
if (phase === 'failed') {
return 'failed';
}
if (phase === 'completed') {
return 'ready';
}
if (
phase === 'generating' ||
phase === 'developing' ||
phase === 'inserting'
) {
return 'generating';
}
return 'pending';
}
function deriveTaskBatchSlotStatus(
task: Task,
phase: ImageGenerationAnchorPhase
): ImageGenerationAnchorBatchSlot['status'] {
const postProcessing = workflowCompletionService.getPostProcessingStatus(
task.id
);
if (
task.status === TaskStatus.FAILED ||
postProcessing?.status === 'failed'
) {
return 'failed';
}
if (task.insertedToCanvas || postProcessing?.status === 'completed') {
return 'ready';
}
if (
postProcessing?.status === 'processing' ||
task.status === TaskStatus.COMPLETED
) {
return 'generating';
}
if (task.status === TaskStatus.PROCESSING) {
return task.executionPhase === TaskExecutionPhase.SUBMITTING
? 'pending'
: 'generating';
}
if (task.status === TaskStatus.PENDING) {
return derivePlaceholderSlotStatus(phase);
}
return derivePlaceholderSlotStatus(phase);
}
function buildBatchPreview(
anchor: PlaitImageGenerationAnchor,
phase: ImageGenerationAnchorPhase,
tasks: Task[],
fallbackProgress: number | null
): ImageGenerationAnchorViewModel['batchPreview'] {
if (anchor.anchorType !== 'stack') {
return undefined;
}
const slots: ImageGenerationAnchorBatchSlot[] = [];
const sortedTasks = sortBatchTasks(tasks);
sortedTasks.forEach((task) => {
const slotStatus = deriveTaskBatchSlotStatus(task, phase);
const previewUrls = getTaskResultPreviewUrls(task);
if (previewUrls.length > 0) {
previewUrls.forEach((previewUrl, index) => {
slots.push({
id: `${task.id}-${index}`,
taskId: task.id,
status: slotStatus,
previewImageUrl: previewUrl,
error:
slotStatus === 'failed'
? task.error?.message ||
task.error?.details?.originalError ||
workflowCompletionService.getPostProcessingStatus(task.id)
?.error
: undefined,
});
});
return;
}
slots.push({
id: task.id,
taskId: task.id,
status: slotStatus,
error:
slotStatus === 'failed'
? task.error?.message ||
task.error?.details?.originalError ||
workflowCompletionService.getPostProcessingStatus(task.id)?.error
: undefined,
});
});
const totalCount = Math.max(
getImageGenerationBatchExpectedCount(anchor),
slots.length
);
const placeholderStatus = derivePlaceholderSlotStatus(phase);
while (slots.length < totalCount) {
const slotIndex = slots.length;
slots.push({
id: `placeholder-${slotIndex}`,
status: placeholderStatus,
});
}
const visibleSlotCount = Math.max(2, Math.min(totalCount, 4));
const overflowCount = Math.max(totalCount - visibleSlotCount, 0);
const visibleSlots = slots.slice(0, visibleSlotCount);
const readySlotCount = slots.filter((slot) => slot.status === 'ready').length;
const generatingSlotCount = slots.filter(
(slot) => slot.status === 'generating'
).length;
const pendingSlotCount = slots.filter(
(slot) => slot.status === 'pending'
).length;
const failedSlotCount = slots.filter(
(slot) => slot.status === 'failed'
).length;
const progress = resolveImageGenerationBatchDisplayProgress(
anchor,
tasks,
fallbackProgress
);
const settledCount = readySlotCount + failedSlotCount;
const hasPreviewImage = visibleSlots.some((slot) =>
Boolean(slot.previewImageUrl)
);
const statusText =
phase !== 'completed' && readySlotCount > 0 && readySlotCount < totalCount
? `${readySlotCount}/${totalCount} 已完成`
: phase !== 'failed' && settledCount > 0 && settledCount < totalCount
? `${settledCount}/${totalCount} 已处理`
: progress != null &&
(phase === 'submitted' || phase === 'queued' || phase === 'generating')
? getImageTaskProgressStatusText(progress)
: BATCH_STATUS_LABELS[phase];
return {
totalCount,
visibleSlotCount,
overflowCount,
readySlotCount,
generatingSlotCount,
pendingSlotCount,
failedSlotCount,
hasPreviewImage,
progress,
statusText,
slots: visibleSlots,
};
}
export interface BuildImageGenerationAnchorViewModelOptions {
anchor: PlaitImageGenerationAnchor;
task?: Task | null;
tasks?: Task[];
workflow?: WorkflowMessageData | null;
postProcessingStatus?: WorkflowMessageData['postProcessingStatus'];
isInserting?: boolean;
hasInserted?: boolean;
taskDisplayProgress?: number | null;
}
export function deriveImageGenerationAnchorPhase(
options: BuildImageGenerationAnchorViewModelOptions
): ImageGenerationAnchorPhase {
const {
anchor,
task,
tasks = [],
workflow,
postProcessingStatus,
isInserting,
hasInserted,
} = options;
const relatedTasks = tasks.length > 0 ? tasks : task ? [task] : [];
const hasBatchFailure =
anchor.anchorType === 'stack' &&
relatedTasks.some((relatedTask) => {
const relatedPostProcessing =
workflowCompletionService.getPostProcessingStatus(relatedTask.id);
return (
relatedTask.status === TaskStatus.FAILED ||
relatedPostProcessing?.status === 'failed'
);
});
const hasBatchNonFailed =
anchor.anchorType === 'stack' &&
relatedTasks.some((relatedTask) => {
const relatedPostProcessing =
workflowCompletionService.getPostProcessingStatus(relatedTask.id);
return (
relatedTask.status !== TaskStatus.FAILED &&
relatedPostProcessing?.status !== 'failed'
);
});
const shouldSuppressFailedPhase = hasBatchFailure && hasBatchNonFailed;
if (
!shouldSuppressFailedPhase &&
(task?.status === TaskStatus.FAILED || workflow?.status === 'failed')
) {
return 'failed';
}
if (
!shouldSuppressFailedPhase &&
(postProcessingStatus === 'failed' ||
workflow?.postProcessingStatus === 'failed')
) {
return 'failed';
}
if (hasInserted || workflow?.postProcessingStatus === 'completed') {
return 'completed';
}
if (isInserting) {
return 'inserting';
}
if (
postProcessingStatus === 'processing' ||
workflow?.postProcessingStatus === 'processing'
) {
return 'developing';
}
if (
task?.status === TaskStatus.COMPLETED ||
workflow?.status === 'completed'
) {
return 'developing';
}
if (task?.status === TaskStatus.PROCESSING) {
if (task.executionPhase === TaskExecutionPhase.SUBMITTING) {
return 'queued';
}
return 'generating';
}
if (task?.status === TaskStatus.PENDING || workflow?.status === 'pending') {
return 'submitted';
}
return anchor.phase;
}
export function buildImageGenerationAnchorViewModel(
options: BuildImageGenerationAnchorViewModelOptions
): ImageGenerationAnchorViewModel {
const { anchor, task, tasks = [], taskDisplayProgress, workflow } = options;
const phase = deriveImageGenerationAnchorPhase(options);
const rectangle = RectangleClient.getRectangleByPoints(anchor.points);
const fallbackProgress = clampProgress(task?.progress ?? anchor.progress);
const batchPreview = buildBatchPreview(
anchor,
phase,
tasks,
fallbackProgress
);
const resolvedProgress =
task?.type === TaskType.IMAGE &&
task.status === TaskStatus.PROCESSING &&
(phase === 'queued' || phase === 'generating')
? resolveImageTaskDisplayProgress({
startedAt: task.startedAt,
fallbackProgress,
})
: fallbackProgress;
const progress = clampProgress(
anchor.anchorType === 'stack'
? batchPreview?.progress ?? taskDisplayProgress ?? resolvedProgress
: taskDisplayProgress ?? resolvedProgress
);
const subtitle =
anchor.subtitle && anchor.phase === phase
? anchor.subtitle
: PHASE_SUBTITLES[phase];
const tone: ImageGenerationAnchorViewModel['tone'] =
phase === 'failed'
? 'danger'
: phase === 'completed'
? 'success'
: phase === 'queued' || phase === 'developing'
? 'warning'
: 'default';
const progressMode: ImageGenerationAnchorViewModel['progressMode'] =
phase === 'queued' || phase === 'generating'
? progress != null
? 'determinate'
: 'indeterminate'
: phase === 'submitted'
? 'indeterminate'
: 'hidden';
const primaryAction =
phase === 'failed'
? { type: 'retry' as const, label: '重试' }
: { type: 'details' as const, label: '详情' };
const secondaryAction =
phase === 'failed'
? { type: 'dismiss' as const, label: '关闭' }
: undefined;
return {
id: anchor.id,
anchorType: anchor.anchorType,
phase,
title: anchor.title || workflow?.name || '图片生成',
subtitle,
previewImageUrl: anchor.previewImageUrl,
batchPreview,
progress,
progressMode,
phaseLabel: PHASE_LABELS[phase],
tone,
geometry: {
position: anchor.points[0],
width: rectangle.width,
height: rectangle.height,
},
transitionMode: anchor.transitionMode,
primaryAction,
secondaryAction,
error: anchor.error || workflow?.error,
isTerminal: phase === 'completed' || phase === 'failed',
};
}