refactor(core): 模型路由与画布 - 收敛渠道模型映射并优化交互
This commit is contained in:
@@ -22,7 +22,6 @@ type CanvasNodeProps = {
|
||||
isRelated: boolean;
|
||||
isFocusRelated: boolean;
|
||||
isConnectionTarget: boolean;
|
||||
isConnecting: boolean;
|
||||
forceInputVisible?: boolean;
|
||||
showImageInfo: boolean;
|
||||
reduceMediaEffects?: boolean;
|
||||
@@ -364,9 +363,9 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasReplaceAction ? (
|
||||
{hasReplaceAction && (!hasMediaContent || isSelected || hovered) ? (
|
||||
<div
|
||||
className={`absolute right-3 top-3 z-[var(--node-z-overlay)] motion-safe:transition motion-safe:duration-200 ${!hasMediaContent || isSelected || hovered ? "opacity-100" : "pointer-events-none opacity-0"}`}
|
||||
className="absolute right-3 top-3 z-[var(--node-z-overlay)]"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
@@ -436,7 +435,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!readOnly && !data.metadata?.locked ? <>
|
||||
{!readOnly && !data.metadata?.locked && (isSelected || hovered) ? <>
|
||||
<ResizeHandle corner="top-left" onMouseDown={handleResizeMouseDown} />
|
||||
<ResizeHandle corner="top-right" onMouseDown={handleResizeMouseDown} />
|
||||
<ResizeHandle corner="bottom-left" onMouseDown={handleResizeMouseDown} />
|
||||
@@ -444,12 +443,61 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
</> : null}
|
||||
</div>
|
||||
|
||||
{!readOnly && data.type !== CanvasNodeType.Script ? <ConnectionSideRail side="left" scale={scale} visible={hovered || forceInputVisible} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
|
||||
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config ? <ConnectionSideRail side="right" scale={scale} visible={hovered} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
|
||||
{!readOnly && data.type !== CanvasNodeType.Script && (hovered || forceInputVisible) ? <ConnectionSideRail side="left" scale={scale} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "target", undefined, anchorRatio)} /> : null}
|
||||
{!readOnly && data.type !== CanvasNodeType.Script && data.type !== CanvasNodeType.Config && hovered ? <ConnectionSideRail side="right" scale={scale} theme={theme} onPointerDown={(event, anchorRatio) => onConnectStart(event, data.id, "source", undefined, anchorRatio)} /> : null}
|
||||
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}, areCanvasNodePropsEqual);
|
||||
|
||||
function areCanvasNodePropsEqual(previous: CanvasNodeProps, next: CanvasNodeProps) {
|
||||
return (
|
||||
previous.data === next.data &&
|
||||
previous.dragOffset?.x === next.dragOffset?.x &&
|
||||
previous.dragOffset?.y === next.dragOffset?.y &&
|
||||
previous.scale === next.scale &&
|
||||
previous.isSelected === next.isSelected &&
|
||||
previous.isRelated === next.isRelated &&
|
||||
previous.isFocusRelated === next.isFocusRelated &&
|
||||
previous.isConnectionTarget === next.isConnectionTarget &&
|
||||
previous.forceInputVisible === next.forceInputVisible &&
|
||||
previous.showImageInfo === next.showImageInfo &&
|
||||
previous.reduceMediaEffects === next.reduceMediaEffects &&
|
||||
previous.readOnly === next.readOnly &&
|
||||
previous.resourceLabel === next.resourceLabel &&
|
||||
previous.mentionReferences === next.mentionReferences &&
|
||||
previous.renderNodeContent === next.renderNodeContent &&
|
||||
previous.drawingProjectId === next.drawingProjectId &&
|
||||
previous.batchCount === next.batchCount &&
|
||||
previous.batchExpanded === next.batchExpanded &&
|
||||
previous.batchClosing === next.batchClosing &&
|
||||
previous.batchOpening === next.batchOpening &&
|
||||
previous.batchRecovering === next.batchRecovering &&
|
||||
previous.batchPrimary === next.batchPrimary &&
|
||||
previous.batchMotion?.x === next.batchMotion?.x &&
|
||||
previous.batchMotion?.y === next.batchMotion?.y &&
|
||||
previous.batchMotion?.index === next.batchMotion?.index &&
|
||||
previous.onMouseDown === next.onMouseDown &&
|
||||
previous.onHoverStart === next.onHoverStart &&
|
||||
previous.onHoverEnd === next.onHoverEnd &&
|
||||
previous.onConnectStart === next.onConnectStart &&
|
||||
previous.onResize === next.onResize &&
|
||||
previous.onTitleChange === next.onTitleChange &&
|
||||
previous.onContentChange === next.onContentChange &&
|
||||
previous.onToggleBatch === next.onToggleBatch &&
|
||||
previous.onSetBatchPrimary === next.onSetBatchPrimary &&
|
||||
previous.onRetry === next.onRetry &&
|
||||
previous.onCancelTask === next.onCancelTask &&
|
||||
previous.onOpenTaskDetails === next.onOpenTaskDetails &&
|
||||
previous.onOpenVersions === next.onOpenVersions &&
|
||||
previous.onViewImage === next.onViewImage &&
|
||||
previous.onReplaceMedia === next.onReplaceMedia &&
|
||||
previous.onOpenTextEditor === next.onOpenTextEditor &&
|
||||
previous.onOpenDirector === next.onOpenDirector &&
|
||||
previous.onOpenDrawing === next.onOpenDrawing &&
|
||||
previous.onContextMenu === next.onContextMenu
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceLabelBadge({ reference, theme }: { reference: CanvasResourceReference; theme: CanvasTheme }) {
|
||||
return (
|
||||
@@ -624,7 +672,7 @@ function NodeStatusBadge({ status }: { status: "loading" | "success" | "error" }
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { side: "left" | "right"; scale: number; visible: boolean; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) {
|
||||
function ConnectionSideRail({ side, scale, theme, onPointerDown }: { side: "left" | "right"; scale: number; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) {
|
||||
const handleRef = useRef<HTMLSpanElement>(null);
|
||||
const anchorRatioRef = useRef(0.5);
|
||||
const inverseScale = 1 / Math.max(scale, 0.05);
|
||||
@@ -634,10 +682,6 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si
|
||||
if (handleRef.current) handleRef.current.style.top = "50%";
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) resetAnchor();
|
||||
}, [resetAnchor, visible]);
|
||||
|
||||
const updateAnchor = (event: React.PointerEvent<HTMLButtonElement>) => {
|
||||
const railBounds = event.currentTarget.getBoundingClientRect();
|
||||
const nodeBounds = event.currentTarget.parentElement?.getBoundingClientRect() || railBounds;
|
||||
@@ -651,7 +695,7 @@ function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { si
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`group absolute top-1/2 z-[var(--node-z-overlay)] touch-none -translate-y-1/2 outline-none transition-opacity duration-150 ${visible ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`}
|
||||
className="group pointer-events-auto absolute top-1/2 z-[var(--node-z-overlay)] touch-none -translate-y-1/2 opacity-100 outline-none transition-opacity duration-150"
|
||||
style={{ width: 56 * inverseScale, height: `min(100%, ${72 * inverseScale}px)`, ...(side === "left" ? { right: "100%" } : { left: "100%" }) }}
|
||||
onPointerEnter={updateAnchor}
|
||||
onPointerMove={updateAnchor}
|
||||
|
||||
@@ -65,11 +65,12 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
onPointerDown={() => preloadWorkspaceRoute(tool.slug)}
|
||||
onPointerEnter={() => preloadWorkspaceRoute(tool.slug)}
|
||||
className={cn(
|
||||
"app-workspace-nav-link app-workspace-rail-tile grid size-10 place-items-center rounded-md text-[var(--fs-tiny)] transition-colors",
|
||||
"app-workspace-nav-link app-workspace-rail-tile flex min-h-12 w-full flex-col items-center justify-center gap-1 rounded-md px-1 py-1 text-[var(--fs-tiny)] leading-none transition-colors",
|
||||
active ? "is-active font-medium" : "",
|
||||
)}
|
||||
>
|
||||
<Icon className="app-workspace-nav-icon shrink-0" strokeWidth={1.8} />
|
||||
<span className="truncate">{tool.label.slice(0, 2)}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -121,8 +122,9 @@ export function AppWorkspaceShell({ children }: { children: ReactNode }) {
|
||||
{/* 桌面:64px 电影胶片条式轨道 */}
|
||||
<div className="app-workspace-rail hidden min-h-0 flex-col overflow-y-auto lg:flex">
|
||||
<div className="app-workspace-rail-header flex h-16 shrink-0 items-center justify-center">
|
||||
<Link to="/home" className="grid size-10 shrink-0 place-items-center rounded-md text-foreground/55 transition-colors hover:bg-surface-hover hover:text-foreground" title="工作台首页">
|
||||
<Link to="/home" className="app-workspace-rail-tile flex min-h-12 w-10 shrink-0 flex-col items-center justify-center gap-1 rounded-md px-1 py-1 text-[var(--fs-tiny)] leading-none text-foreground/55 transition-colors hover:bg-surface-hover hover:text-foreground" title="工作台首页">
|
||||
<Home className="app-workspace-nav-icon" strokeWidth={1.8} />
|
||||
<span>首页</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="app-workspace-rail-middle flex min-h-0 w-full flex-1 flex-col items-center justify-start overflow-y-auto px-3 pt-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Input, InputNumber, Segmented, Select, Switch } from "antd";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { defaultImageCapabilityConfig, defaultModelCapabilityConfig, normalizeModelCapabilityConfig, type ImageCapabilityConfig, type ModelCapabilityConfig, type TextCapabilityConfig, type VideoCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import type { ModelProtocol } from "@/lib/model-protocols";
|
||||
import { VIDEO_RESOLUTION_CAPABILITY_OPTIONS } from "@/lib/video-generation-options";
|
||||
|
||||
@@ -33,7 +33,7 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability =
|
||||
if (capability === "image") {
|
||||
return <ImageCapabilityEditor value={value} onChange={onChange} protocol={protocol} model={model} disabled={disabled} />;
|
||||
}
|
||||
const profile = value?.video || defaultModelCapabilityConfig(protocol).video!;
|
||||
const profile = normalizeModelCapabilityConfig(value || defaultModelCapabilityConfig(protocol)).video!;
|
||||
const update = (patch: Partial<VideoCapabilityConfig>) => onChange?.({ version: 1, video: { ...profile, ...patch } });
|
||||
const updateReferences = (patch: Partial<VideoCapabilityConfig["references"]>) => update({ references: { ...profile.references, ...patch } });
|
||||
const updateDuration = (patch: Partial<VideoCapabilityConfig["duration"]>) => update({ duration: { ...profile.duration, ...patch } });
|
||||
@@ -125,7 +125,7 @@ function TextCapabilityEditor({ value, onChange, protocol, disabled }: Pick<Prop
|
||||
}
|
||||
|
||||
function ImageCapabilityEditor({ value, onChange, protocol, model, disabled }: Required<Pick<Props, "model" | "disabled">> & Pick<Props, "value" | "onChange" | "protocol">) {
|
||||
const profile = value?.image || defaultImageCapabilityConfig(protocol, model);
|
||||
const profile = normalizeModelCapabilityConfig(value || { version: 1, image: defaultImageCapabilityConfig(protocol, model) }).image!;
|
||||
const update = (patch: Partial<ImageCapabilityConfig>) => onChange?.({ version: 1, image: { ...profile, ...patch } });
|
||||
const updateReferences = (patch: Partial<ImageCapabilityConfig["references"]>) => update({ references: { ...profile.references, ...patch } });
|
||||
const updateSize = (patch: Partial<ImageCapabilityConfig["size"]>) => update({ size: { ...profile.size, ...patch } });
|
||||
|
||||
Reference in New Issue
Block a user