feat(*): 画布创作体验 - 完善 Agent 引导、生成防重、搜索快捷键、素材多选与分辨率兼容

This commit is contained in:
ddCat committed 2026-08-28 16:24:58 +08:00
1 parent 913cf4b304
commit ab89c05362
52 files changed
+2344 -500

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
import { getNodeListLabel } from "@/lib/canvas/node-registry";
import { canvasNodeCreatedAt, canvasNodeUpdatedAt } from "@/lib/canvas/canvas-node-timestamps";
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
const searchTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
export function searchCanvasNodes(nodes: CanvasNodeData[], query: string, limit = 80) {
const keyword = query.trim().toLocaleLowerCase();
return nodes
.filter((node) => !keyword || canvasNodeSearchTerms(node).some((value) => value.toLocaleLowerCase().includes(keyword)))
.toSorted((left, right) => timestampValue(canvasNodeUpdatedAt(right)) - timestampValue(canvasNodeUpdatedAt(left)))
.slice(0, keyword ? limit : Math.min(limit, 40));
}
export function canvasNodeSearchContext(node: CanvasNodeData) {
const location = [node.metadata?.chapterTitle, typeof node.metadata?.shotIndex === "number" ? `镜头 ${node.metadata.shotIndex + 1}` : ""].filter(Boolean).join(" · ");
const textContent = node.type === CanvasNodeType.Text || node.type === CanvasNodeType.Markdown || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Skill
? node.metadata?.content
: undefined;
return location || node.metadata?.prompt || node.metadata?.composerContent || node.metadata?.workflowDescription || textContent || getNodeListLabel(node.type);
}
export function canvasNodeMaterialSummary(node: CanvasNodeData) {
const details = [getNodeListLabel(node.type)];
const width = node.metadata?.naturalWidth;
const height = node.metadata?.naturalHeight;
if (width && height) details.push(`${width}×${height}`);
if (node.metadata?.durationMs) details.push(formatDuration(node.metadata.durationMs));
if (node.metadata?.bytes) details.push(formatBytes(node.metadata.bytes));
if (node.metadata?.model) details.push(node.metadata.model);
return details.slice(0, 3).join(" · ");
}
export function canvasNodeSearchTimes(node: CanvasNodeData) {
const createdAt = canvasNodeCreatedAt(node);
const updatedAt = canvasNodeUpdatedAt(node);
return {
createdAt,
updatedAt,
createdLabel: createdAt ? searchTimeFormatter.format(new Date(createdAt)) : "未记录",
updatedLabel: updatedAt ? searchTimeFormatter.format(new Date(updatedAt)) : "未记录",
};
}
function canvasNodeSearchTerms(node: CanvasNodeData) {
return [
node.title,
node.type,
getNodeListLabel(node.type),
node.metadata?.prompt,
node.metadata?.composerContent,
node.metadata?.model,
node.metadata?.chapterTitle,
node.metadata?.workflowTitle,
node.metadata?.workflowDescription,
typeof node.metadata?.shotIndex === "number" ? `镜头 ${node.metadata.shotIndex + 1}` : "",
...(node.metadata?.assetTags || []),
].filter((value): value is string => typeof value === "string" && Boolean(value));
}
function timestampValue(value?: string) {
const timestamp = value ? Date.parse(value) : 0;
return Number.isFinite(timestamp) ? timestamp : 0;
}
function formatDuration(durationMs: number) {
const seconds = Math.max(0, Math.round(durationMs / 1000));
if (seconds < 60) return `${seconds}秒`;
return `${Math.floor(seconds / 60)}分${seconds % 60}秒`;
}
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(bytes < 10 * 1024 * 1024 ? 1 : 0)} MB`;
}