refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
||||
} from "@/lib/canvas/camera-prompt-library";
|
||||
|
||||
const defaultCameraControl: CameraControlOptions = {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
camera: "arri_alexa_mini_lf",
|
||||
lens: "arri_signature_prime",
|
||||
focalLength: 50,
|
||||
@@ -232,7 +232,7 @@ interface CardColumnProps {
|
||||
|
||||
const CardColumn = memo(({ label, tooltipTitle, tooltipDesc, tooltipUseCase, visual, cornerBadge, captionBelow, onPrev, onNext, theme }: CardColumnProps) => {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center gap-1.5">
|
||||
<div className="flex min-w-0 flex-col items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onPrev}
|
||||
@@ -244,7 +244,7 @@ const CardColumn = memo(({ label, tooltipTitle, tooltipDesc, tooltipUseCase, vis
|
||||
</button>
|
||||
<HoverTip title={tooltipTitle} description={tooltipDesc} useCase={tooltipUseCase}>
|
||||
<div
|
||||
className="relative flex h-[140px] w-[140px] cursor-help flex-col items-center justify-between rounded-xl border px-3 pt-2 pb-2 transition-colors"
|
||||
className="relative flex h-[clamp(116px,14vw,140px)] w-full min-w-0 cursor-help flex-col items-center justify-between rounded-xl border px-2.5 pt-2 pb-2 transition-colors"
|
||||
style={{ borderColor: theme.toolbar.border, background: theme.toolbar.itemHover }}
|
||||
>
|
||||
<span className="text-[11px] font-medium tracking-wide" style={{ color: theme.node.muted }}>{label}</span>
|
||||
@@ -286,7 +286,7 @@ export function CanvasNodeCameraPanel({
|
||||
const reducedMotion = useReducedMotion();
|
||||
const copyText = useCopyText();
|
||||
|
||||
const [enabled, setEnabled] = useState(cameraControl?.enabled ?? true);
|
||||
const [enabled, setEnabled] = useState(cameraControl?.enabled ?? defaultCameraControl.enabled);
|
||||
const [cameraIdx, setCameraIdx] = useState(() => {
|
||||
const i = CAMERA_PROFILES.findIndex((c) => c.id === cameraControl?.camera);
|
||||
return i >= 0 ? i : 0;
|
||||
@@ -350,16 +350,30 @@ export function CanvasNodeCameraPanel({
|
||||
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.97 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel}
|
||||
className="w-full overflow-hidden rounded-[var(--r-2xl)] border backdrop-blur-2xl"
|
||||
className="w-full overflow-hidden rounded-[var(--r-xl)] border backdrop-blur-2xl"
|
||||
style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
{/* 标题区由外层 AppModal 统一提供,面板只负责镜头参数。 */}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 border-b px-4 py-3" style={{ borderColor: theme.toolbar.border }}>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold tracking-tight">摄像机控制</div>
|
||||
<div className="mt-0.5 truncate text-[11px]" style={{ color: theme.node.muted }}>为当前图片生成设置镜头、焦距与光圈</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-7 shrink-0 place-items-center rounded-md transition-colors hover:bg-white/10"
|
||||
style={{ color: theme.node.muted }}
|
||||
onClick={onClose}
|
||||
aria-label="关闭摄像机控制"
|
||||
title="关闭"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 相机、镜头、焦距和光圈均来自已注册枚举,确认时由纯函数再次强校验。 */}
|
||||
<div className="flex justify-center gap-3 px-5 py-5">
|
||||
<div className="grid grid-cols-2 gap-2.5 px-4 py-4 sm:grid-cols-4">
|
||||
<CardColumn
|
||||
label="相机"
|
||||
tooltipTitle={`${currentCamera.zhName} · ${currentCamera.label}`}
|
||||
@@ -421,13 +435,13 @@ export function CanvasNodeCameraPanel({
|
||||
</div>
|
||||
|
||||
{/* 当前有效配置摘要。 */}
|
||||
<div className="mx-5 mb-3 rounded-lg border px-3.5 py-2.5" style={{ borderColor: theme.toolbar.border, background: theme.toolbar.itemHover }}>
|
||||
<div className="mx-4 mb-3 rounded-lg border px-3.5 py-2.5" style={{ borderColor: theme.toolbar.border, background: theme.toolbar.itemHover }}>
|
||||
<div className="text-[10px] uppercase tracking-wide" style={{ color: theme.node.faint }}>当前配置</div>
|
||||
<div className="mt-0.5 text-xs">{summary}</div>
|
||||
</div>
|
||||
|
||||
{/* 操作区:重置与复制不产生写入,只有应用会提交生成配置。 */}
|
||||
<div className="flex h-12 items-center gap-2 border-t px-5" style={{ borderColor: theme.toolbar.border }}>
|
||||
<div className="flex min-h-12 flex-wrap items-center gap-2 border-t px-4 py-2" style={{ borderColor: theme.toolbar.border }}>
|
||||
<button type="button" className={secondaryButtonClass} style={{ color: theme.node.muted }} onClick={handleReset}>
|
||||
<RotateCcw className="size-3.5" />重置参数
|
||||
</button>
|
||||
@@ -445,18 +459,24 @@ export function CanvasNodeCameraPanel({
|
||||
role="switch"
|
||||
aria-checked={enabled}
|
||||
onClick={() => setEnabled((v) => !v)}
|
||||
className="relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full transition-colors"
|
||||
style={{ background: enabled ? theme.node.activeStroke : theme.toolbar.itemHover }}
|
||||
className="canvas-node-camera-switch relative inline-flex h-5 w-9 cursor-pointer items-center rounded-full border transition-colors"
|
||||
style={{
|
||||
background: enabled ? `${theme.node.activeStroke}22` : theme.toolbar.itemHover,
|
||||
borderColor: enabled ? theme.node.activeStroke : theme.toolbar.border,
|
||||
}}
|
||||
>
|
||||
<span className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${enabled ? "translate-x-[22px]" : "translate-x-0.5"}`} />
|
||||
<span
|
||||
className={`inline-block size-3.5 transform rounded-full shadow-sm transition-transform ${enabled ? "translate-x-[18px]" : "translate-x-0.5"}`}
|
||||
style={{ background: enabled ? theme.node.activeStroke : theme.node.muted }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<motion.button
|
||||
type="button"
|
||||
whileHover={reducedMotion ? undefined : { y: -1 }}
|
||||
whileTap={reducedMotion ? undefined : { scale: 0.97 }}
|
||||
className="flex h-8 items-center gap-1.5 rounded-[var(--dock-item-radius)] px-4 text-[var(--fs-label)] font-semibold"
|
||||
style={{ background: theme.node.activeStroke, color: theme.node.panel }}
|
||||
className="canvas-node-camera-apply flex h-7 items-center gap-1.5 rounded-[var(--dock-item-radius)] border px-3.5 text-[var(--fs-label)] font-semibold transition-colors"
|
||||
style={{ borderColor: theme.node.activeStroke, color: theme.node.activeStroke, background: "transparent" }}
|
||||
onClick={handleApply}
|
||||
>
|
||||
<CameraIcon className="size-3.5" />应用
|
||||
|
||||
@@ -14,7 +14,6 @@ import { modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefa
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { AppModal } from "@/components/ui/product/app-modal/app-modal";
|
||||
import type { CameraControlOptions } from "@/lib/canvas/camera-prompt-library";
|
||||
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
|
||||
import { CanvasNodeCameraPanel } from "./canvas-node-camera-dialog";
|
||||
@@ -54,7 +53,8 @@ type CanvasNodePromptPanelProps = {
|
||||
type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes];
|
||||
|
||||
const PROMPT_REFERENCE_SHELF_HEIGHT = 58;
|
||||
const PROMPT_EDITOR_MIN_HEIGHT = 44;
|
||||
// Keep the compact editor readable at rest: three 20px lines plus 12px vertical padding.
|
||||
const PROMPT_EDITOR_MIN_HEIGHT = 72;
|
||||
const PROMPT_EDITOR_EXPANDED_MIN_HEIGHT = 76;
|
||||
const PROMPT_EDITOR_LINE_HEIGHT = 20;
|
||||
const PROMPT_EDITOR_EXPANDED_LINE_HEIGHT = 24;
|
||||
@@ -83,6 +83,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const [manualPromptHeight, setManualPromptHeight] = useState<number | null>(null);
|
||||
const [manualExpandedPromptHeight, setManualExpandedPromptHeight] = useState<number | null>(null);
|
||||
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
|
||||
const [cameraPanelOpen, setCameraPanelOpen] = useState(false);
|
||||
const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false);
|
||||
const [autoLinkEnabled, setAutoLinkEnabled] = useState(true);
|
||||
const resolvedMentionReferences = useResolvedCanvasResourceReferences(mentionReferences, { projectId });
|
||||
@@ -180,6 +181,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true));
|
||||
setManualPromptHeight(null);
|
||||
setManualExpandedPromptHeight(null);
|
||||
setCameraPanelOpen(false);
|
||||
}, [node.id]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -401,9 +403,10 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
<>
|
||||
<CameraToolsPopover
|
||||
cameraControl={node.metadata?.cameraControl}
|
||||
onCameraControlChange={(options) => onConfigChange(node.id, { cameraControl: options })}
|
||||
theme={theme}
|
||||
compact={!expanded}
|
||||
open={cameraPanelOpen}
|
||||
onOpenChange={setCameraPanelOpen}
|
||||
/>
|
||||
<CanvasImageSettingsPopover
|
||||
config={config}
|
||||
@@ -471,6 +474,19 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
);
|
||||
};
|
||||
|
||||
const renderCameraPanel = () => (
|
||||
<div className="canvas-node-composer-camera-panel">
|
||||
<CanvasNodeCameraPanel
|
||||
cameraControl={node.metadata?.cameraControl}
|
||||
onClose={() => setCameraPanelOpen(false)}
|
||||
onConfirm={(options) => {
|
||||
onConfigChange(node.id, { cameraControl: options });
|
||||
setCameraPanelOpen(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<CanvasPromptOptimizerDrawer
|
||||
open={promptOptimizerOpen}
|
||||
@@ -486,7 +502,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
onApply={(nextPrompt) => updatePrompt(nextPrompt)}
|
||||
>
|
||||
<div
|
||||
className="canvas-node-composer"
|
||||
className={`canvas-node-composer${cameraPanelOpen ? " has-camera-panel" : ""}`}
|
||||
style={composerSurfaceStyle}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
@@ -495,6 +511,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
{renderComposerHeader(false)}
|
||||
|
||||
{renderPromptEditor(false)}
|
||||
{cameraPanelOpen ? renderCameraPanel() : null}
|
||||
|
||||
{/* B区 参数区(对应 #98 决策2:默认折叠,手风琴展开)*/}
|
||||
{hasVideoPromptTools ? (
|
||||
@@ -538,9 +555,10 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
body: { minHeight: 0, padding: 0 },
|
||||
}}
|
||||
>
|
||||
<div className="flex min-h-0 flex-col gap-2.5 p-3" style={{ ...composerTokens, color: theme.node.text }}>
|
||||
<div className="relative flex min-h-0 flex-col gap-2.5 overflow-visible p-3" style={{ ...composerTokens, color: theme.node.text }}>
|
||||
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
|
||||
{renderPromptEditor(true)}
|
||||
{cameraPanelOpen ? renderCameraPanel() : null}
|
||||
{hasVideoPromptTools ? (
|
||||
<div className="canvas-node-composer-parameters shrink-0">
|
||||
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
|
||||
@@ -555,39 +573,26 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
);
|
||||
}
|
||||
|
||||
function CameraToolsPopover({ cameraControl, onCameraControlChange, theme, compact }: { cameraControl?: CameraControlOptions; onCameraControlChange: (options: CameraControlOptions) => void; theme: CanvasTheme; compact: boolean }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
function CameraToolsPopover({ cameraControl, theme, compact, open, onOpenChange }: { cameraControl?: CameraControlOptions; theme: CanvasTheme; compact: boolean; open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const cameraEnabled = cameraControl?.enabled === true;
|
||||
const active = open || cameraEnabled;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={`canvas-node-composer-camera-tools-trigger inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 text-[var(--fs-tiny)] transition-colors ${compact ? "is-compact h-7" : "h-8"}`}
|
||||
style={{
|
||||
background: cameraEnabled ? `${theme.node.activeStroke}66` : theme.node.fill,
|
||||
color: cameraEnabled ? theme.node.panel : theme.node.text,
|
||||
}}
|
||||
aria-pressed={cameraEnabled}
|
||||
aria-label="摄像机控制"
|
||||
title={`摄像机控制${cameraEnabled ? " · 已启用" : ""}`}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Camera className="size-4.5" />
|
||||
{!compact ? <span>摄像机</span> : null}
|
||||
</button>
|
||||
{open ? (
|
||||
<AppModal title="摄像机控制" open centered footer={null} width={780} flush onCancel={() => setOpen(false)}>
|
||||
<CanvasNodeCameraPanel
|
||||
cameraControl={cameraControl}
|
||||
onClose={() => setOpen(false)}
|
||||
onConfirm={(options) => {
|
||||
onCameraControlChange(options);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</AppModal>
|
||||
) : null}
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
className={`canvas-node-composer-camera-tools-trigger inline-flex shrink-0 items-center gap-1 rounded-[var(--r-md)] border px-1.5 text-[var(--fs-tiny)] transition-colors focus:outline-none focus-visible:outline-none ${compact ? "is-compact h-7" : "h-8"}`}
|
||||
style={{
|
||||
background: active ? `${theme.node.activeStroke}14` : theme.node.fill,
|
||||
borderColor: active ? theme.node.activeStroke : "transparent",
|
||||
color: active ? theme.node.activeStroke : theme.node.text,
|
||||
}}
|
||||
aria-pressed={active}
|
||||
aria-label="摄像机控制"
|
||||
title={`摄像机控制${cameraEnabled ? " · 已启用" : ""}`}
|
||||
onClick={() => onOpenChange(!open)}
|
||||
>
|
||||
<Camera className="size-4.5" />
|
||||
{!compact ? <span>摄像机</span> : null}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -599,30 +604,31 @@ function ReferenceToolsPopover({ canAutoMention, autoLinkEnabled, onAutoMention,
|
||||
rootClassName="canvas-reference-tools-popover"
|
||||
arrow={false}
|
||||
align={{ offset: [0, -8] }}
|
||||
styles={{ root: { width: "min(280px, calc(100vw - 24px))" }, container: { width: "100%" }, content: { width: "100%", padding: 12 } }}
|
||||
styles={{ root: { width: "min(280px, calc(100vw - 24px))" }, container: { width: "100%" }, content: { width: "100%", padding: 10 } }}
|
||||
content={
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<div>
|
||||
<div className="text-sm font-medium">智能引用</div>
|
||||
<div className="mt-1 text-xs text-black/50 dark:text-white/50">输入素材序号或名称后按 Tab,可快速引用</div>
|
||||
<div className="text-sm font-medium leading-5">智能引用</div>
|
||||
<div className="mt-0.5 text-xs leading-4 text-black/50 dark:text-white/50">输入素材序号或名称后按 Tab,可快速引用</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-h-6 items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-sm"><Link2 className="size-3.5" />AutoLink</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={autoLinkEnabled}
|
||||
aria-label={autoLinkEnabled ? "关闭 AutoLink" : "开启 AutoLink"}
|
||||
className={`relative h-5 w-9 rounded-full transition-colors ${autoLinkEnabled ? "" : "bg-black/20 dark:bg-white/20"}`}
|
||||
style={autoLinkEnabled ? { background: accent } : undefined}
|
||||
className="canvas-reference-autolink-switch relative inline-flex h-5 w-9 items-center rounded-full border transition-colors"
|
||||
style={{ background: autoLinkEnabled ? `${accent}14` : "transparent", borderColor: autoLinkEnabled ? accent : "color-mix(in srgb, currentColor 22%, transparent)", color: accent }}
|
||||
onClick={() => onAutoLinkEnabledChange(!autoLinkEnabled)}
|
||||
>
|
||||
<span className={`absolute left-0.5 top-0.5 size-4 rounded-full bg-white shadow-sm transition-transform ${autoLinkEnabled ? "translate-x-4" : "translate-x-0"}`} />
|
||||
<span className={`size-3.5 rounded-full shadow-sm transition-transform ${autoLinkEnabled ? "translate-x-[18px]" : "translate-x-0.5"}`} style={{ background: autoLinkEnabled ? accent : "currentColor" }} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-md bg-[var(--canvas-composer-control-surface)] px-2 py-1.5 text-sm transition-colors hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="canvas-reference-tools-mention-button flex h-7 w-full items-center justify-center gap-1.5 rounded-md border px-2.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-45"
|
||||
style={{ borderColor: accent, color: accent, background: "transparent" }}
|
||||
disabled={!canAutoMention}
|
||||
onClick={onAutoMention}
|
||||
>
|
||||
|
||||
@@ -352,7 +352,7 @@ export const CanvasNode = React.memo(function CanvasNode({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`relative flex h-full w-full items-center justify-center rounded-[inherit] ${isBatchRoot || data.type === CanvasNodeType.Script ? "overflow-visible" : "overflow-hidden"}`}
|
||||
className={`relative flex h-full w-full items-center justify-center rounded-[inherit] ${isBatchRoot || data.type === CanvasNodeType.Script || isComposerNode ? "overflow-visible" : "overflow-hidden"}`}
|
||||
style={
|
||||
{
|
||||
background: hasImageContent || hasVideoContent || hasAudioContent ? "transparent" : theme.node.fill,
|
||||
|
||||
@@ -87,7 +87,7 @@ export function CanvasSelectionToolbar({ anchorRef, containerRef, count, childre
|
||||
);
|
||||
}
|
||||
|
||||
export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidth, panelHeight = 190, dragOffset, isDragging = false, children }: { node: CanvasNodeData; viewport: ViewportTransform; containerRef: RefObject<HTMLDivElement | null>; panelWidth?: number; panelHeight?: number; dragOffset?: Position | null; isDragging?: boolean; children: ReactNode }) {
|
||||
export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidth, panelHeight = 190, dragOffset, isDragging = false, allowOverflow = false, children }: { node: CanvasNodeData; viewport: ViewportTransform; containerRef: RefObject<HTMLDivElement | null>; panelWidth?: number; panelHeight?: number; dragOffset?: Position | null; isDragging?: boolean; allowOverflow?: boolean; children: ReactNode }) {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const { bringToFront, zIndex } = useCanvasOverlayLayer(`node-panel:${node.id}`, "var(--z-modal-overlay)");
|
||||
const initialWidth = resolveNodePanelWidth(node, viewport, panelWidth);
|
||||
@@ -135,8 +135,8 @@ export function CanvasNodePanelOverlay({ node, viewport, containerRef, panelWidt
|
||||
ref={panelRef}
|
||||
data-canvas-no-zoom
|
||||
data-canvas-node-panel
|
||||
className="thin-scrollbar absolute max-w-[calc(100%_-_24px)] overflow-y-auto"
|
||||
style={{ left: 0, top: 0, transform: `translate3d(${initialPosition.left}px, ${initialPosition.top}px, 0)`, width: initialWidth, maxHeight: "calc(100% - 84px)", zIndex }}
|
||||
className={`thin-scrollbar absolute max-w-[calc(100%_-_24px)] ${allowOverflow ? "overflow-visible" : "overflow-y-auto"}`}
|
||||
style={{ left: 0, top: 0, transform: `translate3d(${initialPosition.left}px, ${initialPosition.top}px, 0)`, width: initialWidth, maxHeight: allowOverflow ? "none" : "calc(100% - 84px)", zIndex }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDownCapture={bringToFront}
|
||||
onFocusCapture={bringToFront}
|
||||
|
||||
@@ -60,7 +60,11 @@ export function useResolvedCanvasResourceReferences(references: CanvasResourceRe
|
||||
}
|
||||
|
||||
function previewIdentity(reference: CanvasResourceReference, projectId?: string) {
|
||||
if (reference.drawingId && projectId) return `drawing:${projectId}:${reference.drawingId}:${reference.drawingRevision || 0}`;
|
||||
// 只有当 drawingId 和 projectId 同时存在时才返回绘图身份,避免不完整的缓存键导致运行时错误
|
||||
if (reference.drawingId) {
|
||||
if (!projectId) return "";
|
||||
return `drawing:${projectId}:${reference.drawingId}:${reference.drawingRevision || 0}`;
|
||||
}
|
||||
const storageKey = reference.kind === "video" ? reference.previewStorageKey : reference.storageKey;
|
||||
if (!storageKey || !["image", "video", "character"].includes(reference.kind)) return "";
|
||||
return `${reference.kind}:${storageKey}`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import "./image-size-picker.css";
|
||||
import { Input, Button } from "antd";
|
||||
import type { ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { IMAGE_RESOLUTIONS, imagePresetForRatio, imagePresetValue, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets";
|
||||
import { IMAGE_RESOLUTIONS, imagePresetForRatio, imagePresetValue, imageQualityForSelection, imageQualityForTier, imageResolutionUsesQuality, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets";
|
||||
import { buildImageResolutionOptions, type ImageResolutionOption, type ImageResolutionTier } from "@/lib/image-resolution-tiers";
|
||||
|
||||
import { resolveImageRequestSize, validateImageSize } from "@/services/api/image-validation";
|
||||
@@ -14,8 +14,13 @@ export function ImageSizePicker({ profile, size, quality, onChange }: { profile:
|
||||
const [customPreset, setCustomPreset] = useState<ImageResolutionOption>();
|
||||
const [custom, setCustom] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const matches = (preset: ImageResolutionOption) =>
|
||||
imagePresetValue(profile, preset) === size && (profile.size.parameter !== "aspect_ratio" || !imageResolutionUsesQuality(profile) || imageQualityForTier(profile, preset.tier) === (quality || profile.quality.default));
|
||||
const qualityTier = IMAGE_RESOLUTIONS.find((value) => value === (quality || "").toLowerCase());
|
||||
const matches = (preset: ImageResolutionOption) => {
|
||||
if (imagePresetValue(profile, preset) !== size) return false;
|
||||
if (profile.size.parameter !== "aspect_ratio") return true;
|
||||
if (imageResolutionUsesQuality(profile)) return imageQualityForTier(profile, preset.tier) === (quality || profile.quality.default);
|
||||
return !qualityTier || qualityTier === preset.tier;
|
||||
};
|
||||
let resolvedLegacySize: string | undefined;
|
||||
if (profile.size.parameter === "size" && size.includes(":")) {
|
||||
try {
|
||||
@@ -29,7 +34,9 @@ export function ImageSizePicker({ profile, size, quality, onChange }: { profile:
|
||||
try {
|
||||
if (profile.size.parameter === "size") restoredCustom = buildImageResolutionOptions([resolvedLegacySize || size])[0];
|
||||
else if (profile.size.parameter === "aspect_ratio") {
|
||||
const restoredTier = IMAGE_RESOLUTIONS.find((value) => imageQualityForTier(profile, value) === (quality || profile.quality.default)) || "1k";
|
||||
const restoredTier = imageResolutionUsesQuality(profile)
|
||||
? IMAGE_RESOLUTIONS.find((value) => imageQualityForTier(profile, value) === (quality || profile.quality.default)) || "1k"
|
||||
: (qualityTier && visibleTiers.includes(qualityTier) ? qualityTier : visibleTiers[0]) || "1k";
|
||||
restoredCustom = imagePresetForRatio(restoredTier, size);
|
||||
}
|
||||
} catch {
|
||||
@@ -44,7 +51,8 @@ export function ImageSizePicker({ profile, size, quality, onChange }: { profile:
|
||||
const available = (value: ImageResolutionTier) => imageTierAvailable(profile, value) && (profile.size.allowCustom || presets.some((preset) => preset.tier === value));
|
||||
const tier =
|
||||
(active && visibleTiers.includes(active.tier) ? active.tier : undefined) ||
|
||||
(profile.size.parameter === "aspect_ratio" ? visibleTiers.find((value) => imageQualityForTier(profile, value) === (quality || profile.quality.default)) : undefined) ||
|
||||
(profile.size.parameter === "aspect_ratio" && imageResolutionUsesQuality(profile) ? visibleTiers.find((value) => imageQualityForTier(profile, value) === (quality || profile.quality.default)) : undefined) ||
|
||||
(qualityTier && visibleTiers.includes(qualityTier) ? qualityTier : undefined) ||
|
||||
(visibleTiers.includes(chosenTier) ? chosenTier : visibleTiers[0]) ||
|
||||
"1k";
|
||||
const choices = presets.filter((preset) => preset.tier === tier);
|
||||
@@ -53,7 +61,7 @@ export function ImageSizePicker({ profile, size, quality, onChange }: { profile:
|
||||
setChosenTier(preset.tier);
|
||||
setCustomPreset(preset);
|
||||
setError("");
|
||||
onChange(imagePresetValue(profile, preset), profile.size.parameter === "aspect_ratio" ? imageQualityForTier(profile, preset.tier) : undefined);
|
||||
onChange(imagePresetValue(profile, preset), profile.size.parameter === "aspect_ratio" ? imageQualityForSelection(profile, preset.tier) : undefined);
|
||||
};
|
||||
const makeCustom = (next: ImageResolutionTier, ratio: string) => {
|
||||
const preset = imagePresetForRatio(next, ratio);
|
||||
@@ -154,7 +162,7 @@ export function ImageSizePicker({ profile, size, quality, onChange }: { profile:
|
||||
<output aria-live="polite" className="image-size-output">
|
||||
{size === "auto" ? "尺寸由模型决定" : active ? `${active.ratio} · ${active.width} × ${active.height} px` : size.replace("x", " × ")}
|
||||
</output>
|
||||
{profile.size.parameter === "aspect_ratio" ? <p className="image-size-hint">尺寸为换算参考,实际像素由模型决定{!imageResolutionUsesQuality(profile) ? ";当前协议未配置独立分辨率" : ""}。</p> : null}
|
||||
{profile.size.parameter === "aspect_ratio" ? <p className="image-size-hint">尺寸为换算参考,实际像素由模型决定{!imageResolutionUsesQuality(profile) && visibleTiers.length <= 1 ? ";当前协议未配置独立分辨率" : ""}。</p> : null}
|
||||
{profile.size.allowCustom ? (
|
||||
<details className="image-size-custom">
|
||||
<summary>自定义比例或尺寸</summary>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Check, Plus } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import "./image-size-picker.css";
|
||||
import type { ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets";
|
||||
import { IMAGE_RATIOS, IMAGE_RESOLUTIONS, imagePresetForRatio, imageResolutionUsesQuality, imageSizeConfigWithPresets, imageSizePresets, imageTierAvailable } from "@/lib/image-size-presets";
|
||||
import type { ImageResolutionTier } from "@/lib/image-resolution-tiers";
|
||||
|
||||
export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profile: ImageCapabilityConfig; disabled?: boolean; onChange: (size: ImageCapabilityConfig["size"]) => void }) {
|
||||
@@ -62,7 +62,7 @@ export function ImageSizePresetsEditor({ profile, disabled, onChange }: { profil
|
||||
</div>
|
||||
</header>
|
||||
<div className="image-size-preset-body">
|
||||
{!imageTierAvailable(profile, tier) ? <p className="image-size-hint">需先配置此档位对应的质量值,用户端才会开放。</p> : null}
|
||||
{imageResolutionUsesQuality(profile) && !imageTierAvailable(profile, tier) ? <p className="image-size-hint">需先配置此档位对应的质量值,用户端才会开放。</p> : null}
|
||||
<div className="image-size-preset-options" role="group" aria-label={`${tier.toUpperCase()} 支持的宽高比`}>
|
||||
{ratios.map((ratio) => {
|
||||
const selected = items.find((item) => item.ratio === ratio);
|
||||
|
||||
Reference in New Issue
Block a user