fix(*): Web 构建 - 修复短剧项目空间合并后的 TypeScript 类型错误
This commit is contained in:
1 parent
5d1a19fdf4
commit
87f3576df2
15 files changed
+63
-55
No files matched your search
@@ -6,6 +6,8 @@ import { WorkspaceState } from "@/components/layout/workspace-state";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
|
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
|
||||||
|
|
||||||
|
type InsertableAsset = Extract<Asset, { kind: "text" | "image" | "video" | "audio" }>;
|
||||||
|
|
||||||
export type InsertAssetPayload =
|
export type InsertAssetPayload =
|
||||||
| { kind: "text"; content: string; title: string; assetId?: string }
|
| { kind: "text"; content: string; title: string; assetId?: string }
|
||||||
| { kind: "image"; dataUrl: string; title: string; storageKey?: string; assetId?: string }
|
| { kind: "image"; dataUrl: string; title: string; storageKey?: string; assetId?: string }
|
||||||
@@ -70,7 +72,7 @@ function MyAssetsTab({ onInsert }: { onInsert: (payload: InsertAssetPayload) =>
|
|||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const query = keyword.trim().toLowerCase();
|
const query = keyword.trim().toLowerCase();
|
||||||
return assets
|
return assets
|
||||||
.filter((a) => a.kind === "text" || a.kind === "image" || a.kind === "video" || a.kind === "audio")
|
.filter((asset): asset is InsertableAsset => asset.kind === "text" || asset.kind === "image" || asset.kind === "video" || asset.kind === "audio")
|
||||||
.filter((a) => kindFilter === "all" || a.kind === kindFilter)
|
.filter((a) => kindFilter === "all" || a.kind === kindFilter)
|
||||||
.filter((a) => !query || [a.title, ...(a.tags || [])].join(" ").toLowerCase().includes(query));
|
.filter((a) => !query || [a.title, ...(a.tags || [])].join(" ").toLowerCase().includes(query));
|
||||||
}, [assets, keyword, kindFilter]);
|
}, [assets, keyword, kindFilter]);
|
||||||
@@ -82,15 +84,14 @@ function MyAssetsTab({ onInsert }: { onInsert: (payload: InsertAssetPayload) =>
|
|||||||
setPage((v) => Math.min(v, maxPage));
|
setPage((v) => Math.min(v, maxPage));
|
||||||
}, [filtered.length]);
|
}, [filtered.length]);
|
||||||
|
|
||||||
const handleInsert = (asset: Asset) => {
|
const handleInsert = (asset: InsertableAsset) => {
|
||||||
if (asset.kind === "model") return;
|
|
||||||
if (asset.kind === "text") {
|
if (asset.kind === "text") {
|
||||||
onInsert({ kind: "text", content: asset.data.content, title: asset.title, assetId: asset.id });
|
onInsert({ kind: "text", content: asset.data.content, title: asset.title, assetId: asset.id });
|
||||||
} else if (asset.kind === "audio") {
|
} else if (asset.kind === "audio") {
|
||||||
onInsert({ kind: "audio", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, durationMs: asset.data.durationMs, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id });
|
onInsert({ kind: "audio", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, durationMs: asset.data.durationMs, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id });
|
||||||
} else if (asset.kind === "video") {
|
} else if (asset.kind === "video") {
|
||||||
onInsert({ kind: "video", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, width: asset.data.width, height: asset.data.height, durationMs: asset.data.durationMs, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id });
|
onInsert({ kind: "video", url: asset.data.url, storageKey: asset.data.storageKey, title: asset.title, width: asset.data.width, height: asset.data.height, durationMs: asset.data.durationMs, bytes: asset.data.bytes, mimeType: asset.data.mimeType, assetId: asset.id });
|
||||||
} else {
|
} else if (asset.kind === "image") {
|
||||||
onInsert({ kind: "image", dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, title: asset.title, assetId: asset.id });
|
onInsert({ kind: "image", dataUrl: asset.data.dataUrl, storageKey: asset.data.storageKey, title: asset.title, assetId: asset.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -908,25 +908,25 @@ function AgentTextModelPicker({ config, value, onChange }: { config: AiConfig; v
|
|||||||
const options = useMemo(() => Array.from(new Set([value, ...selectableModelsByCapability(config, "text")].filter(Boolean))), [config, value]);
|
const options = useMemo(() => Array.from(new Set([value, ...selectableModelsByCapability(config, "text")].filter(Boolean))), [config, value]);
|
||||||
const current = value || "";
|
const current = value || "";
|
||||||
return (
|
return (
|
||||||
<Select<string>
|
<div className="min-w-0 max-w-[240px]" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||||
size="small"
|
<Select<string>
|
||||||
variant="borderless"
|
size="small"
|
||||||
value={current || undefined}
|
variant="borderless"
|
||||||
className="agent-text-model-select min-w-0 max-w-[240px]"
|
value={current || undefined}
|
||||||
popupMatchSelectWidth={288}
|
className="agent-text-model-select w-full"
|
||||||
options={options.map((model) => ({ value: model, label: `${modelDisplayName(config, model)} ${modelOptionName(model)} ${resolveModelChannel(config, model).name}` }))}
|
popupMatchSelectWidth={288}
|
||||||
notFoundContent={<span className="block py-2 text-center text-xs text-foreground/48">暂无文本模型</span>}
|
options={options.map((model) => ({ value: model, label: `${modelDisplayName(config, model)} ${modelOptionName(model)} ${resolveModelChannel(config, model).name}` }))}
|
||||||
optionRender={(option) => {
|
notFoundContent={<span className="block py-2 text-center text-xs text-foreground/48">暂无文本模型</span>}
|
||||||
const model = String(option.value);
|
optionRender={(option) => {
|
||||||
return <span className="flex min-w-0 items-center gap-2"><AgentModelIcon model={model} /><span className="min-w-0 flex-1"><span className="block truncate">{modelDisplayName(config, model)}</span><span className="block truncate text-[10px] opacity-45">{modelOptionName(model)}</span></span><span className="shrink-0 text-xs opacity-55">{resolveModelChannel(config, model).name}</span></span>;
|
const model = String(option.value);
|
||||||
}}
|
return <span className="flex min-w-0 items-center gap-2"><AgentModelIcon model={model} /><span className="min-w-0 flex-1"><span className="block truncate">{modelDisplayName(config, model)}</span><span className="block truncate text-[10px] opacity-45">{modelOptionName(model)}</span></span><span className="shrink-0 text-xs opacity-55">{resolveModelChannel(config, model).name}</span></span>;
|
||||||
labelRender={() => <span className="flex min-w-0 items-center gap-1.5"><AgentModelIcon model={current} /><span className="min-w-0 truncate">{current ? modelDisplayName(config, current) : "选择文本模型"}</span>{current ? <span className="shrink-0 opacity-55">{resolveModelChannel(config, current).name}</span> : null}</span>}
|
}}
|
||||||
onChange={onChange}
|
labelRender={() => <span className="flex min-w-0 items-center gap-1.5"><AgentModelIcon model={current} /><span className="min-w-0 truncate">{current ? modelDisplayName(config, current) : "选择文本模型"}</span>{current ? <span className="shrink-0 opacity-55">{resolveModelChannel(config, current).name}</span> : null}</span>}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onChange={onChange}
|
||||||
onPointerDown={(event) => event.stopPropagation()}
|
aria-label="选择 Agent 文本模型"
|
||||||
aria-label="选择 Agent 文本模型"
|
title={current ? `${modelDisplayName(config, current)} · ${modelOptionName(current)} · ${resolveModelChannel(config, current).name}` : "选择文本模型"}
|
||||||
title={current ? `${modelDisplayName(config, current)} · ${modelOptionName(current)} · ${resolveModelChannel(config, current).name}` : "选择文本模型"}
|
/>
|
||||||
/>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSav
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
width="100vw"
|
width="100vw"
|
||||||
centered
|
centered
|
||||||
styles={{ body: { padding: 0 }, content: { padding: 0, overflow: "hidden" } }}
|
styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }}
|
||||||
className="canvas-drawing-editor-modal"
|
className="canvas-drawing-editor-modal"
|
||||||
>
|
>
|
||||||
<div className="flex h-[min(92dvh,980px)] flex-col">
|
<div className="flex h-[min(92dvh,980px)] flex-col">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/g
|
|||||||
import { useCopyText } from "@/hooks/use-copy-text";
|
import { useCopyText } from "@/hooks/use-copy-text";
|
||||||
import { useThemeStore } from "@/stores/use-theme-store";
|
import { useThemeStore } from "@/stores/use-theme-store";
|
||||||
import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock";
|
import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock";
|
||||||
import { CanvasNodeType, type CanvasNodeData, type CanvasWorkspaceMode, type ViewportTransform } from "@/types/canvas";
|
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata, type CanvasWorkspaceMode, type ViewportTransform } from "@/types/canvas";
|
||||||
import { ImageToolSettingsModal } from "./canvas-image-toolbar-settings-modal";
|
import { ImageToolSettingsModal } from "./canvas-image-toolbar-settings-modal";
|
||||||
import { IMAGE_QUICK_TOOLS_STORAGE_KEY, buildImageToolbarTools, defaultImageQuickToolIds, isImageQuickToolId, readImageQuickToolsConfig, type ImageQuickToolId } from "./canvas-image-toolbar-tools";
|
import { IMAGE_QUICK_TOOLS_STORAGE_KEY, buildImageToolbarTools, defaultImageQuickToolIds, isImageQuickToolId, readImageQuickToolsConfig, type ImageQuickToolId } from "./canvas-image-toolbar-tools";
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ export function CanvasNodeHoverToolbar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, readOnly = false, onUnauthorized }: { node: CanvasNodeData | null; open: boolean; onClose: () => void; onMetadataChange?: (nodeId: string, metadata: Partial<CanvasNodeData["metadata"]>) => void; readOnly?: boolean; onUnauthorized?: () => void }) {
|
export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, readOnly = false, onUnauthorized }: { node: CanvasNodeData | null; open: boolean; onClose: () => void; onMetadataChange?: (nodeId: string, metadata: Partial<CanvasNodeMetadata>) => void; readOnly?: boolean; onUnauthorized?: () => void }) {
|
||||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||||
const [view, setView] = useState<"info" | "json">("info");
|
const [view, setView] = useState<"info" | "json">("info");
|
||||||
const [assetTags, setAssetTags] = useState<string[]>([]);
|
const [assetTags, setAssetTags] = useState<string[]>([]);
|
||||||
@@ -418,7 +418,7 @@ export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, rea
|
|||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{assetCategoryOptions.map((option) => {
|
{assetCategoryOptions.map((option) => {
|
||||||
const active = assetCategory === option.value;
|
const active = assetCategory === option.value;
|
||||||
return <button key={option.value} type="button" disabled={readOnly} onClick={() => saveAssetCategory(option.value)} className="h-7 rounded-md border px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60" style={{ borderColor: active ? theme.accent.primary : theme.toolbar.border, background: active ? theme.accent.soft : theme.toolbar.panel, color: active ? theme.accent.primary : theme.node.muted }}>{option.label}</button>;
|
return <button key={option.value} type="button" disabled={readOnly} onClick={() => saveAssetCategory(option.value)} className="h-7 rounded-md border px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60" style={{ borderColor: active ? theme.accent.primary : theme.toolbar.border, background: active ? theme.accent.primarySoft : theme.toolbar.panel, color: active ? theme.accent.primary : theme.node.muted }}>{option.label}</button>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-[11px] leading-5 opacity-45">生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。</div>
|
<div className="mt-2 text-[11px] leading-5 opacity-45">生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textare
|
|||||||
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
||||||
import { CanvasVideoPromptTools } from "./canvas-video-prompt-tools";
|
import { CanvasVideoPromptTools } from "./canvas-video-prompt-tools";
|
||||||
import { CanvasPresetPicker, type CanvasPromptPreset } from "./canvas-preset-picker";
|
import { CanvasPresetPicker, type CanvasPromptPreset } from "./canvas-preset-picker";
|
||||||
import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData, type CanvasWorkspaceMode } from "@/types/canvas";
|
import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData, type CanvasNodeMetadata, type CanvasWorkspaceMode } from "@/types/canvas";
|
||||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||||
|
|
||||||
export type CanvasNodeGenerationMode = CanvasGenerationMode;
|
export type CanvasNodeGenerationMode = CanvasGenerationMode;
|
||||||
@@ -24,7 +24,7 @@ type CanvasNodePromptPanelProps = {
|
|||||||
node: CanvasNodeData;
|
node: CanvasNodeData;
|
||||||
isRunning: boolean;
|
isRunning: boolean;
|
||||||
onPromptChange: (nodeId: string, prompt: string) => void;
|
onPromptChange: (nodeId: string, prompt: string) => void;
|
||||||
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeData["metadata"]>) => void;
|
onConfigChange: (nodeId: string, patch: Partial<CanvasNodeMetadata>) => void;
|
||||||
onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void;
|
onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void;
|
||||||
onStop: (nodeId: string) => void;
|
onStop: (nodeId: string) => void;
|
||||||
mentionReferences?: CanvasResourceReference[];
|
mentionReferences?: CanvasResourceReference[];
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export async function exportCanvasProjects(projects: CanvasProject[], fileName =
|
|||||||
zipFiles.push({ name: path, data: blob });
|
zipFiles.push({ name: path, data: blob });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const drawingDocuments = (await Promise.all(project.nodes.filter((node) => node.type === "drawing" && node.metadata?.drawingId).map(async (node) => {
|
const drawingDocuments = (await Promise.all(project.nodes.filter((node) => node.type === "drawing" && node.metadata?.drawingId).map(async (node): Promise<CanvasDrawingExport | null> => {
|
||||||
const drawingId = node.metadata?.drawingId;
|
const drawingId = node.metadata?.drawingId;
|
||||||
if (!drawingId) return null;
|
if (!drawingId) return null;
|
||||||
const [saved, preview, render] = await Promise.all([
|
const [saved, preview, render] = await Promise.all([
|
||||||
@@ -43,7 +43,7 @@ export async function exportCanvasProjects(projects: CanvasProject[], fileName =
|
|||||||
? { path: generationRenderPath, pageId: render.pageId, width: render.width, height: render.height, mimeType: render.mimeType, background: render.background }
|
? { path: generationRenderPath, pageId: render.pageId, width: render.width, height: render.height, mimeType: render.mimeType, background: render.background }
|
||||||
: undefined,
|
: undefined,
|
||||||
} satisfies CanvasDrawingExport;
|
} satisfies CanvasDrawingExport;
|
||||||
}))).filter((item): item is CanvasDrawingExport => Boolean(item));
|
}))).filter((item): item is CanvasDrawingExport => item !== null);
|
||||||
drawingDocuments.forEach((document) => zipFiles.push({ name: `projects/${project.id}/drawings/${safeFileName(document.drawingId)}.json`, data: JSON.stringify(document) }));
|
drawingDocuments.forEach((document) => zipFiles.push({ name: `projects/${project.id}/drawings/${safeFileName(document.drawingId)}.json`, data: JSON.stringify(document) }));
|
||||||
return { project, files, drawingDocuments };
|
return { project, files, drawingDocuments };
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ function sameAssetResource(asset: Asset, node: CanvasNodeData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function assetStorageKey(asset: Asset) {
|
function assetStorageKey(asset: Asset) {
|
||||||
return asset.kind === "text" ? undefined : asset.data.storageKey;
|
if (asset.kind === "text" || asset.kind === "entity") return undefined;
|
||||||
|
return asset.data.storageKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function declaredCanvasNodeAssetCategory(node: CanvasNodeData): AssetCategory | undefined {
|
export function declaredCanvasNodeAssetCategory(node: CanvasNodeData): AssetCategory | undefined {
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function storyboardRowsFromTask(task: GenerationTask) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function applyNodeConfigPatch(node: CanvasNodeData, patch: Partial<CanvasNodeData["metadata"]>) {
|
export function applyNodeConfigPatch(node: CanvasNodeData, patch: Partial<CanvasNodeMetadata>) {
|
||||||
const safePatch = patch || {};
|
const safePatch = patch || {};
|
||||||
const next = { ...node, metadata: { ...node.metadata, ...safePatch } };
|
const next = { ...node, metadata: { ...node.metadata, ...safePatch } };
|
||||||
const spec = node.type === CanvasNodeType.Video ? NODE_DEFAULT_SIZE[CanvasNodeType.Video] : NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
const spec = node.type === CanvasNodeType.Video ? NODE_DEFAULT_SIZE[CanvasNodeType.Video] : NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { useAssetStore, type Asset, type AssetCategory, type AssetKind, type Ima
|
|||||||
import { exportAssets, readAssetPackage } from "./asset-transfer";
|
import { exportAssets, readAssetPackage } from "./asset-transfer";
|
||||||
import { deleteAssetWithRemoteSync } from "@/services/user-data-sync";
|
import { deleteAssetWithRemoteSync } from "@/services/user-data-sync";
|
||||||
|
|
||||||
|
type LibraryAsset = Exclude<Asset, { kind: "entity" }>;
|
||||||
|
|
||||||
type AssetFormValues = {
|
type AssetFormValues = {
|
||||||
kind: AssetKind;
|
kind: AssetKind;
|
||||||
category: AssetCategory;
|
category: AssetCategory;
|
||||||
@@ -64,10 +66,10 @@ export default function AssetsPage() {
|
|||||||
const [categoryFilter, setCategoryFilter] = useState<AssetCategory | "all">("all");
|
const [categoryFilter, setCategoryFilter] = useState<AssetCategory | "all">("all");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [editingAsset, setEditingAsset] = useState<Asset | null>(null);
|
const [editingAsset, setEditingAsset] = useState<LibraryAsset | null>(null);
|
||||||
const [isAssetOpen, setIsAssetOpen] = useState(false);
|
const [isAssetOpen, setIsAssetOpen] = useState(false);
|
||||||
const [previewAsset, setPreviewAsset] = useState<Asset | null>(null);
|
const [previewAsset, setPreviewAsset] = useState<LibraryAsset | null>(null);
|
||||||
const [deletingAsset, setDeletingAsset] = useState<Asset | null>(null);
|
const [deletingAsset, setDeletingAsset] = useState<LibraryAsset | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
|
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
|
||||||
const [formKind, setFormKind] = useState<AssetKind>("text");
|
const [formKind, setFormKind] = useState<AssetKind>("text");
|
||||||
@@ -76,7 +78,7 @@ export default function AssetsPage() {
|
|||||||
const title = Form.useWatch("title", form) || "";
|
const title = Form.useWatch("title", form) || "";
|
||||||
const tags = Form.useWatch("tags", form) || [];
|
const tags = Form.useWatch("tags", form) || [];
|
||||||
const content = Form.useWatch("content", form) || "";
|
const content = Form.useWatch("content", form) || "";
|
||||||
const validAssets = useMemo(() => assets.filter((asset) => asset.kind === "text" || asset.kind === "image" || asset.kind === "video" || asset.kind === "audio" || asset.kind === "model"), [assets]);
|
const validAssets = useMemo(() => assets.filter((asset): asset is LibraryAsset => asset.kind !== "entity"), [assets]);
|
||||||
const selectedAssets = useMemo(() => validAssets.filter((asset) => selectedIds.includes(asset.id)), [selectedIds, validAssets]);
|
const selectedAssets = useMemo(() => validAssets.filter((asset) => selectedIds.includes(asset.id)), [selectedIds, validAssets]);
|
||||||
const kindCounts = useMemo(() => new Map(kindOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => asset.kind === option.value).length])), [validAssets]);
|
const kindCounts = useMemo(() => new Map(kindOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => asset.kind === option.value).length])), [validAssets]);
|
||||||
const categoryCounts = useMemo(() => new Map(categoryOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => (asset.category || "other") === option.value).length])), [validAssets]);
|
const categoryCounts = useMemo(() => new Map(categoryOptions.map((option) => [option.value, option.value === "all" ? validAssets.length : validAssets.filter((asset) => (asset.category || "other") === option.value).length])), [validAssets]);
|
||||||
@@ -114,7 +116,7 @@ export default function AssetsPage() {
|
|||||||
setIsAssetOpen(true);
|
setIsAssetOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (asset: Asset) => {
|
const openEdit = (asset: LibraryAsset) => {
|
||||||
setEditingAsset(asset);
|
setEditingAsset(asset);
|
||||||
setFormKind(asset.kind);
|
setFormKind(asset.kind);
|
||||||
setImageDraft(asset.kind === "image" ? asset.data : null);
|
setImageDraft(asset.kind === "image" ? asset.data : null);
|
||||||
@@ -183,12 +185,12 @@ export default function AssetsPage() {
|
|||||||
message.success("3D 模型已保存");
|
message.success("3D 模型已保存");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyAssetText = async (asset: Asset) => {
|
const copyAssetText = async (asset: LibraryAsset) => {
|
||||||
if (asset.kind !== "text") return;
|
if (asset.kind !== "text") return;
|
||||||
copyText(asset.data.content, "文本已复制");
|
copyText(asset.data.content, "文本已复制");
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadImage = (asset: Asset) => {
|
const downloadImage = (asset: LibraryAsset) => {
|
||||||
if (asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio" && asset.kind !== "model") return;
|
if (asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio" && asset.kind !== "model") return;
|
||||||
const url = asset.kind === "image" ? asset.data.dataUrl : asset.data.url;
|
const url = asset.kind === "image" ? asset.data.dataUrl : asset.data.url;
|
||||||
const extension = asset.kind === "model" ? asset.data.fileName.split(".").pop() || "glb" : asset.data.mimeType.split("/")[1] || "png";
|
const extension = asset.kind === "model" ? asset.data.fileName.split(".").pop() || "glb" : asset.data.mimeType.split("/")[1] || "png";
|
||||||
@@ -421,7 +423,7 @@ export default function AssetsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetCard({ asset, selected, onSelect, onOpen, onEdit, onCopy, onDownload, onDelete }: { asset: Asset; selected: boolean; onSelect: (selected: boolean) => void; onOpen: () => void; onEdit: () => void; onCopy: (asset: Asset) => void; onDownload: (asset: Asset) => void; onDelete: () => void }) {
|
function AssetCard({ asset, selected, onSelect, onOpen, onEdit, onCopy, onDownload, onDelete }: { asset: LibraryAsset; selected: boolean; onSelect: (selected: boolean) => void; onOpen: () => void; onEdit: () => void; onCopy: (asset: LibraryAsset) => void; onDownload: (asset: LibraryAsset) => void; onDelete: () => void }) {
|
||||||
const cover = asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : "");
|
const cover = asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : "");
|
||||||
const summary = assetSummary(asset);
|
const summary = assetSummary(asset);
|
||||||
return (
|
return (
|
||||||
@@ -494,7 +496,7 @@ function AssetFilterGroup({ title, options, value, counts, onChange, className =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: Asset | null; onClose: () => void; onCopy: (asset: Asset) => void; onDownload: (asset: Asset) => void }) {
|
function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: LibraryAsset | null; onClose: () => void; onCopy: (asset: LibraryAsset) => void; onDownload: (asset: LibraryAsset) => void }) {
|
||||||
const cover = asset ? asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : "") : "";
|
const cover = asset ? asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : "") : "";
|
||||||
return (
|
return (
|
||||||
<Drawer title="素材详情" open={Boolean(asset)} size="large" onClose={onClose}>
|
<Drawer title="素材详情" open={Boolean(asset)} size="large" onClose={onClose}>
|
||||||
@@ -565,14 +567,14 @@ function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: Asset | nu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assetSummary(asset: Asset) {
|
function assetSummary(asset: LibraryAsset) {
|
||||||
if (asset.kind === "text") return asset.data.content;
|
if (asset.kind === "text") return asset.data.content;
|
||||||
if (asset.kind === "audio") return `${formatAssetDuration(asset.data.durationMs)} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
if (asset.kind === "audio") return `${formatAssetDuration(asset.data.durationMs)} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
||||||
if (asset.kind === "model") return `${asset.data.fileName} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
if (asset.kind === "model") return `${asset.data.fileName} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
||||||
return `${asset.data.width}x${asset.data.height} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
return `${asset.data.width}x${asset.data.height} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StorageTag({ asset }: { asset: Asset }) {
|
function StorageTag({ asset }: { asset: LibraryAsset }) {
|
||||||
if (asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio" && asset.kind !== "model") return null;
|
if (asset.kind !== "image" && asset.kind !== "video" && asset.kind !== "audio" && asset.kind !== "model") return null;
|
||||||
const location = resourceStorageLocation(asset.data.storageKey);
|
const location = resourceStorageLocation(asset.data.storageKey);
|
||||||
const color = location === "oss" ? "green" : location === "local" ? "gold" : "default";
|
const color = location === "oss" ? "green" : location === "local" ? "gold" : "default";
|
||||||
@@ -583,7 +585,7 @@ function StorageTag({ asset }: { asset: Asset }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function assetSearchText(asset: Asset) {
|
function assetSearchText(asset: LibraryAsset) {
|
||||||
return [asset.title, asset.source || "", asset.note || "", assetCategoryLabel(asset.category), (asset.tags || []).join(" "), asset.kind === "text" ? asset.data.content : asset.data.mimeType].join(" ").toLowerCase();
|
return [asset.title, asset.source || "", asset.note || "", assetCategoryLabel(asset.category), (asset.tags || []).join(" "), asset.kind === "text" ? asset.data.content : asset.data.mimeType].join(" ").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +593,7 @@ function assetCategoryLabel(category?: AssetCategory) {
|
|||||||
return categoryOptions.find((item) => item.value === (category || "other"))?.label || "其他";
|
return categoryOptions.find((item) => item.value === (category || "other"))?.label || "其他";
|
||||||
}
|
}
|
||||||
|
|
||||||
function assetProjectLabel(asset: Asset) {
|
function assetProjectLabel(asset: LibraryAsset) {
|
||||||
const projectName = asset.metadata?.projectName;
|
const projectName = asset.metadata?.projectName;
|
||||||
if (typeof projectName === "string" && projectName.trim()) return projectName;
|
if (typeof projectName === "string" && projectName.trim()) return projectName;
|
||||||
return Array.isArray(asset.metadata?.projectIds) && asset.metadata.projectIds.length ? "已关联项目" : "未关联项目";
|
return Array.isArray(asset.metadata?.projectIds) && asset.metadata.projectIds.length ? "已关联项目" : "未关联项目";
|
||||||
|
|||||||
@@ -423,7 +423,6 @@ function InfiniteCanvasPage() {
|
|||||||
setSelectedConnectionId,
|
setSelectedConnectionId,
|
||||||
setContextMenu,
|
setContextMenu,
|
||||||
setDialogNodeId,
|
setDialogNodeId,
|
||||||
setDrawingNodeId,
|
|
||||||
setToolbarNodeId,
|
setToolbarNodeId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { applyNodeConfigPatch } from "@/lib/canvas/canvas-project-domain";
|
|||||||
import { audioExtension, imageExtension, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
import { audioExtension, imageExtension, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||||
import { CanvasNodeType, type CanvasNodeData, type Position } from "@/types/canvas";
|
import { CanvasNodeType, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
|
||||||
|
|
||||||
type UseCanvasNodeEditorOptions = {
|
type UseCanvasNodeEditorOptions = {
|
||||||
canvasId: string;
|
canvasId: string;
|
||||||
@@ -136,7 +136,7 @@ export function useCanvasNodeEditor({
|
|||||||
}));
|
}));
|
||||||
}, [setNodes]);
|
}, [setNodes]);
|
||||||
|
|
||||||
const handleConfigNodeChange = useCallback((nodeId: string, patch: Partial<CanvasNodeData["metadata"]>) => {
|
const handleConfigNodeChange = useCallback((nodeId: string, patch: Partial<CanvasNodeMetadata>) => {
|
||||||
setNodes((current) => current.map((node) => (node.id === nodeId ? applyNodeConfigPatch(node, patch) : node)));
|
setNodes((current) => current.map((node) => (node.id === nodeId ? applyNodeConfigPatch(node, patch) : node)));
|
||||||
if (!patch.assetCategory) return;
|
if (!patch.assetCategory) return;
|
||||||
const node = nodesRef.current.find((item) => item.id === nodeId);
|
const node = nodesRef.current.find((item) => item.id === nodeId);
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function MediaAssetCard({ asset, personalAsset, onCategoryChange, onVersion, onR
|
|||||||
}
|
}
|
||||||
|
|
||||||
function characterDefinition(values: CharacterForm) {
|
function characterDefinition(values: CharacterForm) {
|
||||||
const definition = Object.fromEntries(characterFields.map(([key]) => [key, values[key]?.trim() || (key === "aliases" ? [] : "")]));
|
const definition: Record<string, unknown> = Object.fromEntries(characterFields.map(([key]) => [key, values[key]?.trim() || (key === "aliases" ? [] : "")]));
|
||||||
definition.aliases = values.aliases?.split(/[,,]/).map((item) => item.trim()).filter(Boolean) || [];
|
definition.aliases = values.aliases?.split(/[,,]/).map((item) => item.trim()).filter(Boolean) || [];
|
||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Highlight from "@tiptap/extension-highlight";
|
|||||||
import Placeholder from "@tiptap/extension-placeholder";
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
import TextAlign from "@tiptap/extension-text-align";
|
import TextAlign from "@tiptap/extension-text-align";
|
||||||
import { TextStyle } from "@tiptap/extension-text-style";
|
import { TextStyle } from "@tiptap/extension-text-style";
|
||||||
import { EditorContent, useEditor } from "@tiptap/react";
|
import { EditorContent, useEditor, type Editor } from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
import {
|
import {
|
||||||
AlignCenter,
|
AlignCenter,
|
||||||
@@ -425,7 +425,7 @@ export default function ProjectChaptersView({ detail, refreshProject }: ProjectD
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
function EditorToolbar({ editor }: { editor: Editor | null }) {
|
||||||
const setLink = () => {
|
const setLink = () => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
const current = String(editor.getAttributes("link").href || "");
|
const current = String(editor.getAttributes("link").href || "");
|
||||||
@@ -472,7 +472,7 @@ function EditorToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorTool({ editor, label, icon, active = false, onClick }: { editor: ReturnType<typeof useEditor>; label: string; icon: ReactNode; active?: boolean; onClick: () => void }) {
|
function EditorTool({ editor, label, icon, active = false, onClick }: { editor: Editor | null; label: string; icon: ReactNode; active?: boolean; onClick: () => void }) {
|
||||||
return <Tooltip title={label}><button type="button" aria-label={label} className={`grid size-7 shrink-0 place-items-center rounded ${active ? "bg-foreground/[.08] text-[var(--workspace-accent)]" : "text-foreground/55 hover:bg-foreground/[.06] hover:text-foreground"}`} disabled={!editor} onClick={onClick}>{icon}</button></Tooltip>;
|
return <Tooltip title={label}><button type="button" aria-label={label} className={`grid size-7 shrink-0 place-items-center rounded ${active ? "bg-foreground/[.08] text-[var(--workspace-accent)]" : "text-foreground/55 hover:bg-foreground/[.06] hover:text-foreground"}`} disabled={!editor} onClick={onClick}>{icon}</button></Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,14 +161,18 @@ async function saveRemoteUserDataBatch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hydrateAssets(assets: Asset[]) {
|
async function hydrateAssets(assets: Asset[]): Promise<Asset[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
assets.map(async (asset) => {
|
assets.map(async (asset) => {
|
||||||
if (asset.kind === "image" && asset.data.storageKey) {
|
if (asset.kind === "image" && asset.data.storageKey) {
|
||||||
const dataUrl = await resolveImageUrl(asset.data.storageKey, asset.data.dataUrl);
|
const dataUrl = await resolveImageUrl(asset.data.storageKey, asset.data.dataUrl);
|
||||||
return { ...asset, coverUrl: shouldReplaceEphemeralUrl(asset.coverUrl) ? dataUrl : asset.coverUrl, data: { ...asset.data, dataUrl } };
|
return { ...asset, coverUrl: shouldReplaceEphemeralUrl(asset.coverUrl) ? dataUrl : asset.coverUrl, data: { ...asset.data, dataUrl } };
|
||||||
}
|
}
|
||||||
if ((asset.kind === "video" || asset.kind === "audio") && asset.data.storageKey) {
|
if (asset.kind === "video" && asset.data.storageKey) {
|
||||||
|
const url = await resolveResourceOrMediaUrl(asset.data.storageKey, asset.data.url);
|
||||||
|
return { ...asset, coverUrl: shouldReplaceEphemeralUrl(asset.coverUrl) ? url : asset.coverUrl, data: { ...asset.data, url } };
|
||||||
|
}
|
||||||
|
if (asset.kind === "audio" && asset.data.storageKey) {
|
||||||
const url = await resolveResourceOrMediaUrl(asset.data.storageKey, asset.data.url);
|
const url = await resolveResourceOrMediaUrl(asset.data.storageKey, asset.data.url);
|
||||||
return { ...asset, coverUrl: shouldReplaceEphemeralUrl(asset.coverUrl) ? url : asset.coverUrl, data: { ...asset.data, url } };
|
return { ...asset, coverUrl: shouldReplaceEphemeralUrl(asset.coverUrl) ? url : asset.coverUrl, data: { ...asset.data, url } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type CanvasProjectExportItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CanvasDrawingExport = {
|
export type CanvasDrawingExport = {
|
||||||
|
version: 1;
|
||||||
drawingId: string;
|
drawingId: string;
|
||||||
previewPath?: string;
|
previewPath?: string;
|
||||||
snapshot: unknown;
|
snapshot: unknown;
|
||||||
|
|||||||
Reference in new issue
Block a user