improve canvas generation placement
This commit is contained in:
parent
016d588559
commit
03aee55cb6
@ -210,6 +210,30 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes canvas-generated-pulse {
|
||||
0% {
|
||||
opacity: 0.9;
|
||||
transform: scale(0.985);
|
||||
box-shadow:
|
||||
0 0 0 0 rgba(124, 156, 255, 0.3),
|
||||
0 0 26px rgba(124, 156, 255, 0.2);
|
||||
}
|
||||
55% {
|
||||
opacity: 0.35;
|
||||
transform: scale(1.03);
|
||||
box-shadow:
|
||||
0 0 0 12px rgba(124, 156, 255, 0.02),
|
||||
0 0 42px rgba(124, 156, 255, 0.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(1.055);
|
||||
box-shadow:
|
||||
0 0 0 18px rgba(124, 156, 255, 0),
|
||||
0 0 52px rgba(124, 156, 255, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-dark-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.18) rgba(255, 255, 255, 0.04);
|
||||
|
||||
@ -436,6 +436,72 @@ const CANVAS_AGENT_SECTION_TITLES = new Set([
|
||||
]);
|
||||
const CANVAS_IMAGE_TASK_POLL_INTERVAL_MS = 1600;
|
||||
const CANVAS_IMAGE_TASK_TIMEOUT_MS = 4 * 60 * 1000;
|
||||
const CANVAS_GENERATION_NODE_GAP = 56;
|
||||
const CANVAS_GENERATION_STACK_GAP = 28;
|
||||
|
||||
function findCanvasImageAnchorNode(
|
||||
nodes: CanvasNode[],
|
||||
params: {
|
||||
selectedNodes?: CanvasNode[];
|
||||
sourceImageUrls?: string[];
|
||||
} = {}
|
||||
) {
|
||||
const sourceKeys = new Set(
|
||||
(params.sourceImageUrls || [])
|
||||
.map((url) => normalizeImageIdentityUrl(url))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
if (sourceKeys.size > 0) {
|
||||
const matchedNode = nodes.find((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const src = String((node.data as any)?.src || "").trim();
|
||||
return sourceKeys.has(normalizeImageIdentityUrl(src));
|
||||
});
|
||||
if (matchedNode) return matchedNode;
|
||||
}
|
||||
|
||||
return params.selectedNodes?.find((node) => node.type === "image") || null;
|
||||
}
|
||||
|
||||
function getAnchoredGenerationPosition(params: {
|
||||
anchorNode?: CanvasNode | null;
|
||||
size: { width: number; height: number };
|
||||
index?: number;
|
||||
fallbackNodeCount?: number;
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
}) {
|
||||
const index = params.index || 0;
|
||||
const anchorNode = params.anchorNode || null;
|
||||
|
||||
if (anchorNode) {
|
||||
const anchorWidth = Number(anchorNode.size?.width || 300);
|
||||
const anchorHeight = Number(anchorNode.size?.height || 300);
|
||||
const stackStep = Math.max(
|
||||
96,
|
||||
Math.min(params.size.height, anchorHeight) + CANVAS_GENERATION_STACK_GAP
|
||||
);
|
||||
return {
|
||||
x: anchorNode.position.x + anchorWidth + CANVAS_GENERATION_NODE_GAP,
|
||||
y: anchorNode.position.y + index * stackStep,
|
||||
};
|
||||
}
|
||||
|
||||
const viewport = params.viewport || { x: 0, y: 0, zoom: 1 };
|
||||
const zoom = Math.max(0.1, viewport.zoom || 1);
|
||||
if (typeof window !== "undefined") {
|
||||
return {
|
||||
x: (-viewport.x + window.innerWidth * 0.52) / zoom - params.size.width / 2 + index * 28,
|
||||
y: (-viewport.y + window.innerHeight * 0.46) / zoom - params.size.height / 2 + index * 28,
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackNodeCount = params.fallbackNodeCount || 0;
|
||||
return {
|
||||
x: 100 + (fallbackNodeCount % 3) * (params.size.width + 20),
|
||||
y: 100 + Math.floor(fallbackNodeCount / 3) * (params.size.height + 20),
|
||||
};
|
||||
}
|
||||
|
||||
type CanvasImageGenerationRequest = {
|
||||
prompt: string;
|
||||
@ -1372,6 +1438,8 @@ export function CanvasChatPanel({
|
||||
const nodes = useCanvasStore((state) => state.nodes);
|
||||
const projectTitle = useCanvasStore((state) => state.projectTitle);
|
||||
const selectedNodeIds = useCanvasStore((state) => state.selectedNodeIds);
|
||||
const selectNodes = useCanvasStore((state) => state.selectNodes);
|
||||
const setViewport = useCanvasStore((state) => state.setViewport);
|
||||
const clearSelection = useCanvasStore((state) => state.clearSelection);
|
||||
const pendingChatAttachments = useCanvasStore((state) => state.pendingChatAttachments);
|
||||
const clearPendingChatAttachments = useCanvasStore((state) => state.clearPendingChatAttachments);
|
||||
@ -1445,8 +1513,44 @@ export function CanvasChatPanel({
|
||||
);
|
||||
|
||||
// 获取选中的图片节点
|
||||
const selectedImageNodes = nodes.filter(
|
||||
n => selectedNodeIds.includes(n.id) && isCanvasImageReadyForAi(n)
|
||||
const selectedImageNodes = useMemo(
|
||||
() =>
|
||||
nodes.filter(
|
||||
(node) => selectedNodeIds.includes(node.id) && isCanvasImageReadyForAi(node)
|
||||
),
|
||||
[nodes, selectedNodeIds]
|
||||
);
|
||||
const selectedImageNodesRef = useRef<CanvasNode[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedImageNodesRef.current = selectedImageNodes;
|
||||
}, [selectedImageNodes]);
|
||||
|
||||
const focusCanvasOnNode = useCallback(
|
||||
(node: Pick<CanvasNode, "position" | "size">) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const { viewport } = useCanvasStore.getState();
|
||||
const width = Number(node.size?.width || 300);
|
||||
const height = Number(node.size?.height || 300);
|
||||
const availableWidth = Math.max(360, window.innerWidth * 0.54);
|
||||
const availableHeight = Math.max(320, window.innerHeight * 0.52);
|
||||
const fitZoom = Math.min(
|
||||
1.16,
|
||||
availableWidth / Math.max(width, 1),
|
||||
availableHeight / Math.max(height, 1)
|
||||
);
|
||||
const zoom = Math.max(0.48, Math.min(fitZoom, 4));
|
||||
const centerX = node.position.x + width / 2;
|
||||
const centerY = node.position.y + height / 2;
|
||||
window.requestAnimationFrame(() => {
|
||||
setViewport({
|
||||
x: window.innerWidth * 0.52 - centerX * zoom,
|
||||
y: window.innerHeight * 0.46 - centerY * zoom,
|
||||
zoom,
|
||||
});
|
||||
});
|
||||
},
|
||||
[setViewport]
|
||||
);
|
||||
|
||||
const handleAssistantVote = useCallback(
|
||||
@ -3283,9 +3387,15 @@ export function CanvasChatPanel({
|
||||
return;
|
||||
}
|
||||
|
||||
removeCanvasGenerationPlaceholders();
|
||||
|
||||
const latestNodes = useCanvasStore.getState().nodes;
|
||||
const placeholderNodes = latestNodes.filter((node) => {
|
||||
if (node.type !== "image") return false;
|
||||
const metadata = ((node.data as any)?.metadata || {}) as Record<string, unknown>;
|
||||
return (
|
||||
metadata.canvasGenerationPlaceholder === true &&
|
||||
(!metadata.chatId || String(metadata.chatId) === activeChatId)
|
||||
);
|
||||
});
|
||||
const suppressedMediaKeys = new Set(
|
||||
(useCanvasStore.getState().getCanvasState().suppressedMediaKeys || [])
|
||||
.map((item) => String(item || "").trim())
|
||||
@ -3332,6 +3442,8 @@ export function CanvasChatPanel({
|
||||
String(payload.agentExecutionSnapshotId || "").trim() || undefined;
|
||||
let addedImageCount = 0;
|
||||
let addedVideoCount = 0;
|
||||
const insertedNodeIds: string[] = [];
|
||||
const completedAt = Date.now();
|
||||
|
||||
for (let index = 0; index < imageUrls.length; index += 1) {
|
||||
const url = imageUrls[index]!;
|
||||
@ -3356,11 +3468,6 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
const dimensions = await getImageDimensions(url);
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const existingNodes = currentNodes.length + addedImageCount + addedVideoCount;
|
||||
const spacing = 20;
|
||||
const x = 100 + (existingNodes % 3) * (dimensions.width + spacing);
|
||||
const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing);
|
||||
const compareSourceUrlRaw =
|
||||
sourceImageUrls[Math.min(index, Math.max(0, sourceImageUrls.length - 1))] ||
|
||||
sourceImageUrls[0] ||
|
||||
@ -3380,36 +3487,79 @@ export function CanvasChatPanel({
|
||||
})?.id || ""
|
||||
: "";
|
||||
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position: { x, y },
|
||||
size: { width: dimensions.width, height: dimensions.height },
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
title: "图片",
|
||||
prompt: generationPrompt,
|
||||
originalSrc: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt,
|
||||
sourceMessageId: messageId || undefined,
|
||||
sourceImageKey,
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
agentPresetId,
|
||||
agentPresetName,
|
||||
agentCategoryId,
|
||||
agentCategoryName,
|
||||
agentExecutionSnapshotId,
|
||||
},
|
||||
},
|
||||
connections: sourceNodeId ? [sourceNodeId] : [],
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const anchorNode = findCanvasImageAnchorNode(currentNodes, {
|
||||
selectedNodes: selectedImageNodesRef.current,
|
||||
sourceImageUrls,
|
||||
});
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
const data = {
|
||||
src: url,
|
||||
status: "completed" as const,
|
||||
title: "图片",
|
||||
prompt: generationPrompt,
|
||||
originalSrc: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
metadata: {
|
||||
originalWidth: dimensions.originalWidth,
|
||||
originalHeight: dimensions.originalHeight,
|
||||
generationPrompt,
|
||||
sourceMessageId: messageId || undefined,
|
||||
sourceImageKey,
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
agentPresetId,
|
||||
agentPresetName,
|
||||
agentCategoryId,
|
||||
agentCategoryName,
|
||||
agentExecutionSnapshotId,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
recentlyGeneratedAt: completedAt,
|
||||
},
|
||||
};
|
||||
const placeholderNode = placeholderNodes[index];
|
||||
const nodeId = placeholderNode
|
||||
? placeholderNode.id
|
||||
: addNode({
|
||||
type: "image",
|
||||
position,
|
||||
size,
|
||||
data,
|
||||
connections: sourceNodeId ? [sourceNodeId] : anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
if (placeholderNode) {
|
||||
updateNode(nodeId, {
|
||||
position,
|
||||
size,
|
||||
data,
|
||||
connections: sourceNodeId ? [sourceNodeId] : anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
}
|
||||
insertedNodeIds.push(nodeId);
|
||||
void autoNameGeneratedImageNode(nodeId, url);
|
||||
addedImageCount += 1;
|
||||
}
|
||||
|
||||
placeholderNodes.slice(imageUrls.length).forEach((node) => deleteNode(node.id));
|
||||
if (insertedNodeIds.length > 0) {
|
||||
selectNodes(insertedNodeIds);
|
||||
const focusNode = useCanvasStore
|
||||
.getState()
|
||||
.nodes.find((node) => node.id === insertedNodeIds[0]);
|
||||
if (focusNode) {
|
||||
focusCanvasOnNode(focusNode);
|
||||
}
|
||||
} else if (videoUrls.length > 0) {
|
||||
removeCanvasGenerationPlaceholders(activeChatId);
|
||||
}
|
||||
|
||||
for (let index = 0; index < videoUrls.length; index += 1) {
|
||||
const url = videoUrls[index]!;
|
||||
const key = normalizeMediaUrl(url);
|
||||
@ -3446,7 +3596,16 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
},
|
||||
[addNode, autoNameGeneratedImageNode, removeCanvasGenerationPlaceholders]
|
||||
[
|
||||
activeChatId,
|
||||
addNode,
|
||||
autoNameGeneratedImageNode,
|
||||
deleteNode,
|
||||
focusCanvasOnNode,
|
||||
removeCanvasGenerationPlaceholders,
|
||||
selectNodes,
|
||||
updateNode,
|
||||
]
|
||||
);
|
||||
|
||||
const {
|
||||
@ -4451,6 +4610,8 @@ export function CanvasChatPanel({
|
||||
|
||||
let addedImageCount = 0;
|
||||
let addedVideoCount = 0;
|
||||
const insertedNodeIds: string[] = [];
|
||||
const completedAt = Date.now();
|
||||
|
||||
const newImageEntries = imageUrls.reduce<Array<{ url: string; sourceImageKey: string }>>(
|
||||
(acc, url) => {
|
||||
@ -4505,10 +4666,6 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
const dimensions = await getImageDimensions(url);
|
||||
const existingNodes = nodes.length + addedImageCount + addedVideoCount;
|
||||
const spacing = 20;
|
||||
const x = 100 + (existingNodes % 3) * (dimensions.width + spacing);
|
||||
const y = 100 + Math.floor(existingNodes / 3) * (dimensions.height + spacing);
|
||||
const compareSourceUrlRaw =
|
||||
nearestUserImageUrls[Math.min(index, Math.max(0, nearestUserImageUrls.length - 1))] ||
|
||||
nearestUserImageUrls[0] ||
|
||||
@ -4528,10 +4685,24 @@ export function CanvasChatPanel({
|
||||
})?.id || ""
|
||||
: "";
|
||||
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const anchorNode = findCanvasImageAnchorNode(currentNodes, {
|
||||
selectedNodes: selectedImageNodesRef.current,
|
||||
sourceImageUrls: nearestUserImageUrls,
|
||||
});
|
||||
const size = { width: dimensions.width, height: dimensions.height };
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
fallbackNodeCount: currentNodes.length + addedImageCount + addedVideoCount,
|
||||
index,
|
||||
size,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position: { x, y },
|
||||
size: { width: dimensions.width, height: dimensions.height },
|
||||
position,
|
||||
size,
|
||||
data: {
|
||||
src: url,
|
||||
status: "completed",
|
||||
@ -4545,14 +4716,29 @@ export function CanvasChatPanel({
|
||||
sourceMessageId: message.id,
|
||||
sourceImageKey: imageEntry.sourceImageKey,
|
||||
compareSourceUrl: canUseCompareSource ? compareSourceUrl : undefined,
|
||||
sourceNodeId: sourceNodeId || anchorNode?.id || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
recentlyGeneratedAt: completedAt,
|
||||
},
|
||||
},
|
||||
connections: sourceNodeId ? [sourceNodeId] : [],
|
||||
connections: sourceNodeId ? [sourceNodeId] : anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
insertedNodeIds.push(nodeId);
|
||||
void autoNameGeneratedImageNode(nodeId, url);
|
||||
addedImageCount += 1;
|
||||
}
|
||||
|
||||
if (insertedNodeIds.length > 0) {
|
||||
selectNodes(insertedNodeIds);
|
||||
const focusNode = useCanvasStore
|
||||
.getState()
|
||||
.nodes.find((node) => node.id === insertedNodeIds[0]);
|
||||
if (focusNode) {
|
||||
focusCanvasOnNode(focusNode);
|
||||
}
|
||||
}
|
||||
|
||||
const newVideoUrls = videoUrls.filter((url) => {
|
||||
const key = normalizeMediaUrl(url);
|
||||
if (!key || existingVideoUrlKeys.has(key)) return false;
|
||||
@ -4588,7 +4774,14 @@ export function CanvasChatPanel({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [messages, addNode, nodes, autoNameGeneratedImageNode]);
|
||||
}, [
|
||||
messages,
|
||||
addNode,
|
||||
nodes,
|
||||
autoNameGeneratedImageNode,
|
||||
focusCanvasOnNode,
|
||||
selectNodes,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodes.length < 2) return;
|
||||
@ -7935,6 +8128,8 @@ export function CanvasChatPanel({
|
||||
phase === "queued" ? 12 : phase === "finalizing" ? 96 : phase === "failed" ? 100 : undefined,
|
||||
modelName: streamState.modelName || currentModel?.displayName || undefined,
|
||||
providerType: streamState.providerType || currentModel?.providerType || undefined,
|
||||
placementMode: "anchored-generation",
|
||||
allowOverlap: true,
|
||||
};
|
||||
|
||||
if (placeholderId) {
|
||||
@ -7962,13 +8157,18 @@ export function CanvasChatPanel({
|
||||
}
|
||||
|
||||
const currentNodes = useCanvasStore.getState().nodes;
|
||||
const existingNodes = currentNodes.length;
|
||||
const anchorNode = findCanvasImageAnchorNode(currentNodes, {
|
||||
selectedNodes: selectedImageNodesRef.current,
|
||||
});
|
||||
const position = getAnchoredGenerationPosition({
|
||||
anchorNode,
|
||||
fallbackNodeCount: currentNodes.length,
|
||||
size: placeholderSize,
|
||||
viewport: useCanvasStore.getState().viewport,
|
||||
});
|
||||
const nodeId = addNode({
|
||||
type: "image",
|
||||
position: {
|
||||
x: 100 + (existingNodes % 3) * (placeholderSize.width + 20),
|
||||
y: 100 + Math.floor(existingNodes / 3) * (placeholderSize.height + 20),
|
||||
},
|
||||
position,
|
||||
size: placeholderSize,
|
||||
data: {
|
||||
src: "",
|
||||
@ -7977,16 +8177,20 @@ export function CanvasChatPanel({
|
||||
prompt: label,
|
||||
metadata,
|
||||
},
|
||||
connections: [],
|
||||
connections: anchorNode?.id ? [anchorNode.id] : [],
|
||||
});
|
||||
canvasGenerationPlaceholderByChatIdRef.current[activeChatId] = nodeId;
|
||||
selectNodes([nodeId]);
|
||||
focusCanvasOnNode({ position, size: placeholderSize });
|
||||
}, [
|
||||
activeChatId,
|
||||
addNode,
|
||||
currentModel?.displayName,
|
||||
currentModel?.providerType,
|
||||
imageParams,
|
||||
focusCanvasOnNode,
|
||||
removeCanvasGenerationPlaceholders,
|
||||
selectNodes,
|
||||
streamImageGenerationStateByChatId,
|
||||
updateNode,
|
||||
]);
|
||||
|
||||
@ -1282,6 +1282,15 @@ export function ImageNode({
|
||||
Number.isFinite(data.metadata.generationProgress)
|
||||
? Math.max(0, Math.min(100, Math.round(data.metadata.generationProgress)))
|
||||
: null;
|
||||
const recentlyGeneratedAt =
|
||||
typeof data.metadata?.recentlyGeneratedAt === "number" &&
|
||||
Number.isFinite(data.metadata.recentlyGeneratedAt)
|
||||
? data.metadata.recentlyGeneratedAt
|
||||
: 0;
|
||||
const isRecentlyGenerated =
|
||||
Boolean(data.src) &&
|
||||
recentlyGeneratedAt > 0 &&
|
||||
Date.now() - recentlyGeneratedAt < 20_000;
|
||||
const flipRotateOriginalSrc =
|
||||
data.metadata && typeof data.metadata === "object"
|
||||
? String((data.metadata as Record<string, unknown>).flipRotateOriginalSrc || "").trim()
|
||||
@ -7521,17 +7530,25 @@ export function ImageNode({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full w-full rounded-none border border-[#dbe2ee] bg-white shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_16px_34px_rgba(22,24,29,0.08)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
localEditMode ? "overflow-visible" : "overflow-hidden",
|
||||
isSelected
|
||||
? "border-[#5b76fe] ring-2 ring-[#5b76fe]/14 shadow-[0_0_0_1px_rgba(91,118,254,0.14),0_20px_38px_rgba(91,118,254,0.12)]"
|
||||
: "hover:border-[#cfd9ea]"
|
||||
)}
|
||||
"relative h-full w-full rounded-none border border-[#dbe2ee] bg-white shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_16px_34px_rgba(22,24,29,0.08)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
localEditMode ? "overflow-visible" : "overflow-hidden",
|
||||
isRecentlyGenerated &&
|
||||
"border-[#7c9cff] shadow-[0_0_0_1px_rgba(124,156,255,0.42),0_0_0_8px_rgba(124,156,255,0.12),0_18px_42px_rgba(91,118,254,0.22)]",
|
||||
isSelected
|
||||
? "border-[#5b76fe] ring-2 ring-[#5b76fe]/14 shadow-[0_0_0_1px_rgba(91,118,254,0.14),0_20px_38px_rgba(91,118,254,0.12)]"
|
||||
: "hover:border-[#cfd9ea]"
|
||||
)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="relative h-full w-full" style={scaledContentStyle}>
|
||||
>
|
||||
{isRecentlyGenerated ? (
|
||||
<div
|
||||
className="pointer-events-none absolute -inset-2 z-[3] rounded-[inherit] border border-[#8fb0ff]/55"
|
||||
style={{ animation: "canvas-generated-pulse 1.35s ease-out 0s 4" }}
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative h-full w-full" style={scaledContentStyle}>
|
||||
{/* 图片内容 - 使用 object-contain 保持原始比例 */}
|
||||
{data.src ? (
|
||||
<div className="relative h-full w-full">
|
||||
|
||||
@ -80,14 +80,15 @@ uniform float uTime;
|
||||
uniform float uOpacity;
|
||||
varying float vDistance;
|
||||
|
||||
void main() {
|
||||
float dist = distance(gl_PointCoord, vec2(0.5));
|
||||
if (dist > 0.5) discard;
|
||||
float strength = pow(1.0 - dist * 2.0, 1.55);
|
||||
float shimmer = 0.72 + sin(vDistance * 0.33 + uTime) * 0.22;
|
||||
gl_FragColor = vec4(vec3(0.12, 0.13, 0.14), strength * shimmer * uOpacity);
|
||||
}
|
||||
`;
|
||||
void main() {
|
||||
float dist = distance(gl_PointCoord, vec2(0.5));
|
||||
if (dist > 0.5) discard;
|
||||
float edge = 1.0 - smoothstep(0.42, 0.5, dist);
|
||||
float core = 0.92 + (1.0 - smoothstep(0.0, 0.34, dist)) * 0.08;
|
||||
float shimmer = 0.78 + sin(vDistance * 0.33 + uTime) * 0.18;
|
||||
gl_FragColor = vec4(vec3(0.11, 0.12, 0.13), edge * core * shimmer * uOpacity);
|
||||
}
|
||||
`;
|
||||
|
||||
function clampProgress(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
@ -138,9 +139,12 @@ function ScatterParticleLayer({
|
||||
antialias: true,
|
||||
powerPreference: "high-performance",
|
||||
});
|
||||
renderer.setClearColor(0xffffff, 0);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.75));
|
||||
container.appendChild(renderer.domElement);
|
||||
renderer.setClearColor(0xffffff, 0);
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2.5));
|
||||
renderer.domElement.style.display = "block";
|
||||
renderer.domElement.style.height = "100%";
|
||||
renderer.domElement.style.width = "100%";
|
||||
container.appendChild(renderer.domElement);
|
||||
|
||||
const particleCount = compact ? 7200 : 12800;
|
||||
const positions = new Float32Array(particleCount * 3);
|
||||
@ -172,7 +176,7 @@ function ScatterParticleLayer({
|
||||
blending: THREE.NormalBlending,
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
uPointSize: { value: compact ? 88 : 108 },
|
||||
uPointSize: { value: compact ? 76 : 94 },
|
||||
uEffectIntensity: { value: 1 },
|
||||
uOpacity: { value: opacity },
|
||||
},
|
||||
@ -256,7 +260,7 @@ export function GenerationDotPlaceholder({
|
||||
);
|
||||
const rotatingTitle = ROTATING_TITLES[Math.floor(elapsedMs / 3200) % ROTATING_TITLES.length];
|
||||
const resolvedTitle = title || (phase === "processing" ? rotatingTitle : PHASE_TITLES[phase]);
|
||||
const dotOpacity = phase === "failed" ? 0.18 : 0.42;
|
||||
const dotOpacity = phase === "failed" ? 0.18 : 0.56;
|
||||
|
||||
const progressStyle = useMemo(
|
||||
() => ({ width: `${resolvedProgress}%` }),
|
||||
|
||||
@ -1869,7 +1869,7 @@ function PureModelSelectorCompact({
|
||||
</Button>
|
||||
)}
|
||||
</Trigger>
|
||||
<PromptInputModelSelectContent className="!w-[344px] max-h-[426px] overflow-hidden rounded-[22px] border border-[#bfc6cf] bg-[#f2f3f5]/90 p-2 shadow-[0_10px_24px_rgba(15,23,42,0.12)]">
|
||||
<PromptInputModelSelectContent className="!w-[min(430px,calc(100vw-24px))] max-h-[426px] overflow-hidden rounded-[22px] border border-[#bfc6cf] bg-[#f2f3f5]/90 p-2 shadow-[0_10px_24px_rgba(15,23,42,0.12)]">
|
||||
<div className="max-h-[392px] space-y-1 overflow-y-auto pr-1">
|
||||
{displayModels.map((model) => (
|
||||
<SelectItem
|
||||
@ -1884,8 +1884,8 @@ function PureModelSelectorCompact({
|
||||
model.selectable === false && "opacity-55"
|
||||
)}
|
||||
>
|
||||
<span className="flex w-full items-center justify-between gap-3">
|
||||
<span className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<span className="flex w-full items-center justify-between gap-2">
|
||||
<span className="flex min-w-0 flex-1 items-center gap-3 overflow-visible">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-[#2f3640]">
|
||||
{model.avatarUrl ? (
|
||||
<img
|
||||
@ -1910,7 +1910,7 @@ function PureModelSelectorCompact({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="min-w-0 whitespace-nowrap text-[17px] font-medium leading-none tracking-[-0.01em]">
|
||||
<span className="min-w-max whitespace-nowrap text-[17px] font-medium leading-none tracking-[-0.01em]">
|
||||
{model.isSubModel ? `└ ${model.displayName}` : model.displayName}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user