Browse Source

tag 支持拖拽

feature/video-erase
Luckyu_js 1 week ago
parent
commit
927aebe399
  1. 140
      src/components/WorkflowCanvas.tsx
  2. 234
      src/components/composer/ComposerMentionInput.tsx
  3. 33
      src/components/composer/GenerationComposer.tsx

140
src/components/WorkflowCanvas.tsx

@ -99,10 +99,6 @@ import { canConnectByNodeConnectionSpec } from "@/utils/nodeConnectionSpec";
const BOX_SELECTION_EXPAND_MIN_DISTANCE = 4;
const MULTI_SELECTION_BOUNDS_PADDING = 40;
const GROUP_NODE_Z_INDEX = 0;
const DEFAULT_NODE_Z_INDEX = 1;
const INITIAL_TOP_STACK_Z_INDEX = 100;
const INITIAL_TOP_MEMBER_Z_INDEX = 1;
type ScreenPoint = {
x: number;
@ -227,17 +223,15 @@ function getWorkflowNodeDimensions(node: Node): { width: number; height: number
// wrapper is left untouched during a drag.
const reactFlowNodeDimensionsCache = new WeakMap<Node, Node>();
// Cache the visual-state-decorated (className + zIndex) node keyed on the RAW
// store node. `applyNodeChanges` keeps the same reference for nodes that didn't
// move this frame, so keying on the raw node lets us return the exact same
// decorated object across frames — as long as the computed className/zIndex
// signature is unchanged. Store nodes never carry className/zIndex themselves
// (they're undefined), so without this cache the `{...node}` branch below minted
// a new object for EVERY node on EVERY frame, breaking React Flow's per-node
// `adoptUserNodes` reference check and re-rendering every wrapper during a drag.
// Cache the className-decorated node keyed on the RAW store node. `applyNodeChanges`
// keeps the same reference for nodes that didn't change this frame, so keying on the
// raw node returns the exact same decorated object across frames as long as the
// dim/skip className is unchanged — keeping React Flow's per-node wrapper untouched.
// (z-index is no longer derived here; React Flow owns it via elevateNodesOnSelect +
// parentId, so nodes carry no zIndex field and this overlay only touches className.)
const nodeVisualStateCache = new WeakMap<
WorkflowNode,
{ decorated: WorkflowNode; className: string; zIndex: number }
{ decorated: WorkflowNode; className: string }
>();
function withReactFlowNodeDimensions<T extends Node>(node: T): T {
@ -624,11 +618,6 @@ export function WorkflowCanvas() {
const [isBuildingWorkflow, setIsBuildingWorkflow] = useState(false);
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
const [zoomPercent, setZoomPercent] = useState(100);
const [draggingGroupId, setDraggingGroupId] = useState<string | null>(null);
const [stackLayerMap, setStackLayerMap] = useState<Record<string, number>>({});
const [memberLayerMap, setMemberLayerMap] = useState<Record<string, number>>({});
const [topStackLayer, setTopStackLayer] = useState(INITIAL_TOP_STACK_Z_INDEX);
const topMemberLayerRef = useRef(INITIAL_TOP_MEMBER_Z_INDEX);
const canvasGridColor = "var(--canvas-grid)";
const miniMapMaskColor = "var(--surface-overlay)";
const miniMapNodeColor = "var(--text-muted)";
@ -643,41 +632,6 @@ export function WorkflowCanvas() {
const lastCanvasPointerFlowPositionRef = useRef<ScreenPoint | null>(null);
const [boxSelectionRect, setBoxSelectionRect] = useState<FlowRect | null>(null);
const bringNodeToFront = useCallback((node: Node) => {
const nodeGroupId = node.type === "group"
? (node.data as { groupId?: string }).groupId
: (node as Node & { groupId?: string }).groupId;
const groupNodeId = nodeGroupId ? `group-node-${nodeGroupId}` : null;
if (node.type !== "group" && nodeGroupId && groupNodeId) {
topMemberLayerRef.current += 1;
const nextMemberLayer = topMemberLayerRef.current;
setMemberLayerMap((layers) => ({
...layers,
[node.id]: nextMemberLayer,
}));
setTopStackLayer((current) => {
const nextStackLayer = current + 1;
setStackLayerMap((layers) => ({
...layers,
[groupNodeId]: nextStackLayer,
}));
return nextStackLayer;
});
return;
}
setTopStackLayer((current) => {
const nextStackLayer = current + 1;
setStackLayerMap((layers) => ({
...layers,
[node.id]: nextStackLayer,
}));
return nextStackLayer;
});
}, []);
const onNodesChange = useCallback((changes: NodeChange<WorkflowNode>[]) => {
if (!boxSelectionActiveRef.current) {
storeOnNodesChange(changes);
@ -967,27 +921,14 @@ export function WorkflowCanvas() {
}
}, [tutorialActive, nodes, setCenter]);
// Apply visual state that React Flow can turn into DOM classes and z-index.
// Overlay dim/skip CSS classes that React Flow turns into DOM classes.
// z-index is owned by React Flow itself (elevateNodesOnSelect for the
// selected/dragged node + parentId auto-elevation for group members), so it is
// no longer derived here — this overlay only touches className.
const allNodes = useMemo(() => {
const draggingGroupNodeId = draggingGroupId ? `group-node-${draggingGroupId}` : null;
const groupStackLayerById = new Map<string, number>();
for (const node of nodes) {
if (node.type !== "group") continue;
const groupId = (node.data as { groupId?: string }).groupId;
const stackLayer = stackLayerMap[node.id];
if (groupId && stackLayer !== undefined) {
groupStackLayerById.set(groupId, stackLayer);
}
}
return nodes.map((node) => {
// Never dim Switch or ConditionalSwitch nodes themselves
const canDimNode = node.type !== "switch" && node.type !== "conditionalSwitch";
const isDraggingGroupShell = Boolean(draggingGroupNodeId && node.id === draggingGroupNodeId);
const isDraggingGroupMember = Boolean(draggingGroupId && node.type !== "group" && (
node.groupId === draggingGroupId || node.parentId === draggingGroupNodeId
));
const isDimmed = canDimNode && dimmedNodeIds.has(node.id);
const isSkipped = canDimNode && skippedNodeIds.has(node.id);
const extraClasses = [
@ -998,38 +939,18 @@ export function WorkflowCanvas() {
// Preserve existing className if any, add/remove dimmed/skipped classes
const baseClass = (node.className || "").replace(/\bswitch-dimmed\b/g, "").replace(/\bnode-skipped\b/g, "").trim();
const newClass = extraClasses ? `${baseClass} ${extraClasses}`.trim() : baseClass;
const nodeGroupId = (node as Node & { groupId?: string }).groupId;
const groupStackLayer = nodeGroupId ? groupStackLayerById.get(nodeGroupId) : undefined;
let zIndex = node.type === "group"
? stackLayerMap[node.id] ?? GROUP_NODE_Z_INDEX
: nodeGroupId && groupStackLayer !== undefined
? groupStackLayer + (memberLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX)
: stackLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX;
if (isDraggingGroupShell) {
zIndex = Math.max(zIndex, topStackLayer);
} else if (isDraggingGroupMember) {
zIndex = Math.max(zIndex, topStackLayer + (memberLayerMap[node.id] ?? DEFAULT_NODE_Z_INDEX));
} else if (node.dragging) {
zIndex = Math.max(zIndex, topStackLayer + 1);
}
// Return a reference-stable decorated node. Keyed on the raw store node,
// so an unchanged node yields the identical object across frames and React
// Flow leaves its wrapper untouched; only the node whose className/zIndex
// actually changed (e.g. the one being dragged) gets a new reference.
// Reference-stable: keyed on the raw store node, so an unchanged node yields
// the identical object across frames and React Flow leaves its wrapper alone.
const cached = nodeVisualStateCache.get(node);
if (cached && cached.className === newClass && cached.zIndex === zIndex) {
if (cached && cached.className === newClass) {
return cached.decorated;
}
const decorated =
node.className === newClass && node.zIndex === zIndex
? node
: { ...node, className: newClass, zIndex };
nodeVisualStateCache.set(node, { decorated, className: newClass, zIndex });
const decorated = node.className === newClass ? node : { ...node, className: newClass };
nodeVisualStateCache.set(node, { decorated, className: newClass });
return decorated;
});
}, [nodes, dimmedNodeIds, draggingGroupId, memberLayerMap, skippedNodeIds, stackLayerMap, topStackLayer]);
}, [nodes, dimmedNodeIds, skippedNodeIds]);
const absoluteNodePositions = useMemo(() => {
const byId = new Map(allNodes.map((node) => [node.id, node]));
@ -1269,7 +1190,6 @@ export function WorkflowCanvas() {
};
window.getSelection()?.removeAllRanges();
bringNodeToFront(node);
selectSingleNode(node.id);
setZoomPercent(Math.round(nextZoom * 100));
setCenter(center.x, center.y, {
@ -1277,7 +1197,7 @@ export function WorkflowCanvas() {
zoom: nextZoom,
});
}, [absoluteNodePositions, bringNodeToFront, nodes, selectSingleNode, setCenter]);
}, [absoluteNodePositions, nodes, selectSingleNode, setCenter]);
const zoomNodeById = useCallback((nodeId: string) => {
zoomNodeIntoFocusById(nodeId);
@ -1414,6 +1334,15 @@ export function WorkflowCanvas() {
[allNodes, batchNodes]
);
// Pin every edge to zIndex 0 so it always renders below nodes. React Flow only
// auto-elevates an edge's zIndex when it's left undefined (edges touching grouped
// nodes get lifted to the node's band and end up above other nodes); setting an
// explicit value bypasses that. The ternary keeps unchanged edge references stable.
const reactFlowEdges = useMemo(
() => edges.map((edge) => (edge.zIndex === 0 ? edge : { ...edge, zIndex: 0 })),
[edges]
);
const handleConnect = useCallback(
@ -3040,28 +2969,27 @@ export function WorkflowCanvas() {
<NodeDoubleClickZoomProvider value={{ zoomNodeById }}>
<ReactFlow
nodes={reactFlowNodes}
edges={edges}
edges={reactFlowEdges}
onNodesChange={handleReactFlowNodesChange}
onEdgesChange={onEdgesChange}
onConnect={handleConnect}
onConnectEnd={handleConnectEnd}
onMoveStart={() => { isPanningRef.current = true; setHoveredNodeId(null); document.documentElement.classList.add("canvas-interacting"); }}
onMoveEnd={() => { isPanningRef.current = false; document.documentElement.classList.remove("canvas-interacting"); syncZoomPercent(); }}
onNodeClick={(_event, node) => {
bringNodeToFront(node);
}}
onNodeDragStart={(_event, node) => {
isDraggingNodeRef.current = true;
document.documentElement.classList.add("canvas-interacting");
document.documentElement.classList.add("canvas-node-drag-active");
bringNodeToFront(node);
setDraggingGroupId(node.type === "group" ? (node.data as { groupId?: string }).groupId ?? null : null);
// Selecting the dragged node lets React Flow's elevateNodesOnSelect
// raise it to the top. selectNodesOnDrag is false, so React Flow
// won't select it for us; only select when it isn't already, to
// preserve an existing multi-selection drag.
if (!node.selected) selectSingleNode(node.id);
}}
onNodeDragStop={(event, node) => {
isDraggingNodeRef.current = false;
document.documentElement.classList.remove("canvas-interacting");
document.documentElement.classList.remove("canvas-node-drag-active");
setDraggingGroupId(null);
handleNodeDragStop(event, node);
}}
onPaneClick={handlePaneClick}
@ -3082,7 +3010,7 @@ export function WorkflowCanvas() {
selectionKeyCode={["Space", "Control", "Meta"]}
panOnDrag={canvasPanOnDrag}
selectNodesOnDrag={false}
elevateNodesOnSelect={false}
elevateNodesOnSelect={true}
nodesFocusable={false}
nodeDragThreshold={5}
nodeClickDistance={5}

234
src/components/composer/ComposerMentionInput.tsx

@ -11,6 +11,7 @@ import {
type CompositionEvent,
type KeyboardEvent,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type ReactNode,
type SyntheticEvent,
} from "react";
@ -172,6 +173,30 @@ function parseTokensFromDom(element: HTMLDivElement): ComposerPromptToken[] {
return tokens;
}
function createReferenceTagLeadingVisual(
token: Extract<ComposerPromptToken, { type: "reference" }>
): HTMLElement | null {
if (token.thumbnailUrl) {
const thumbnail = document.createElement("img");
thumbnail.src = token.thumbnailUrl;
thumbnail.alt = "";
thumbnail.draggable = false;
thumbnail.className = "h-4 w-4 shrink-0 rounded-[3px] object-cover";
return thumbnail;
}
if (token.mediaType === "audio" || token.mediaType === "video") {
const icon = document.createElement("span");
icon.contentEditable = "false";
icon.setAttribute("contenteditable", "false");
icon.className = "flex h-4 w-4 shrink-0 items-center justify-center rounded-[3px] bg-[var(--surface-active)] text-[var(--text-muted)]";
icon.innerHTML = token.mediaType === "audio"
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-3 w-3"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>'
: '<svg viewBox="0 0 24 24" fill="currentColor" class="h-3 w-3"><path d="M8 5v14l11-7z"/></svg>';
return icon;
}
return null;
}
function createReferenceTagElement(item: ComposerMentionReferenceItem): HTMLElement | null {
const token = createReferenceToken(item);
if (!token || token.type !== "reference") return null;
@ -187,22 +212,28 @@ function createReferenceTagElement(item: ComposerMentionReferenceItem): HTMLElem
tag.dataset.referenceUrl = token.url;
if (token.thumbnailUrl) tag.dataset.referenceThumbnailUrl = token.thumbnailUrl;
if (token.duration) tag.dataset.referenceDuration = String(token.duration);
tag.className = "mx-0.5 inline-flex h-5 max-w-[12rem] select-none items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-1)] px-1.5 align-middle text-xs leading-none text-[var(--text-primary)]";
tag.className = "mx-0.5 inline-flex h-5 max-w-[12rem] cursor-grab select-none items-center gap-1 rounded-md border border-[var(--border-subtle)] bg-[var(--surface-1)] px-1.5 align-middle text-xs leading-none text-[var(--text-primary)] active:cursor-grabbing";
if (token.thumbnailUrl) {
const thumbnail = document.createElement("img");
thumbnail.src = token.thumbnailUrl;
thumbnail.alt = "";
thumbnail.draggable = false;
thumbnail.className = "h-4 w-4 shrink-0 rounded-[3px] object-cover";
tag.appendChild(thumbnail);
}
const leadingVisual = createReferenceTagLeadingVisual(token);
if (leadingVisual) tag.appendChild(leadingVisual);
const label = document.createElement("span");
label.contentEditable = "false";
label.setAttribute("contenteditable", "false");
label.className = "min-w-0 truncate";
label.textContent = token.label;
// Hide the leading "@" visually while keeping it in the DOM text, so caret/offset math
// (which counts rendered text via Range.toString) stays aligned with the prompt string
// that includes "@". Stripping it from textContent shifts every downstream offset.
const atMatch = token.label.match(/^(@+)([\s\S]*)$/);
if (atMatch) {
const hiddenAt = document.createElement("span");
hiddenAt.textContent = atMatch[1];
hiddenAt.style.display = "none";
label.appendChild(hiddenAt);
label.appendChild(document.createTextNode(atMatch[2]));
} else {
label.textContent = token.label;
}
tag.appendChild(label);
return tag;
}
@ -261,6 +292,93 @@ function isReferenceTagNode(node: Node | null): node is HTMLElement {
return node instanceof HTMLElement && node.dataset.composerReferenceTag === "true";
}
const REFERENCE_TAG_DRAG_THRESHOLD_PX = 4;
function getReferenceTagAncestor(node: Node | null, root: HTMLElement): HTMLElement | null {
let current: Node | null = node;
while (current && current !== root) {
if (isReferenceTagNode(current)) return current;
current = current.parentNode;
}
return null;
}
function caretRangeFromPoint(x: number, y: number): Range | null {
const doc = document as Document & {
caretRangeFromPoint?: (x: number, y: number) => Range | null;
caretPositionFromPoint?: (x: number, y: number) => { offsetNode: Node; offset: number } | null;
};
if (typeof doc.caretRangeFromPoint === "function") {
return doc.caretRangeFromPoint(x, y);
}
if (typeof doc.caretPositionFromPoint === "function") {
const position = doc.caretPositionFromPoint(x, y);
if (!position) return null;
const range = document.createRange();
range.setStart(position.offsetNode, position.offset);
range.collapse(true);
return range;
}
return null;
}
/**
* Resolve the collapsed caret range for a drop point, snapping outside any reference tag
* so a dragged tag is never inserted inside another tag.
*/
function resolveReferenceDrop(
element: HTMLDivElement,
draggedTag: HTMLElement,
clientX: number,
clientY: number
): Range | null {
const range = caretRangeFromPoint(clientX, clientY);
if (!range || !element.contains(range.startContainer)) return null;
const tagAncestor = getReferenceTagAncestor(range.startContainer, element);
if (tagAncestor === draggedTag) return null;
if (tagAncestor) {
const tagRect = tagAncestor.getBoundingClientRect();
if (clientX > tagRect.left + tagRect.width / 2) {
range.setStartAfter(tagAncestor);
} else {
range.setStartBefore(tagAncestor);
}
range.collapse(true);
}
return range;
}
/**
* Floating drag preview that follows the pointer a clone of the whole tag
* (cover image + label), so the user drags a visual copy of the chip itself.
*/
function createReferenceDragPreview(tag: HTMLElement): HTMLElement {
const preview = document.createElement("div");
preview.style.position = "fixed";
preview.style.left = "0";
preview.style.top = "0";
preview.style.zIndex = "10090";
preview.style.pointerEvents = "none";
preview.style.opacity = "0.9";
preview.style.willChange = "transform";
const clone = tag.cloneNode(true) as HTMLElement;
clone.style.opacity = "1";
clone.style.margin = "0";
clone.style.boxShadow = "0 8px 24px rgba(0, 0, 0, 0.4)";
preview.appendChild(clone);
document.body.appendChild(preview);
return preview;
}
function positionReferenceDragPreview(preview: HTMLElement, clientX: number, clientY: number): void {
const width = preview.offsetWidth;
const height = preview.offsetHeight;
preview.style.transform = `translate(${clientX - width / 2}px, ${clientY - height - 2}px)`;
}
function getChildNodeIndex(element: HTMLDivElement, node: Node): number {
return Array.prototype.indexOf.call(element.childNodes, node);
}
@ -423,6 +541,7 @@ export const ComposerMentionInput = forwardRef<HTMLDivElement, ComposerMentionIn
const inputRef = useRef<HTMLDivElement>(null);
const isComposingRef = useRef(false);
const mentionTriggerIndexRef = useRef<number | null>(null);
const pointerDragCleanupRef = useRef<(() => void) | null>(null);
const [menuPosition, setMenuPosition] = useState<{ left: number; top: number } | null>(null);
const [textContextMenu, setTextContextMenu] = useState<TextContextMenuState | null>(null);
const [activeReferenceIndex, setActiveReferenceIndex] = useState(0);
@ -457,6 +576,99 @@ export const ComposerMentionInput = forwardRef<HTMLDivElement, ComposerMentionIn
onChange(nextValue, textToTokens(nextValue));
}, [onChange]);
const handleReferenceTagPointerDown = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
// Preserve the canvas-interaction guard for every pointer down on the editor.
event.stopPropagation();
if (readOnly || event.button !== 0 || !event.isPrimary) return;
const editor = inputRef.current;
if (!editor) return;
const tag = getReferenceTagAncestor(event.target as Node, editor);
if (!tag) return;
// Take over from contentEditable so the chip drags instead of placing a caret.
event.preventDefault();
closeReferenceMenu();
const pointerId = event.pointerId;
const startX = event.clientX;
const startY = event.clientY;
let dragging = false;
let preview: HTMLElement | null = null;
let handleMove: (moveEvent: PointerEvent) => void;
let handleUp: (upEvent: PointerEvent) => void;
let handleCancel: (cancelEvent: PointerEvent) => void;
const cleanup = () => {
window.removeEventListener("pointermove", handleMove);
window.removeEventListener("pointerup", handleUp);
window.removeEventListener("pointercancel", handleCancel);
tag.style.opacity = "";
if (preview) {
preview.remove();
preview = null;
}
pointerDragCleanupRef.current = null;
};
handleMove = (moveEvent) => {
if (moveEvent.pointerId !== pointerId) return;
if (!dragging) {
if (Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < REFERENCE_TAG_DRAG_THRESHOLD_PX) return;
dragging = true;
editor.focus({ preventScroll: true });
tag.style.opacity = "0.35";
preview = createReferenceDragPreview(tag);
}
moveEvent.preventDefault();
if (preview) positionReferenceDragPreview(preview, moveEvent.clientX, moveEvent.clientY);
const dropRange = resolveReferenceDrop(editor, tag, moveEvent.clientX, moveEvent.clientY);
if (dropRange) {
// Move the real input caret to the drop point so it visibly follows the pointer.
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
selection.addRange(dropRange.cloneRange());
}
}
};
handleUp = (upEvent) => {
if (upEvent.pointerId !== pointerId) return;
const wasDragging = dragging;
const dropX = upEvent.clientX;
const dropY = upEvent.clientY;
cleanup();
if (!wasDragging || !editor.contains(tag)) return;
const dropRange = resolveReferenceDrop(editor, tag, dropX, dropY);
if (!dropRange) return;
dropRange.insertNode(tag);
editor.normalize();
emitDomValue(editor);
window.requestAnimationFrame(() => {
if (!tag.isConnected) return;
const caretRange = document.createRange();
caretRange.setStartAfter(tag);
caretRange.collapse(true);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(caretRange);
});
};
handleCancel = (cancelEvent) => {
if (cancelEvent.pointerId !== pointerId) return;
cleanup();
};
pointerDragCleanupRef.current = cleanup;
window.addEventListener("pointermove", handleMove);
window.addEventListener("pointerup", handleUp);
window.addEventListener("pointercancel", handleCancel);
}, [closeReferenceMenu, emitDomValue, readOnly]);
useEffect(() => () => {
pointerDragCleanupRef.current?.();
}, []);
const handleTextContextMenu = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
@ -814,7 +1026,7 @@ export const ComposerMentionInput = forwardRef<HTMLDivElement, ComposerMentionIn
emitDomValue(event.currentTarget);
onCompositionEnd?.(event);
}}
onPointerDownCapture={stopCanvasInteraction}
onPointerDownCapture={handleReferenceTagPointerDown}
onMouseDownCapture={stopCanvasInteraction}
onWheelCapture={stopCanvasInteraction}
style={{

33
src/components/composer/GenerationComposer.tsx

@ -48,6 +48,7 @@ import {
} from "@/utils/composerPricingContext";
import { useAppConfigStore } from "@/store/appConfigStore";
import { buildPreviewImageUrl } from "@/utils/previewImageUrl";
import { buildVideoPosterUrl } from "@/utils/videoPosterUrl";
import {
getConcreteGenerationNodeType,
inferCapabilityFromModel,
@ -412,6 +413,7 @@ function ComposerEditor({
void embedded;
const { t } = useI18n();
const previewImageSuffix = useAppConfigStore((state) => state.previewImageSuffix);
const videoPosterQuality = useAppConfigStore((state) => state.videoPosterQuality);
const promptInputRef = useRef<HTMLDivElement>(null);
const referenceAudioPreviewRef = useRef<HTMLAudioElement | null>(null);
const referenceMaterialsRef = useRef<ComposerReferenceMaterial[]>([]);
@ -962,18 +964,25 @@ function ComposerEditor({
}];
}
return referenceMaterials.map((material) => ({
key: `${material.type}-${material.id}-${material.url.slice(0, 24)}`,
label: material.name.replace(/^@/, ""),
value: `${material.name} `,
image: material.type === "image" ? buildPreviewImageUrl(material.url, previewImageSuffix) ?? material.url : undefined,
sourceNodeId: material.sourceNodeId,
referenceType: material.type,
url: material.url,
thumbnailUrl: material.thumbnailUrl,
duration: material.duration,
}));
}, [previewImageSuffix, referenceMaterials, t]);
return referenceMaterials.map((material) => {
const cover = material.type === "image"
? buildPreviewImageUrl(material.url, previewImageSuffix) ?? material.url
: material.type === "video"
? buildVideoPosterUrl(material.url, videoPosterQuality) ?? undefined
: undefined;
return {
key: `${material.type}-${material.id}-${material.url.slice(0, 24)}`,
label: material.name.replace(/^@/, ""),
value: `${material.name} `,
image: cover,
sourceNodeId: material.sourceNodeId,
referenceType: material.type,
url: material.url,
thumbnailUrl: cover,
duration: material.duration,
};
});
}, [previewImageSuffix, referenceMaterials, t, videoPosterQuality]);
const runAction = useCallback(() => {
if (isCurrentNodeRunning) {

Loading…
Cancel
Save