fix(canvas): 画布创作交互 - 优化提示词面板并修复工具弹窗与节点缩放卡顿
This commit is contained in:
@@ -24,6 +24,7 @@ export function CanvasAudioSettingsPopover({ config, onConfigChange, buttonClass
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [buttonRect, setButtonRect] = useState<DOMRect | null>(null);
|
||||
const summary = `${audioVoiceLabel(config.audioVoice)} · ${audioFormatLabel(config.audioFormat)} · ${audioSpeedLabel(config.audioSpeed)}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -51,10 +52,8 @@ export function CanvasAudioSettingsPopover({ config, onConfigChange, buttonClass
|
||||
return (
|
||||
<>
|
||||
<span ref={buttonRef} className="inline-flex min-w-0">
|
||||
<Button size="small" type="text" className={buttonClassName || "!h-8 !max-w-[170px] !justify-start !rounded-full !px-2.5"} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} onClick={() => setOpen((current) => !current)}>
|
||||
<span className="truncate">
|
||||
{audioVoiceLabel(config.audioVoice)} · {audioFormatLabel(config.audioFormat)} · {audioSpeedLabel(config.audioSpeed)}
|
||||
</span>
|
||||
<Button size="small" type="text" className={`canvas-generation-settings-trigger ${buttonClassName || "!h-8 !max-w-[170px] !justify-start !rounded-full !px-2.5"}`} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} aria-expanded={open} aria-label={`音频设置:${summary}`} title={`音频设置 · ${summary}`} onClick={() => setOpen((current) => !current)}>
|
||||
<span className="truncate">{summary}</span>
|
||||
</Button>
|
||||
</span>
|
||||
{panel}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function CanvasImageSettingsPopover({ config, onConfigChange, onOpenChang
|
||||
return (
|
||||
<>
|
||||
<span ref={buttonRef} className="inline-flex min-w-0">
|
||||
<Button size="small" type="text" className={buttonClassName || "!h-8 !max-w-[180px] !justify-start !rounded-full !px-2.5"} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} onClick={() => updateOpen(!open)}>
|
||||
<Button size="small" type="text" className={`canvas-generation-settings-trigger ${buttonClassName || "!h-8 !max-w-[180px] !justify-start !rounded-full !px-2.5"}`} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} aria-expanded={open} aria-label={`图像设置:${summary}`} title={`图像设置 · ${summary}`} onClick={() => updateOpen(!open)}>
|
||||
<span className="truncate">{summary}</span>
|
||||
</Button>
|
||||
</span>
|
||||
@@ -90,9 +90,9 @@ function ImageSettingsPortal({
|
||||
showCount: boolean;
|
||||
onConfigChange: (key: keyof AiConfig, value: string) => void;
|
||||
}) {
|
||||
const width = 328;
|
||||
const gap = 8;
|
||||
const margin = 12;
|
||||
const width = Math.min(420, window.innerWidth - margin * 2);
|
||||
const alignRight = placement?.endsWith("Right");
|
||||
const alignCenter = placement === "top" || placement === "bottom";
|
||||
const left = alignCenter ? buttonRect.left + buttonRect.width / 2 - width / 2 : alignRight ? buttonRect.right - width : buttonRect.left;
|
||||
@@ -121,7 +121,7 @@ function ImageSettingsPortal({
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<ImageSettingsPanel config={config} onConfigChange={(key, value) => onConfigChange(key, value)} theme={theme} showCount={showCount} className="space-y-3" />
|
||||
<ImageSettingsPanel config={config} onConfigChange={(key, value) => onConfigChange(key, value)} theme={theme} showCount={showCount} quickCount={3} className="space-y-3" />
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
@@ -118,9 +118,13 @@ export function CanvasNodeHoverToolbar({
|
||||
const [imageToolMenuOpen, setImageToolMenuOpen] = useState(false);
|
||||
const [anchor, setAnchor] = useState<{ left: number; top: number } | null>(null);
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
// Dropdown 关闭与菜单点击处于同一事件批次,ref 用来同步守住即将打开的 Modal,避免工具栏先被卸载。
|
||||
const imageToolSettingsOpenRef = useRef(false);
|
||||
const imageToolMenuOpenRef = useRef(false);
|
||||
const { message } = App.useApp();
|
||||
const copyText = useCopyText();
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const themeName = useThemeStore((state) => state.theme);
|
||||
const theme = canvasThemes[themeName];
|
||||
const simpleMode = workspaceMode === "simple";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,6 +143,8 @@ export function CanvasNodeHoverToolbar({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
imageToolSettingsOpenRef.current = false;
|
||||
imageToolMenuOpenRef.current = false;
|
||||
setImageToolSettingsOpen(false);
|
||||
setImageToolMenuOpen(false);
|
||||
}, [node?.id]);
|
||||
@@ -162,8 +168,8 @@ export function CanvasNodeHoverToolbar({
|
||||
const halfToolbar = toolbarWidth / 2;
|
||||
const canClamp = toolbarWidth > 0 && toolbarWidth <= containerRect.width - 20;
|
||||
const left = canClamp ? Math.min(Math.max(preferredLeft, halfToolbar + 10), containerRect.width - halfToolbar - 10) : preferredLeft;
|
||||
// 外置节点标题占用节点上方一行,工具 Dock 再向上错开,避免遮挡名称编辑入口。
|
||||
const top = nodeRect.top - containerRect.top - 42;
|
||||
// 外置标题固定占 24px,额外保留 6px 即可兼顾层级和名称编辑入口。
|
||||
const top = nodeRect.top - containerRect.top - 30;
|
||||
if (toolbarRef.current) {
|
||||
toolbarRef.current.style.left = `${left}px`;
|
||||
toolbarRef.current.style.top = `${top}px`;
|
||||
@@ -217,6 +223,8 @@ export function CanvasNodeHoverToolbar({
|
||||
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onAnnotate, onMaskEdit, onEmotion, onPortraitTexture, onCrop, onSplit, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt });
|
||||
|
||||
function openImageToolSettings() {
|
||||
imageToolSettingsOpenRef.current = true;
|
||||
imageToolMenuOpenRef.current = false;
|
||||
onKeep(activeNode.id);
|
||||
setImageToolMenuOpen(false);
|
||||
setDraftImageToolIds(quickImageToolIds);
|
||||
@@ -254,19 +262,22 @@ export function CanvasNodeHoverToolbar({
|
||||
];
|
||||
|
||||
const closeImageToolSettings = () => {
|
||||
imageToolSettingsOpenRef.current = false;
|
||||
setImageToolSettingsOpen(false);
|
||||
onLeave();
|
||||
};
|
||||
|
||||
const runTemporaryImageTool = (tool: ToolbarTool) => {
|
||||
imageToolMenuOpenRef.current = false;
|
||||
setImageToolMenuOpen(false);
|
||||
tool.onClick();
|
||||
};
|
||||
|
||||
const handleImageToolMenuOpenChange = (open: boolean) => {
|
||||
imageToolMenuOpenRef.current = open;
|
||||
setImageToolMenuOpen(open);
|
||||
if (open) onKeep(activeNode.id);
|
||||
else if (!imageToolSettingsOpen) onLeave();
|
||||
else if (!imageToolSettingsOpenRef.current) onLeave();
|
||||
};
|
||||
|
||||
const setDraftImageToolVisible = (id: ImageQuickToolId, visible: boolean) => {
|
||||
@@ -292,6 +303,12 @@ export function CanvasNodeHoverToolbar({
|
||||
|
||||
const dockShellStyle = canvasDockStyle(theme, theme.node.text);
|
||||
const embeddedDockStyle = { ...dockShellStyle, background: "transparent", borderColor: "transparent", boxShadow: "none" };
|
||||
const labeledDockStyle = {
|
||||
...dockShellStyle,
|
||||
boxShadow: themeName === "dark"
|
||||
? `0 18px 52px ${theme.spatial.shadow}`
|
||||
: "0 10px 30px rgba(15,23,42,.12), 0 1px 2px rgba(15,23,42,.05), inset 0 1px 0 rgba(255,255,255,.7)",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -301,12 +318,12 @@ export function CanvasNodeHoverToolbar({
|
||||
style={{ left: anchor.left, top: anchor.top, width: "max-content", maxWidth: `min(calc(100% - 20px), ${showDockLabels ? 840 : 560}px)`, color: theme.node.text }}
|
||||
onMouseEnter={() => onKeep(node.id)}
|
||||
onMouseLeave={() => {
|
||||
if (!imageToolSettingsOpen && !imageToolMenuOpen) onLeave();
|
||||
if (!imageToolSettingsOpenRef.current && !imageToolMenuOpenRef.current) onLeave();
|
||||
}}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className={`aceternity-floating-dock thin-scrollbar relative flex max-w-full overflow-x-auto rounded-[14px] border backdrop-blur-2xl ${showDockLabels ? "h-11 items-center px-2 py-1" : "h-10 items-end gap-1 px-1.5 pb-1"}`} style={showDockLabels ? { ...dockShellStyle, boxShadow: `0 18px 52px ${theme.spatial.shadow}` } : dockShellStyle}>
|
||||
<div className={`aceternity-floating-dock thin-scrollbar relative flex max-w-full overflow-x-auto rounded-[14px] border backdrop-blur-2xl ${showDockLabels ? "h-11 items-center px-2 py-1" : "h-10 items-end gap-1 px-1.5 pb-1"}`} style={showDockLabels ? labeledDockStyle : dockShellStyle}>
|
||||
{dockItems.length ? <FloatingDock embedded items={dockItems} size="compact" showLabels={showDockLabels} ariaLabel="节点快捷工具" className={`pointer-events-auto shrink-0 ${showDockLabels ? "" : "max-w-[min(calc(100vw-20px),400px)]"}`} style={embeddedDockStyle} /> : null}
|
||||
{hasImage && !simpleMode ? (
|
||||
<Dropdown
|
||||
@@ -324,7 +341,7 @@ export function CanvasNodeHoverToolbar({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`aceternity-dock-command pointer-events-auto shrink-0 outline-none focus-visible:ring-2 ${showDockLabels ? "inline-flex h-8 items-center justify-center gap-1.5 rounded-[9px] px-2.5" : "grid size-8 place-items-center rounded-full"}`}
|
||||
className={`aceternity-dock-command pointer-events-auto shrink-0 outline-none focus-visible:ring-2 ${showDockLabels ? "is-labeled inline-flex h-8 items-center justify-center gap-1.5 rounded-[9px] px-2.5" : "grid size-8 place-items-center rounded-full"}`}
|
||||
style={{ color: theme.node.text, "--tw-ring-color": theme.accent.primary } as CSSProperties}
|
||||
aria-label="更多图片工具"
|
||||
title="更多图片工具"
|
||||
|
||||
@@ -59,15 +59,19 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
.map((item) => ({ nodeId: item.nodeId, label: item.label, title: item.title, previewUrl: item.previewUrl }));
|
||||
const darkSurface = themeName === "dark";
|
||||
const monochromeAccent = theme.node.activeStroke;
|
||||
const shellBorder = darkSurface ? "rgba(255,255,255,.08)" : "rgba(17,24,39,.12)";
|
||||
const insetBorder = darkSurface ? "rgba(255,255,255,.06)" : "rgba(17,24,39,.10)";
|
||||
const shellBorder = darkSurface ? "rgba(255,255,255,.08)" : "rgba(15,23,42,.08)";
|
||||
const insetBorder = darkSurface ? "rgba(255,255,255,.06)" : "rgba(15,23,42,.07)";
|
||||
const shellSurface = darkSurface
|
||||
? theme.canvas.background
|
||||
: `linear-gradient(180deg, rgba(255,255,255,.94), rgba(248,250,252,.86)), ${theme.spatial.elevated}`;
|
||||
: "rgba(250,251,252,.97)";
|
||||
const composerSurface = darkSurface
|
||||
? theme.canvas.background
|
||||
: `linear-gradient(180deg, rgba(255,255,255,.7), rgba(248,250,252,.72)), ${theme.spatial.dropzone}`;
|
||||
const controlSurface = darkSurface ? theme.canvas.background : "rgba(17,24,39,.045)";
|
||||
: "rgba(15,23,42,.025)";
|
||||
const controlSurface = darkSurface ? theme.canvas.background : "rgba(15,23,42,.045)";
|
||||
const controlsSurface = darkSurface ? controlSurface : "transparent";
|
||||
const shellShadow = darkSurface ? `0 22px 60px ${theme.spatial.shadow}, 0 2px 8px rgba(0,0,0,.22)` : "0 12px 32px rgba(15,23,42,.10), 0 1px 2px rgba(15,23,42,.06)";
|
||||
const composerShadow = darkSurface ? "inset 0 1px 4px rgba(0,0,0,.22)" : "inset 0 0 0 1px rgba(15,23,42,.045)";
|
||||
const modalShadow = darkSurface ? `0 30px 90px ${theme.spatial.shadow}` : "0 24px 72px rgba(15,23,42,.16)";
|
||||
const referenceShelfHeight = activeReferenceCount ? 42 : 0;
|
||||
const composerMinHeight = activeReferenceCount ? 82 : 58;
|
||||
const composerHeight = Math.min(144, Math.max(composerMinHeight, Math.ceil(promptContentHeight + referenceShelfHeight)));
|
||||
@@ -177,13 +181,13 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
);
|
||||
|
||||
const renderComposerControls = (expanded: boolean) => simpleMode ? (
|
||||
<div className="flex min-w-0 items-center justify-between gap-2 p-1" style={{ background: controlSurface }}>
|
||||
<div className="flex min-w-0 items-center justify-between gap-2 p-1" style={{ background: controlsSurface }}>
|
||||
<span className="min-w-0 truncate px-2 text-[10px]" style={{ color: theme.node.muted }}>
|
||||
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"}
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-md !px-2.5 !text-[10px] !font-medium"
|
||||
className="!inline-flex !h-8 shrink-0 !items-center !gap-1 !rounded-full !px-2.5 !text-[10px] !font-medium"
|
||||
danger={isRunning}
|
||||
disabled={isSubmitDisabled}
|
||||
style={{ background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent, color: isSubmitDisabled ? theme.node.faint : isRunning ? "#ffffff" : theme.canvas.background, boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)` }}
|
||||
@@ -195,29 +199,29 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-w-0 items-center justify-between gap-0.5 p-1" style={{ background: controlSurface }}>
|
||||
<div className="flex min-w-0 items-center justify-between gap-0.5 p-1" style={{ background: controlsSurface }}>
|
||||
<div className={`${expanded ? "max-w-[320px]" : mode === "image" || mode === "video" ? "max-w-[240px]" : "max-w-[174px]"} min-w-[104px] flex-1`}>
|
||||
<ModelPicker className="!h-7 !w-full !min-w-0 !text-[10px] !font-normal [&_img]:!size-3 [&_.lucide]:!size-3" fullWidth config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
|
||||
</div>
|
||||
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-0.5">
|
||||
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-1">
|
||||
{mode === "image" ? (
|
||||
<CanvasImageSettingsPopover
|
||||
config={config}
|
||||
placement={expanded ? "topRight" : "topLeft"}
|
||||
buttonClassName="!h-7 !w-[138px] !justify-start !rounded-md !border-0 !bg-transparent !px-1.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
|
||||
buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3"
|
||||
onConfigChange={(key, value) => onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })}
|
||||
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
|
||||
onOpenChange={expanded ? undefined : onImageSettingsOpenChange}
|
||||
/>
|
||||
) : mode === "video" ? (
|
||||
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-7 !w-[136px] !justify-start !rounded-md !border-0 !bg-transparent !px-1.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
|
||||
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-7 !w-[144px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
|
||||
) : mode === "audio" ? (
|
||||
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[138px] !justify-start !rounded-md !border-0 !bg-transparent !px-1.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
|
||||
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[10px] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
|
||||
) : null}
|
||||
<GenerationCostBadge credits={credits} theme={theme} />
|
||||
<Button
|
||||
type="text"
|
||||
className="!inline-flex !h-8 !w-8 shrink-0 !items-center !justify-center !rounded-md !border !p-0 transition hover:!-translate-y-px hover:!brightness-110 motion-reduce:hover:!translate-y-0"
|
||||
className="!inline-flex !h-8 !w-8 shrink-0 !items-center !justify-center !rounded-full !border !p-0 transition hover:!-translate-y-px hover:!brightness-110 motion-reduce:hover:!translate-y-0"
|
||||
danger={isRunning}
|
||||
disabled={isSubmitDisabled}
|
||||
style={{ background: isSubmitDisabled ? theme.toolbar.itemHover : isRunning ? theme.accent.danger : monochromeAccent, borderColor: isSubmitDisabled ? insetBorder : monochromeAccent, color: isSubmitDisabled ? theme.node.faint : theme.canvas.background, boxShadow: isSubmitDisabled ? "none" : `0 8px 20px ${theme.spatial.shadow}, inset 0 1px 0 rgba(255,255,255,.18)` }}
|
||||
@@ -232,8 +236,8 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
|
||||
return (
|
||||
<div
|
||||
className="aceternity-floating-panel relative overflow-hidden rounded-lg border p-2"
|
||||
style={{ background: shellSurface, borderColor: shellBorder, color: theme.node.text, boxShadow: `0 22px 60px ${theme.spatial.shadow}, 0 2px 8px rgba(0,0,0,.22)` }}
|
||||
className="aceternity-floating-panel relative overflow-hidden rounded-xl border p-2"
|
||||
style={{ background: shellSurface, borderColor: shellBorder, color: theme.node.text, boxShadow: shellShadow }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onWheel={(event) => event.stopPropagation()}
|
||||
@@ -241,8 +245,8 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
{renderComposerHeader(false)}
|
||||
|
||||
<div
|
||||
className="relative mt-2 flex max-h-36 flex-col overflow-hidden rounded-lg outline-none ring-0 transition-[height] duration-150 focus-within:outline-none focus-within:ring-0 motion-reduce:transition-none"
|
||||
style={{ height: composerHeight, background: composerSurface, boxShadow: "inset 0 1px 4px rgba(0,0,0,.22)" }}
|
||||
className="relative mt-2 flex max-h-36 flex-col overflow-hidden rounded-xl outline-none ring-0 transition-[height] duration-150 focus-within:outline-none focus-within:ring-0 motion-reduce:transition-none"
|
||||
style={{ height: composerHeight, background: composerSurface, boxShadow: composerShadow }}
|
||||
>
|
||||
<ConnectedReferenceShelf references={mentionReferences} theme={theme} onInsert={insertPromptReference} />
|
||||
<CanvasResourceMentionTextarea
|
||||
@@ -274,13 +278,13 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
width={760}
|
||||
destroyOnHidden
|
||||
onCancel={() => { setExpandedPresetOpen(false); setExpandedPromptOpen(false); }}
|
||||
styles={{ container: { display: "flex", height: "min(440px, calc(100vh - 40px))", flexDirection: "column", borderRadius: 12, border: `1px solid ${shellBorder}`, padding: 0, overflow: "hidden", background: shellSurface, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }, body: { minHeight: 0, flex: 1, padding: 0 } }}
|
||||
styles={{ container: { display: "flex", height: "min(440px, calc(100vh - 40px))", flexDirection: "column", borderRadius: 12, border: `1px solid ${shellBorder}`, padding: 0, overflow: "hidden", background: shellSurface, boxShadow: modalShadow }, body: { minHeight: 0, flex: 1, padding: 0 } }}
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col gap-2.5 p-3" style={{ color: theme.node.text }}>
|
||||
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
|
||||
<div
|
||||
className="flex min-h-[240px] flex-1 flex-col overflow-hidden rounded-lg outline-none ring-0 focus-within:outline-none focus-within:ring-0"
|
||||
style={{ background: composerSurface, boxShadow: "inset 0 1px 4px rgba(0,0,0,.22)" }}
|
||||
className="flex min-h-[240px] flex-1 flex-col overflow-hidden rounded-xl outline-none ring-0 focus-within:outline-none focus-within:ring-0"
|
||||
style={{ background: composerSurface, boxShadow: composerShadow }}
|
||||
>
|
||||
<ConnectedReferenceShelf references={mentionReferences} theme={theme} onInsert={insertPromptReference} />
|
||||
<CanvasResourceMentionTextarea
|
||||
@@ -375,8 +379,8 @@ function ReferenceThumbnail({ reference }: { reference: CanvasResourceReference
|
||||
function GenerationCostBadge({ credits, theme }: { credits: number | null; theme: CanvasTheme }) {
|
||||
if (credits === null) return null;
|
||||
return (
|
||||
<span className="inline-flex h-6 shrink-0 items-center gap-0.5 px-1 text-[9px] font-medium tabular-nums" style={{ color: theme.node.muted }} title="本次生成消耗">
|
||||
<CreditSymbol />
|
||||
<span className="canvas-generation-cost-badge inline-flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-[9px] font-semibold tabular-nums" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title={`预计消耗 ${credits.toLocaleString()} 积分`} aria-label={`预计消耗 ${credits.toLocaleString()} 积分`}>
|
||||
<CreditSymbol className="text-[11px]" />
|
||||
{credits.toLocaleString()}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1138,8 +1138,8 @@ function NodeExternalHeader({ node, scale, active, editable, editing, draft, the
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 z-40 flex h-6 max-w-[240px] items-center gap-1"
|
||||
style={{ top: -24 * inverseScale, color: active ? theme.node.text : theme.node.label, transform: `scale(${inverseScale})`, transformOrigin: "left top" }}
|
||||
className="canvas-node-external-header absolute bottom-full left-0 z-40 flex h-6 max-w-[240px] items-center gap-1"
|
||||
style={{ color: active ? theme.node.text : theme.node.label, transform: `scale(var(--canvas-live-inverse-scale, ${inverseScale}))`, transformOrigin: "left bottom" }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -87,26 +87,26 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha
|
||||
}, [mode, query, skillReferences]);
|
||||
|
||||
const content = (
|
||||
<div data-canvas-no-zoom className="w-[320px] max-w-[calc(100vw-32px)]" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<Input autoFocus allowClear size="small" prefix={<Search className="size-3.5 opacity-45" />} placeholder="搜索预设或已激活技能" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
<div className="thin-scrollbar mt-2 max-h-72 overflow-y-auto space-y-1">
|
||||
<div data-canvas-no-zoom className="canvas-preset-picker-menu w-[320px] max-w-[calc(100vw-24px)]" onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
|
||||
<Input className="canvas-preset-picker-search" variant="borderless" autoFocus allowClear size="small" prefix={<Search className="size-3.5" />} placeholder="搜索预设或已激活技能" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
<div className="thin-scrollbar mt-1 max-h-72 space-y-0.5 overflow-y-auto">
|
||||
{presets.length ? presets.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
className="flex w-full items-start gap-2 rounded-lg px-2 py-2 text-left transition hover:bg-black/5 dark:hover:bg-white/10"
|
||||
className="canvas-preset-picker-option"
|
||||
onClick={() => {
|
||||
onSelect(preset);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="mt-0.5 grid size-7 shrink-0 place-items-center rounded-md" style={{ background: theme.toolbar.itemHover, color: theme.node.activeStroke }}><WandSparkles className="size-3.5" /></span>
|
||||
<span className="canvas-preset-picker-option-icon" style={{ background: theme.accent.primarySoft, color: theme.accent.primary }}><WandSparkles className="size-3.5" /></span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex items-center gap-1.5 text-xs font-semibold" style={{ color: theme.node.text }}>
|
||||
<span className="truncate">{preset.name}</span>
|
||||
<span className="shrink-0 text-[9px] font-medium" style={{ color: theme.node.faint }}>{preset.source === "skill" ? "技能" : "预设"}</span>
|
||||
<span className="shrink-0 text-[9px] font-medium" style={{ color: theme.accent.primary }}>{preset.source === "skill" ? "技能" : "预设"}</span>
|
||||
</span>
|
||||
<span className="mt-0.5 block line-clamp-2 text-[10px] leading-4" style={{ color: theme.node.muted }}>{preset.description}</span>
|
||||
<span className="mt-0.5 block truncate text-[10px] leading-4" style={{ color: theme.node.muted }}>{preset.description}</span>
|
||||
</span>
|
||||
</button>
|
||||
)) : <div className="py-8 text-center text-xs" style={{ color: theme.node.muted }}>没有匹配的预设</div>}
|
||||
@@ -115,10 +115,10 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={actualOpen} onOpenChange={setOpen} trigger="click" placement="topLeft" content={content} styles={{ content: { padding: 8, background: theme.toolbar.panel, border: `1px solid ${theme.toolbar.border}` } }}>
|
||||
<button type="button" className={`inline-flex shrink-0 items-center justify-center gap-1 rounded-md transition hover:brightness-125 ${compact ? "size-6" : dense ? "h-6 px-1.5" : "h-7 px-2"}`} style={{ background: dense ? theme.canvas.background : theme.toolbar.itemHover, color: dense ? theme.node.text : theme.node.muted }} title="预设(输入 / 也可打开)" aria-label="打开预设">
|
||||
<Popover open={actualOpen} onOpenChange={setOpen} trigger="click" placement="topLeft" arrow={false} content={content} classNames={{ root: "canvas-preset-picker-popover", container: "canvas-composer-popover-surface", content: "canvas-composer-popover-content" }}>
|
||||
<button type="button" className={`canvas-preset-picker-trigger inline-flex shrink-0 items-center justify-center gap-1 rounded-lg transition hover:brightness-110 focus-visible:outline-none ${compact ? "size-6" : dense ? "h-6 px-1.5" : "h-7 px-2"}`} style={{ background: theme.accent.primarySoft, color: theme.accent.primary }} title="打开提示词预设" aria-label="打开提示词预设" aria-expanded={actualOpen}>
|
||||
<WandSparkles className={dense ? "size-3" : "size-3.5"} />
|
||||
{compact ? null : <span className="text-[10px] font-medium">预设</span>}
|
||||
{compact ? null : <span className="text-[10px] font-semibold">预设</span>}
|
||||
</button>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -125,12 +125,12 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
|
||||
|
||||
const syncMention = (nextValue: string, cursor: number) => {
|
||||
const prefix = nextValue.slice(0, cursor);
|
||||
const match = /(^|\s)@([^\s@]*)$/.exec(prefix);
|
||||
const match = /@([^\s@,.;:!?,。;:!?、)\]}】)]*)$/.exec(prefix);
|
||||
if (!match || !references.some((item) => item.active)) {
|
||||
closeMention();
|
||||
return;
|
||||
}
|
||||
const nextMention = { start: cursor - match[2].length - 1, query: match[2] };
|
||||
const nextMention = { start: match.index, query: match[1] };
|
||||
const isSameMention = mention?.start === nextMention.start && mention.query === nextMention.query;
|
||||
if (!isSameMention) {
|
||||
setMention(nextMention);
|
||||
@@ -353,7 +353,7 @@ function createInlineMentionChip(reference: CanvasResourceReference, token: stri
|
||||
const chip = document.createElement("span");
|
||||
chip.contentEditable = "false";
|
||||
chip.dataset.mentionToken = token;
|
||||
chip.className = "mx-[0.06em] inline-flex h-[1.55em] translate-y-[0.18em] select-none items-center gap-[0.18em] rounded-[0.38em] bg-black/[0.06] px-[0.22em] text-[0.92em] font-medium leading-none text-current align-baseline dark:bg-white/[0.1]";
|
||||
chip.className = "mx-[0.06em] inline-flex h-[1.68em] translate-y-[0.08em] select-none items-center gap-[0.18em] rounded-[0.38em] bg-black/[0.06] px-[0.22em] text-[0.92em] font-medium leading-none text-current align-baseline dark:bg-white/[0.1]";
|
||||
|
||||
const at = document.createElement("span");
|
||||
at.className = "shrink-0 opacity-90";
|
||||
|
||||
@@ -21,6 +21,7 @@ export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClass
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [buttonRect, setButtonRect] = useState<DOMRect | null>(null);
|
||||
const summary = `${videoResolutionLabel(config.vquality)} · ${videoSizeLabel(config.size)} · ${videoSecondsLabel(config.videoSeconds)}`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -48,10 +49,8 @@ export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClass
|
||||
return (
|
||||
<>
|
||||
<span ref={buttonRef} className="inline-flex min-w-0">
|
||||
<Button size="small" type="text" className={buttonClassName || "!h-8 !max-w-[170px] !justify-start !rounded-full !px-2.5"} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} onClick={() => setOpen((current) => !current)}>
|
||||
<span className="truncate">
|
||||
{videoResolutionLabel(config.vquality)} · {videoSizeLabel(config.size)} · {videoSecondsLabel(config.videoSeconds)}
|
||||
</span>
|
||||
<Button size="small" type="text" className={`canvas-generation-settings-trigger ${buttonClassName || "!h-8 !max-w-[170px] !justify-start !rounded-full !px-2.5"}`} style={{ background: theme.node.fill, color: theme.node.text }} icon={<Settings2 className="size-3.5" />} aria-expanded={open} aria-label={`视频设置:${summary}`} title={`视频设置 · ${summary}`} onClick={() => setOpen((current) => !current)}>
|
||||
<span className="truncate">{summary}</span>
|
||||
</Button>
|
||||
</span>
|
||||
{panel}
|
||||
|
||||
@@ -375,6 +375,7 @@ export function InfiniteCanvas({ containerRef, viewport, backgroundMode = "dots"
|
||||
"--canvas-live-x": `${viewport.x}px`,
|
||||
"--canvas-live-y": `${viewport.y}px`,
|
||||
"--canvas-live-scale": viewport.k,
|
||||
"--canvas-live-inverse-scale": 1 / Math.max(viewport.k, 0.05),
|
||||
"--canvas-committed-scale": viewport.k,
|
||||
"--canvas-live-scale-ratio": 1,
|
||||
"--canvas-grid-size": `${48 * viewport.k}px`,
|
||||
|
||||
Reference in New Issue
Block a user