Compare commits
3 Commits
453de420fc
...
94718553c6
| Author | SHA1 | Date | |
|---|---|---|---|
| 94718553c6 | |||
| cbf4e21e2b | |||
| 82683c40ea |
@@ -242,6 +242,7 @@
|
|||||||
"referenceImage": "Reference",
|
"referenceImage": "Reference",
|
||||||
"imageSettings": "Image settings",
|
"imageSettings": "Image settings",
|
||||||
"imageCountUnit": "{count} img",
|
"imageCountUnit": "{count} img",
|
||||||
|
"imageCountLabel": "Image",
|
||||||
"quality": "Quality",
|
"quality": "Quality",
|
||||||
"qualityAuto": "Auto",
|
"qualityAuto": "Auto",
|
||||||
"qualityHigh": "High",
|
"qualityHigh": "High",
|
||||||
|
|||||||
@@ -242,6 +242,7 @@
|
|||||||
"referenceImage": "参考图",
|
"referenceImage": "参考图",
|
||||||
"imageSettings": "图片参数",
|
"imageSettings": "图片参数",
|
||||||
"imageCountUnit": "{count} 张",
|
"imageCountUnit": "{count} 张",
|
||||||
|
"imageCountLabel": "生成数量",
|
||||||
"quality": "质量",
|
"quality": "质量",
|
||||||
"qualityAuto": "自动",
|
"qualityAuto": "自动",
|
||||||
"qualityHigh": "高",
|
"qualityHigh": "高",
|
||||||
|
|||||||
@@ -104,43 +104,75 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-preview {
|
.image-generator-preview {
|
||||||
|
--image-generator-viewport-scale: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-label {
|
.image-generator-meta {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: -28px;
|
right: 0;
|
||||||
display: inline-flex;
|
top: calc(-30px / var(--image-generator-viewport-scale));
|
||||||
align-items: center;
|
z-index: 2;
|
||||||
gap: 6px;
|
height: calc(24px / var(--image-generator-viewport-scale));
|
||||||
color: #2f80ff;
|
color: #2f80ff;
|
||||||
font-size: 15px;
|
font-size: calc(16px / var(--image-generator-viewport-scale));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 22px;
|
line-height: calc(24px / var(--image-generator-viewport-scale));
|
||||||
|
letter-spacing: 0;
|
||||||
|
pointer-events: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-generator-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(6px / var(--image-generator-viewport-scale));
|
||||||
|
max-width: calc(100% - (126px / var(--image-generator-viewport-scale)));
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.image-generator-label svg {
|
.image-generator-label svg {
|
||||||
width: 15px;
|
width: calc(17px / var(--image-generator-viewport-scale));
|
||||||
height: 15px;
|
height: calc(17px / var(--image-generator-viewport-scale));
|
||||||
|
flex: 0 0 auto;
|
||||||
fill: #2f80ff;
|
fill: #2f80ff;
|
||||||
stroke: #2f80ff;
|
stroke: #2f80ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-generator-label span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-size-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.image-generator-frame {
|
.image-generator-frame {
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 2px solid #2f80ff;
|
border: calc(2px / var(--image-generator-viewport-scale)) solid #2f80ff;
|
||||||
background: #eeeeed;
|
background: #eeeeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-frame svg {
|
.image-generator-frame svg {
|
||||||
|
width: calc(34px / var(--image-generator-viewport-scale));
|
||||||
|
height: calc(34px / var(--image-generator-viewport-scale));
|
||||||
color: #cfd0d2;
|
color: #cfd0d2;
|
||||||
fill: #cfd0d2;
|
fill: #cfd0d2;
|
||||||
stroke: #cfd0d2;
|
stroke: #cfd0d2;
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import { imageAdjustmentImageStyle, imageAdjustmentLayerStyle, isDefaultImageAdj
|
|||||||
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
|
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
|
||||||
import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
import { isImageUrl } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
||||||
import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
|
import { framePreviewStyle } from "@/ui/pages/CanvasWorkspace/canvas/frameStyle";
|
||||||
|
import { imageGeneratorTargetSizeFromNode, type ImageGeneratorTargetSize } from "@/ui/pages/CanvasWorkspace/canvas/imageGeneratorSizing";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode; viewportScale?: number }) {
|
export function ArtboardPreview({ node, viewportScale = 1, imageGeneratorTargetSize }: { node: CanvasNode; viewportScale?: number; imageGeneratorTargetSize?: ImageGeneratorTargetSize }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const overlayUiStyle = workOverlayUiStyle(viewportScale, node);
|
const overlayUiStyle = workOverlayUiStyle(viewportScale, node);
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export function ArtboardPreview({ node, viewportScale = 1 }: { node: CanvasNode;
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "image" && node.layerRole === "image-generator") {
|
if (node.type === "image" && node.layerRole === "image-generator") {
|
||||||
return <ImageGeneratorPreview node={node} />;
|
return <ImageGeneratorPreview node={node} viewportScale={viewportScale} targetSize={imageGeneratorTargetSize} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "frame" && node.layerRole === "manual-frame") {
|
if (node.type === "frame" && node.layerRole === "manual-frame") {
|
||||||
@@ -110,21 +111,31 @@ function clampNumber(value: number, min: number, max: number) {
|
|||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageGeneratorUiStyle(viewportScale: number) {
|
||||||
|
return {
|
||||||
|
"--image-generator-viewport-scale": String(Math.max(viewportScale, 0.01))
|
||||||
|
} as CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
function artboardKind(node: CanvasNode) {
|
function artboardKind(node: CanvasNode) {
|
||||||
if (node.height > node.width * 1.24) return "tall";
|
if (node.height > node.width * 1.24) return "tall";
|
||||||
if (node.width > node.height * 1.35) return "wide";
|
if (node.width > node.height * 1.35) return "wide";
|
||||||
return "square";
|
return "square";
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageGeneratorPreview({ node }: { node: CanvasNode }) {
|
function ImageGeneratorPreview({ node, viewportScale, targetSize }: { node: CanvasNode; viewportScale: number; targetSize?: ImageGeneratorTargetSize }) {
|
||||||
|
const size = targetSize ?? imageGeneratorTargetSizeFromNode(node);
|
||||||
return (
|
return (
|
||||||
<div className="image-generator-preview">
|
<div className="image-generator-preview" style={imageGeneratorUiStyle(viewportScale)}>
|
||||||
|
<div className="image-generator-meta">
|
||||||
<div className="image-generator-label">
|
<div className="image-generator-label">
|
||||||
<ImageIcon size={14} />
|
<ImageIcon aria-hidden="true" />
|
||||||
<span>{node.title}</span>
|
<span>{node.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="image-generator-size-label">{size.width} × {size.height}</div>
|
||||||
|
</div>
|
||||||
<div className="image-generator-frame">
|
<div className="image-generator-frame">
|
||||||
<ImageIcon size={34} />
|
<ImageIcon aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -413,6 +413,14 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-settings-panel h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #242a34;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.composer-settings-panel section {
|
.composer-settings-panel section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export function composerImageSize(settings: ComposerImageSettings) {
|
|||||||
|
|
||||||
export function composerSettingsSummary(settings: ComposerImageSettings) {
|
export function composerSettingsSummary(settings: ComposerImageSettings) {
|
||||||
const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio;
|
const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio;
|
||||||
return `${qualityLabel(settings.quality)} · ${ratioLabel} · ${settings.count} img`;
|
const sizeLabel = settings.ratio === "auto" ? composerImageSize(settings).replace("x", "×") : ratioLabel;
|
||||||
|
return `${qualityLabel(settings.quality)} · ${sizeLabel} · ${settings.count} img`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function qualityLabel(quality: ComposerImageQuality) {
|
function qualityLabel(quality: ComposerImageQuality) {
|
||||||
@@ -348,16 +349,19 @@ export const CanvasAgentComposer = forwardRef<PromptComposerHandle, CanvasAgentC
|
|||||||
export function ComposerSettingsPanel({
|
export function ComposerSettingsPanel({
|
||||||
settings,
|
settings,
|
||||||
onPatch,
|
onPatch,
|
||||||
onSelectRatio
|
onSelectRatio,
|
||||||
|
title
|
||||||
}: {
|
}: {
|
||||||
settings: ComposerImageSettings;
|
settings: ComposerImageSettings;
|
||||||
onPatch: (patch: Partial<ComposerImageSettings>) => void;
|
onPatch: (patch: Partial<ComposerImageSettings>) => void;
|
||||||
onSelectRatio: (ratio: ComposerImageRatio) => void;
|
onSelectRatio: (ratio: ComposerImageRatio) => void;
|
||||||
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="composer-settings-panel">
|
<div className="composer-settings-panel">
|
||||||
|
{title && <h3>{title}</h3>}
|
||||||
<section>
|
<section>
|
||||||
<h4>{t("quality")}</h4>
|
<h4>{t("quality")}</h4>
|
||||||
<div className="composer-quality-control">
|
<div className="composer-quality-control">
|
||||||
@@ -391,7 +395,7 @@ export function ComposerSettingsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h4>Image</h4>
|
<h4>{t("imageCountLabel")}</h4>
|
||||||
<div className="composer-count-grid">
|
<div className="composer-count-grid">
|
||||||
{Array.from({ length: 10 }, (_, index) => index + 1).map((count) => (
|
{Array.from({ length: 10 }, (_, index) => index + 1).map((count) => (
|
||||||
<button key={count} type="button" className={settings.count === count ? "selected" : ""} onClick={() => onPatch({ count })}>
|
<button key={count} type="button" className={settings.count === count ? "selected" : ""} onClick={() => onPatch({ count })}>
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
.image-generator-floating-composer {
|
.image-generator-floating-composer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 36;
|
z-index: 36;
|
||||||
min-height: 188px;
|
min-height: 150px;
|
||||||
border: 1px solid #cbd2dc;
|
overflow: hidden;
|
||||||
border-radius: 28px;
|
border: 0.5px solid #d7d9de;
|
||||||
|
border-radius: 24px;
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: rgba(255, 255, 255, 0.98);
|
||||||
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.14);
|
box-shadow: 0 18px 45px rgba(17, 24, 39, 0.13);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-reference-row {
|
.image-generator-reference-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
padding: 6px;
|
||||||
min-height: 72px;
|
gap: 8px;
|
||||||
margin: 14px 16px 0;
|
min-height: 68px;
|
||||||
|
margin: 8px 8px 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-reference-row::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-reference-tile,
|
.image-generator-reference-tile,
|
||||||
.image-generator-reference-preview {
|
.image-generator-reference-preview {
|
||||||
width: 72px;
|
width: 56px;
|
||||||
height: 72px;
|
height: 68px;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-reference-tile {
|
.image-generator-reference-tile {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
color: #a6abb5;
|
flex: 0 0 auto;
|
||||||
|
color: rgba(20, 20, 20, 0.46);
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-reference-tile span {
|
.image-generator-reference-tile span {
|
||||||
@@ -48,7 +59,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
@@ -77,23 +88,26 @@
|
|||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 92px;
|
height: auto;
|
||||||
margin: 10px 0 0;
|
min-height: 44px;
|
||||||
padding: 0 32px;
|
max-height: 176px;
|
||||||
|
margin: 2px 0 0;
|
||||||
|
padding: 1px 16px 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
resize: none;
|
resize: none;
|
||||||
color: #1f2937;
|
overflow-y: hidden;
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
caret-color: #111827;
|
caret-color: #111827;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 18px;
|
font-size: 14px;
|
||||||
font-weight: 520;
|
font-weight: 500;
|
||||||
line-height: 26px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-prompt-editor::placeholder {
|
.image-generator-prompt-editor::placeholder {
|
||||||
color: #a6acb6;
|
color: rgba(0, 0, 0, 0.3);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +125,8 @@
|
|||||||
.image-generator-composer-toolbar {
|
.image-generator-composer-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 4px;
|
||||||
padding: 8px 14px 13px 28px;
|
padding: 4px 8px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-settings-chip,
|
.image-generator-settings-chip,
|
||||||
@@ -121,17 +135,21 @@
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
color: #222831;
|
color: #222831;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
font-size: 15px;
|
font-size: 13px;
|
||||||
font-weight: 560;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-settings-chip {
|
.image-generator-settings-chip {
|
||||||
padding: 0 8px;
|
height: 32px;
|
||||||
|
max-width: 220px;
|
||||||
|
padding: 0 8px 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-settings-chip:hover,
|
.image-generator-settings-chip:hover,
|
||||||
@@ -150,11 +168,13 @@
|
|||||||
|
|
||||||
.image-generator-composer-spacer {
|
.image-generator-composer-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-model-button {
|
.image-generator-model-button {
|
||||||
max-width: 162px;
|
height: 32px;
|
||||||
padding: 0 10px;
|
max-width: 140px;
|
||||||
|
padding: 0 6px 0 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -166,9 +186,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.image-generator-send-button {
|
.image-generator-send-button {
|
||||||
min-width: 58px;
|
height: 32px;
|
||||||
|
min-width: 56px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 18px;
|
padding: 0 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #222;
|
background: #222;
|
||||||
font-weight: 760;
|
font-weight: 760;
|
||||||
@@ -179,3 +200,144 @@
|
|||||||
background: #efefef;
|
background: #efefef;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-settings-popover.image-generator-settings-popover-panel {
|
||||||
|
width: min(312px, calc(100vw - 24px));
|
||||||
|
max-height: min(480px, calc(100vh - 40px));
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 0.5px solid #d9dce2;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 14px 36px rgba(17, 24, 39, 0.14);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(16, 24, 40, 0.28) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel::-webkit-scrollbar-thumb {
|
||||||
|
min-height: 42px;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(16, 24, 40, 0.28);
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(16, 24, 40, 0.38);
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-settings-panel {
|
||||||
|
gap: 16px;
|
||||||
|
padding: 14px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-settings-panel h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-settings-panel section {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-settings-panel h4 {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-quality-control {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-quality-control button,
|
||||||
|
.image-generator-settings-popover-panel .composer-count-grid button {
|
||||||
|
height: 32px;
|
||||||
|
border: 1px solid #d8dbe1;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff;
|
||||||
|
color: #262b34;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-quality-control button.selected {
|
||||||
|
border-color: #2d3036;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-dimensions {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-dimensions label {
|
||||||
|
height: 32px;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-dimensions input {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-dimensions i {
|
||||||
|
width: 32px;
|
||||||
|
color: #7c828c;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-ratio-grid,
|
||||||
|
.image-generator-settings-popover-panel .composer-count-grid {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-ratio-grid button {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
height: 72px;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #262b34;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .composer-ratio-grid button.selected,
|
||||||
|
.image-generator-settings-popover-panel .composer-count-grid button.selected {
|
||||||
|
border-color: #d3d6dd;
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .ratio-glyph {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: #525862;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .ratio-glyph.wide {
|
||||||
|
width: 24px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-settings-popover-panel .ratio-glyph.portrait {
|
||||||
|
width: 16px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { AgentContent } from "@/domain/design";
|
|||||||
import { useI18n } from "@/i18n/i18n";
|
import { useI18n } from "@/i18n/i18n";
|
||||||
import {
|
import {
|
||||||
ComposerSettingsPanel,
|
ComposerSettingsPanel,
|
||||||
|
composerImageSize,
|
||||||
composerRatioOptions,
|
composerRatioOptions,
|
||||||
type CanvasComposerModel,
|
type CanvasComposerModel,
|
||||||
type ComposerImageRatio,
|
type ComposerImageRatio,
|
||||||
@@ -34,12 +35,12 @@ type ImageGeneratorFloatingComposerProps = {
|
|||||||
onSubmit: (contents: AgentContent[], reset: () => void) => void;
|
onSubmit: (contents: AgentContent[], reset: () => void) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const horizontalInset = 36;
|
const horizontalInset = 24;
|
||||||
const preferredWidth = 860;
|
const preferredWidth = 536;
|
||||||
const minimumWidth = 520;
|
const minimumWidth = 320;
|
||||||
const verticalInset = 18;
|
const verticalInset = 18;
|
||||||
const estimatedComposerHeight = 210;
|
const estimatedComposerHeight = 150;
|
||||||
const composerGap = 18;
|
const composerGap = 16;
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
@@ -85,8 +86,9 @@ function composerStyle(bounds: NodeScreenBounds, stageWidth: number, stageHeight
|
|||||||
|
|
||||||
function imageSettingsSummary(settings: ComposerImageSettings, t: (key: string) => string) {
|
function imageSettingsSummary(settings: ComposerImageSettings, t: (key: string) => string) {
|
||||||
const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio;
|
const ratioLabel = composerRatioOptions.find((option) => option.id === settings.ratio)?.label ?? settings.ratio;
|
||||||
|
const sizeLabel = settings.ratio === "auto" ? composerImageSize(settings).replace("x", "×") : ratioLabel;
|
||||||
const countLabel = t("imageCountUnit").replace("{count}", String(settings.count));
|
const countLabel = t("imageCountUnit").replace("{count}", String(settings.count));
|
||||||
return `${t(`quality${settings.quality[0].toUpperCase()}${settings.quality.slice(1)}`)} · ${ratioLabel} · ${countLabel}`;
|
return `${t(`quality${settings.quality[0].toUpperCase()}${settings.quality.slice(1)}`)} · ${sizeLabel} · ${countLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageGeneratorFloatingComposer({
|
export function ImageGeneratorFloatingComposer({
|
||||||
@@ -106,6 +108,7 @@ export function ImageGeneratorFloatingComposer({
|
|||||||
}: ImageGeneratorFloatingComposerProps) {
|
}: ImageGeneratorFloatingComposerProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const rootRef = useRef<HTMLDivElement | null>(null);
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const promptEditorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [references, setReferences] = useState<UploadedReferenceImage[]>([]);
|
const [references, setReferences] = useState<UploadedReferenceImage[]>([]);
|
||||||
@@ -126,6 +129,18 @@ export function ImageGeneratorFloatingComposer({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const editor = promptEditorRef.current;
|
||||||
|
if (!editor) return;
|
||||||
|
const style = window.getComputedStyle(editor);
|
||||||
|
const minHeight = Number.parseFloat(style.minHeight) || 44;
|
||||||
|
const maxHeight = Number.parseFloat(style.maxHeight) || 176;
|
||||||
|
editor.style.height = "auto";
|
||||||
|
const nextHeight = Math.min(Math.max(editor.scrollHeight, minHeight), maxHeight);
|
||||||
|
editor.style.height = `${nextHeight}px`;
|
||||||
|
editor.style.overflowY = editor.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||||
|
}, [promptValue]);
|
||||||
|
|
||||||
const patchSettings = (patch: Partial<ComposerImageSettings>) => {
|
const patchSettings = (patch: Partial<ComposerImageSettings>) => {
|
||||||
onSettingsChange({ ...settings, ...patch });
|
onSettingsChange({ ...settings, ...patch });
|
||||||
};
|
};
|
||||||
@@ -209,6 +224,7 @@ export function ImageGeneratorFloatingComposer({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={promptEditorRef}
|
||||||
value={promptValue}
|
value={promptValue}
|
||||||
placeholder={t("canvasPromptPlaceholder")}
|
placeholder={t("canvasPromptPlaceholder")}
|
||||||
aria-label={t("canvasPromptPlaceholder")}
|
aria-label={t("canvasPromptPlaceholder")}
|
||||||
@@ -225,8 +241,8 @@ export function ImageGeneratorFloatingComposer({
|
|||||||
{settingsOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
{settingsOpen ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||||
</button>
|
</button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="composer-settings-popover" align="start" sideOffset={12}>
|
<PopoverContent className="composer-settings-popover image-generator-settings-popover-panel" align="start" sideOffset={12}>
|
||||||
<ComposerSettingsPanel settings={settings} onPatch={patchSettings} onSelectRatio={selectRatio} />
|
<ComposerSettingsPanel settings={settings} onPatch={patchSettings} onSelectRatio={selectRatio} title={t("imageSettings")} />
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function CanvasSelectionFrame({
|
|||||||
title: string;
|
title: string;
|
||||||
size: string;
|
size: string;
|
||||||
Icon: ComponentType<{ size?: number }>;
|
Icon: ComponentType<{ size?: number }>;
|
||||||
variant?: "frame" | "pen" | "text" | "shape" | "line-shape";
|
variant?: "frame" | "image-generator" | "pen" | "text" | "shape" | "line-shape";
|
||||||
selectionScale: number;
|
selectionScale: number;
|
||||||
transform: NodeTransform;
|
transform: NodeTransform;
|
||||||
onResizePointerDown: (event: ReactPointerEvent<HTMLButtonElement>, handle: ResizeHandle) => void;
|
onResizePointerDown: (event: ReactPointerEvent<HTMLButtonElement>, handle: ResizeHandle) => void;
|
||||||
@@ -41,7 +41,10 @@ export function CanvasSelectionFrame({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`moteva-selection-frame ${variant === "frame" ? "frame-selection-frame" : ""} ${variant === "pen" ? "pen-stroke-selection-frame" : ""} ${variant === "shape" ? "shape-selection-frame" : ""}`} style={{ "--selection-scale": selectionScale } as CSSProperties}>
|
<div
|
||||||
|
className={`moteva-selection-frame ${variant === "frame" ? "frame-selection-frame" : ""} ${variant === "image-generator" ? "image-generator-selection-frame" : ""} ${variant === "pen" ? "pen-stroke-selection-frame" : ""} ${variant === "shape" ? "shape-selection-frame" : ""}`}
|
||||||
|
style={{ "--selection-scale": selectionScale } as CSSProperties}
|
||||||
|
>
|
||||||
<div className="moteva-selection-meta">
|
<div className="moteva-selection-meta">
|
||||||
<span className="moteva-selection-title-wrap">
|
<span className="moteva-selection-title-wrap">
|
||||||
<Icon size={17} />
|
<Icon size={17} />
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export function useGenerationStream({
|
|||||||
const toUserMessageText = useUserMessage();
|
const toUserMessageText = useUserMessage();
|
||||||
const closeAgentEventsRef = useRef<(() => void) | null>(null);
|
const closeAgentEventsRef = useRef<(() => void) | null>(null);
|
||||||
const closeAgentProjectPollRef = useRef<(() => void) | null>(null);
|
const closeAgentProjectPollRef = useRef<(() => void) | null>(null);
|
||||||
const closeImageGeneratorEventsRef = useRef<(() => void) | null>(null);
|
const closeImageGeneratorEventsRef = useRef<Map<string, () => void>>(new Map());
|
||||||
const closeCanvasActionEventsRef = useRef<Map<string, () => void>>(new Map());
|
const closeCanvasActionEventsRef = useRef<Map<string, () => void>>(new Map());
|
||||||
const agentStreamingProjectIdRef = useRef<string | null>(null);
|
const agentStreamingProjectIdRef = useRef<string | null>(null);
|
||||||
const imageGeneratorStreamingProjectIdRef = useRef<string | null>(null);
|
const imageGeneratorStreamingProjectIdRef = useRef<Set<string>>(new Set());
|
||||||
const canvasActionStreamingProjectIdRef = useRef<Set<string>>(new Set());
|
const canvasActionStreamingProjectIdRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
const completeCanvasActionStream = useCallback(
|
const completeCanvasActionStream = useCallback(
|
||||||
@@ -49,10 +49,19 @@ export function useGenerationStream({
|
|||||||
[setCanvasActionGenerating]
|
[setCanvasActionGenerating]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const completeImageGeneratorStream = useCallback(
|
||||||
|
(streamKey: string) => {
|
||||||
|
closeImageGeneratorEventsRef.current.delete(streamKey);
|
||||||
|
imageGeneratorStreamingProjectIdRef.current.delete(streamKey);
|
||||||
|
setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
|
||||||
|
},
|
||||||
|
[setImageGeneratorGenerating]
|
||||||
|
);
|
||||||
|
|
||||||
const completeChannel = useCallback(
|
const completeChannel = useCallback(
|
||||||
(channel: GenerationChannel) => {
|
(channel: GenerationChannel) => {
|
||||||
if (channel === "image-generator") {
|
if (channel === "image-generator") {
|
||||||
setImageGeneratorGenerating(false);
|
setImageGeneratorGenerating(closeImageGeneratorEventsRef.current.size > 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setAgentGenerating(false);
|
setAgentGenerating(false);
|
||||||
@@ -66,12 +75,12 @@ export function useGenerationStream({
|
|||||||
if (!threadId) return;
|
if (!threadId) return;
|
||||||
|
|
||||||
if (channel === "canvas-action") {
|
if (channel === "canvas-action") {
|
||||||
if (closeCanvasActionEventsRef.current.has(streamKey)) return;
|
closeCanvasActionEventsRef.current.get(streamKey)?.();
|
||||||
setCanvasActionGenerating(true);
|
setCanvasActionGenerating(true);
|
||||||
canvasActionStreamingProjectIdRef.current.add(streamKey);
|
canvasActionStreamingProjectIdRef.current.add(streamKey);
|
||||||
} else {
|
} else {
|
||||||
closeImageGeneratorEventsRef.current?.();
|
closeImageGeneratorEventsRef.current.get(streamKey)?.();
|
||||||
imageGeneratorStreamingProjectIdRef.current = streamKey;
|
imageGeneratorStreamingProjectIdRef.current.add(streamKey);
|
||||||
setImageGeneratorGenerating(true);
|
setImageGeneratorGenerating(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +99,7 @@ export function useGenerationStream({
|
|||||||
completeCanvasActionStream(streamKey);
|
completeCanvasActionStream(streamKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
completeChannel(channel);
|
completeImageGeneratorStream(streamKey);
|
||||||
imageGeneratorStreamingProjectIdRef.current = null;
|
|
||||||
closeImageGeneratorEventsRef.current = null;
|
|
||||||
};
|
};
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -128,11 +135,11 @@ export function useGenerationStream({
|
|||||||
if (channel === "canvas-action") {
|
if (channel === "canvas-action") {
|
||||||
closeCanvasActionEventsRef.current.set(streamKey, close);
|
closeCanvasActionEventsRef.current.set(streamKey, close);
|
||||||
} else {
|
} else {
|
||||||
closeImageGeneratorEventsRef.current = close;
|
closeImageGeneratorEventsRef.current.set(streamKey, close);
|
||||||
}
|
}
|
||||||
poll();
|
poll();
|
||||||
},
|
},
|
||||||
[applyProject, completeCanvasActionStream, completeChannel, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating]
|
[applyProject, completeCanvasActionStream, completeImageGeneratorStream, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating]
|
||||||
);
|
);
|
||||||
|
|
||||||
const startAgentProjectPolling = useCallback(
|
const startAgentProjectPolling = useCallback(
|
||||||
@@ -253,14 +260,24 @@ export function useGenerationStream({
|
|||||||
(nextProjectId: string, payload: AgentChatPayload, channel: GenerationChannel = "agent") => {
|
(nextProjectId: string, payload: AgentChatPayload, channel: GenerationChannel = "agent") => {
|
||||||
const streamKey = generationStreamKey(nextProjectId, payload.threadId);
|
const streamKey = generationStreamKey(nextProjectId, payload.threadId);
|
||||||
if (channel !== "agent") {
|
if (channel !== "agent") {
|
||||||
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef);
|
if (channel === "canvas-action") {
|
||||||
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
|
closeCanvasActionEventsRef.current.get(streamKey)?.();
|
||||||
closeRef.current?.();
|
setCanvasActionGenerating(true);
|
||||||
|
canvasActionStreamingProjectIdRef.current.add(streamKey);
|
||||||
|
} else {
|
||||||
|
closeImageGeneratorEventsRef.current.get(streamKey)?.();
|
||||||
|
setImageGeneratorGenerating(true);
|
||||||
|
imageGeneratorStreamingProjectIdRef.current.add(streamKey);
|
||||||
|
}
|
||||||
let closed = false;
|
let closed = false;
|
||||||
closeRef.current = () => {
|
const close = () => {
|
||||||
closed = true;
|
closed = true;
|
||||||
};
|
};
|
||||||
streamingRef.current = streamKey;
|
if (channel === "canvas-action") {
|
||||||
|
closeCanvasActionEventsRef.current.set(streamKey, close);
|
||||||
|
} else {
|
||||||
|
closeImageGeneratorEventsRef.current.set(streamKey, close);
|
||||||
|
}
|
||||||
void designGateway
|
void designGateway
|
||||||
.agentChat(nextProjectId, payload)
|
.agentChat(nextProjectId, payload)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@@ -270,10 +287,12 @@ export function useGenerationStream({
|
|||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
completeChannel(channel);
|
if (channel === "canvas-action") {
|
||||||
|
completeCanvasActionStream(streamKey);
|
||||||
|
} else {
|
||||||
|
completeImageGeneratorStream(streamKey);
|
||||||
|
}
|
||||||
setError(toUserMessageText(error, "generateError"));
|
setError(toUserMessageText(error, "generateError"));
|
||||||
streamingRef.current = null;
|
|
||||||
closeRef.current = null;
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -319,7 +338,7 @@ export function useGenerationStream({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[appendThinkingMessage, applyProject, completeChannel, toUserMessageText, messages.generateError, messages.generateTimeout, setError, startAgentProjectPolling, startGeneratorTaskPolling]
|
[appendThinkingMessage, applyProject, completeCanvasActionStream, completeChannel, completeImageGeneratorStream, toUserMessageText, messages.generateError, messages.generateTimeout, setCanvasActionGenerating, setError, setImageGeneratorGenerating, startAgentProjectPolling, startGeneratorTaskPolling]
|
||||||
);
|
);
|
||||||
|
|
||||||
const stopGenerationStream = useCallback(
|
const stopGenerationStream = useCallback(
|
||||||
@@ -331,6 +350,13 @@ export function useGenerationStream({
|
|||||||
setCanvasActionGenerating(false);
|
setCanvasActionGenerating(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (channel === "image-generator") {
|
||||||
|
closeImageGeneratorEventsRef.current.forEach((close) => close());
|
||||||
|
closeImageGeneratorEventsRef.current.clear();
|
||||||
|
imageGeneratorStreamingProjectIdRef.current.clear();
|
||||||
|
setImageGeneratorGenerating(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef);
|
const closeRef = generationCloseRef(channel, closeAgentEventsRef, closeImageGeneratorEventsRef, closeCanvasActionEventsRef);
|
||||||
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
|
const streamingRef = generationStreamingRef(channel, agentStreamingProjectIdRef, imageGeneratorStreamingProjectIdRef, canvasActionStreamingProjectIdRef);
|
||||||
closeRef.current?.();
|
closeRef.current?.();
|
||||||
@@ -342,21 +368,21 @@ export function useGenerationStream({
|
|||||||
streamingRef.current = null;
|
streamingRef.current = null;
|
||||||
completeChannel(channel);
|
completeChannel(channel);
|
||||||
},
|
},
|
||||||
[completeChannel, setCanvasActionGenerating]
|
[completeChannel, setCanvasActionGenerating, setImageGeneratorGenerating]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
closeAgentEventsRef.current?.();
|
closeAgentEventsRef.current?.();
|
||||||
closeAgentProjectPollRef.current?.();
|
closeAgentProjectPollRef.current?.();
|
||||||
closeImageGeneratorEventsRef.current?.();
|
closeImageGeneratorEventsRef.current.forEach((close) => close());
|
||||||
closeCanvasActionEventsRef.current.forEach((close) => close());
|
closeCanvasActionEventsRef.current.forEach((close) => close());
|
||||||
closeAgentEventsRef.current = null;
|
closeAgentEventsRef.current = null;
|
||||||
closeAgentProjectPollRef.current = null;
|
closeAgentProjectPollRef.current = null;
|
||||||
closeImageGeneratorEventsRef.current = null;
|
closeImageGeneratorEventsRef.current.clear();
|
||||||
closeCanvasActionEventsRef.current.clear();
|
closeCanvasActionEventsRef.current.clear();
|
||||||
agentStreamingProjectIdRef.current = null;
|
agentStreamingProjectIdRef.current = null;
|
||||||
imageGeneratorStreamingProjectIdRef.current = null;
|
imageGeneratorStreamingProjectIdRef.current.clear();
|
||||||
canvasActionStreamingProjectIdRef.current.clear();
|
canvasActionStreamingProjectIdRef.current.clear();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -375,20 +401,18 @@ export function useGenerationStream({
|
|||||||
function generationCloseRef(
|
function generationCloseRef(
|
||||||
channel: GenerationChannel,
|
channel: GenerationChannel,
|
||||||
agentRef: RefObject<(() => void) | null>,
|
agentRef: RefObject<(() => void) | null>,
|
||||||
imageGeneratorRef: RefObject<(() => void) | null>,
|
_imageGeneratorRef: RefObject<Map<string, () => void>>,
|
||||||
_canvasActionRef: RefObject<Map<string, () => void>>
|
_canvasActionRef: RefObject<Map<string, () => void>>
|
||||||
) {
|
) {
|
||||||
if (channel === "image-generator") return imageGeneratorRef;
|
|
||||||
return agentRef;
|
return agentRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generationStreamingRef(
|
function generationStreamingRef(
|
||||||
channel: GenerationChannel,
|
channel: GenerationChannel,
|
||||||
agentRef: RefObject<string | null>,
|
agentRef: RefObject<string | null>,
|
||||||
imageGeneratorRef: RefObject<string | null>,
|
_imageGeneratorRef: RefObject<Set<string>>,
|
||||||
_canvasActionRef: RefObject<Set<string>>
|
_canvasActionRef: RefObject<Set<string>>
|
||||||
) {
|
) {
|
||||||
if (channel === "image-generator") return imageGeneratorRef;
|
|
||||||
return agentRef;
|
return agentRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import type { ComposerImageSettings } from "@/ui/components/CanvasAgentComposer";
|
import type { ComposerImageSettings } from "@/ui/components/CanvasAgentComposer";
|
||||||
|
import type { CanvasNode } from "@/domain/design";
|
||||||
|
|
||||||
type StageSize = {
|
type StageSize = {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ImageGeneratorTargetSize = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const targetMetadataKey = "imageGeneratorTarget";
|
||||||
|
|
||||||
export function imageGeneratorPlaceholderSize(settings: ComposerImageSettings, viewportScale: number, stage: StageSize) {
|
export function imageGeneratorPlaceholderSize(settings: ComposerImageSettings, viewportScale: number, stage: StageSize) {
|
||||||
const width = Math.max(1, settings.width);
|
const width = Math.max(1, settings.width);
|
||||||
const height = Math.max(1, settings.height);
|
const height = Math.max(1, settings.height);
|
||||||
@@ -26,6 +34,67 @@ export function imageGeneratorPlaceholderSize(settings: ComposerImageSettings, v
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function imageGeneratorTargetSizeFromSettings(settings: ComposerImageSettings): ImageGeneratorTargetSize {
|
||||||
|
return {
|
||||||
|
width: positiveInteger(settings.width, 1024),
|
||||||
|
height: positiveInteger(settings.height, 1024)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function imageGeneratorTargetSizeFromNode(node: CanvasNode): ImageGeneratorTargetSize {
|
||||||
|
const metadata = parseImageGeneratorTargetMetadata(node.imageAdjustments);
|
||||||
|
if (metadata) {
|
||||||
|
return {
|
||||||
|
width: positiveInteger(metadata.width, 1024),
|
||||||
|
height: positiveInteger(metadata.height, 1024)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ratio = Math.max(node.width, 1) / Math.max(node.height, 1);
|
||||||
|
if (ratio >= 1) {
|
||||||
|
return {
|
||||||
|
width: positiveInteger(1024 * ratio, 1024),
|
||||||
|
height: 1024
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: 1024,
|
||||||
|
height: positiveInteger(1024 / ratio, 1024)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeImageGeneratorTargetSize(settings: ComposerImageSettings, previous?: string): string {
|
||||||
|
const metadata = parseJsonObject(previous);
|
||||||
|
metadata[targetMetadataKey] = imageGeneratorTargetSizeFromSettings(settings);
|
||||||
|
return JSON.stringify(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(Math.max(value, min), max);
|
return Math.min(Math.max(value, min), max);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseImageGeneratorTargetMetadata(value?: string | null): Partial<ImageGeneratorTargetSize> | null {
|
||||||
|
const metadata = parseJsonObject(value)[targetMetadataKey];
|
||||||
|
if (!metadata || typeof metadata !== "object") return null;
|
||||||
|
const source = metadata as Partial<Record<keyof ImageGeneratorTargetSize, unknown>>;
|
||||||
|
return {
|
||||||
|
width: Number(source.width),
|
||||||
|
height: Number(source.height)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonObject(value?: string | null): Record<string, unknown> {
|
||||||
|
if (!value) return {};
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as unknown;
|
||||||
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...(parsed as Record<string, unknown>) };
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function positiveInteger(value: unknown, fallback: number) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (!Number.isFinite(numeric) || numeric <= 0) return Math.max(1, Math.round(fallback));
|
||||||
|
return Math.max(1, Math.round(numeric));
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { useCanvasDocument } from "./canvas/hooks/useCanvasDocument";
|
|||||||
import { useCanvasPersistence } from "./canvas/hooks/useCanvasPersistence";
|
import { useCanvasPersistence } from "./canvas/hooks/useCanvasPersistence";
|
||||||
import { useCanvasSelection } from "./canvas/hooks/useCanvasSelection";
|
import { useCanvasSelection } from "./canvas/hooks/useCanvasSelection";
|
||||||
import { useGenerationStream, type GenerationChannel } from "./canvas/hooks/useGenerationStream";
|
import { useGenerationStream, type GenerationChannel } from "./canvas/hooks/useGenerationStream";
|
||||||
import { imageGeneratorPlaceholderSize } from "./canvas/imageGeneratorSizing";
|
import { imageGeneratorPlaceholderSize, imageGeneratorTargetSizeFromNode, imageGeneratorTargetSizeFromSettings, serializeImageGeneratorTargetSize, type ImageGeneratorTargetSize } from "./canvas/imageGeneratorSizing";
|
||||||
import { canvasNodeToReferenceImage, isAssetReferencedByCanvas, isCanvasNodeReferenceImage } from "./canvas/nodeReferenceImages";
|
import { canvasNodeToReferenceImage, isAssetReferencedByCanvas, isCanvasNodeReferenceImage } from "./canvas/nodeReferenceImages";
|
||||||
import { canvasNodeSvg, exportCanvasNode, exportImageNode, exportTextNode, imageNodeRasterBlob, svgToRasterBlob } from "./canvas/nodeExport";
|
import { canvasNodeSvg, exportCanvasNode, exportImageNode, exportTextNode, imageNodeRasterBlob, svgToRasterBlob } from "./canvas/nodeExport";
|
||||||
import { beginPenStroke, createPenStrokeNode, PenStrokeDraftOverlay, PenToolControls, type PenDraft, type PenSettings } from "@/ui/pages/CanvasWorkspace/canvas/PenTool";
|
import { beginPenStroke, createPenStrokeNode, PenStrokeDraftOverlay, PenToolControls, type PenDraft, type PenSettings } from "@/ui/pages/CanvasWorkspace/canvas/PenTool";
|
||||||
@@ -707,12 +707,13 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
}, [editingTextNodeId, nodes]);
|
}, [editingTextNodeId, nodes]);
|
||||||
|
|
||||||
const applyProject = useCallback((nextProject: Project, options: { preserveMessages?: boolean } = {}) => {
|
const applyProject = useCallback((nextProject: Project, options: { preserveMessages?: boolean } = {}) => {
|
||||||
applyProjectDocument(nextProject);
|
const mergedProject = preserveLocalDraftImageGenerators(nextProject, nodesRef.current);
|
||||||
|
applyProjectDocument(mergedProject);
|
||||||
if (!options.preserveMessages) {
|
if (!options.preserveMessages) {
|
||||||
setMessages(visibleAgentMessagesForThread(nextProject, imageGeneratorThreadIdsRef.current, selectedAgentThreadIdRef.current));
|
setMessages(visibleAgentMessagesForThread(mergedProject, imageGeneratorThreadIdsRef.current, selectedAgentThreadIdRef.current));
|
||||||
}
|
}
|
||||||
setEditingTextNodeId(null);
|
setEditingTextNodeId(null);
|
||||||
}, [applyProjectDocument]);
|
}, [applyProjectDocument, nodesRef]);
|
||||||
|
|
||||||
const appendThinkingMessage = useCallback((message: AgentMessage) => {
|
const appendThinkingMessage = useCallback((message: AgentMessage) => {
|
||||||
if (isImageGeneratorThreadMessage(message, imageGeneratorThreadIdsRef.current)) return;
|
if (isImageGeneratorThreadMessage(message, imageGeneratorThreadIdsRef.current)) return;
|
||||||
@@ -937,7 +938,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
const selectedImageNode = selectedControlNode && isRealCanvasImageNode(selectedControlNode) ? selectedControlNode : null;
|
const selectedImageNode = selectedControlNode && isRealCanvasImageNode(selectedControlNode) ? selectedControlNode : null;
|
||||||
const selectedImageNodeBusy = selectedImageNode ? isImageActionBusy(selectedImageNode) : false;
|
const selectedImageNodeBusy = selectedImageNode ? isImageActionBusy(selectedImageNode) : false;
|
||||||
const selectedImageGeneratorNode = selectedControlNode?.type === "image" && selectedControlNode.layerRole === "image-generator" ? selectedControlNode : null;
|
const selectedImageGeneratorNode = selectedControlNode?.type === "image" && selectedControlNode.layerRole === "image-generator" ? selectedControlNode : null;
|
||||||
const selectedImageGeneratorBusy = imageGeneratorGenerating || selectedImageGeneratorNode?.status === "generating";
|
const selectedImageGeneratorBusy = selectedImageGeneratorNode?.status === "generating";
|
||||||
const selectedPenStrokeNode = selectedControlNode && isCanvasPenStrokeNode(selectedControlNode) ? selectedControlNode : null;
|
const selectedPenStrokeNode = selectedControlNode && isCanvasPenStrokeNode(selectedControlNode) ? selectedControlNode : null;
|
||||||
const selectedFrameNode = selectedControlNode?.type === "frame" && selectedControlNode.layerRole === "manual-frame" ? selectedControlNode : null;
|
const selectedFrameNode = selectedControlNode?.type === "frame" && selectedControlNode.layerRole === "manual-frame" ? selectedControlNode : null;
|
||||||
const selectedShapeNode = selectedControlNode && isShapeNode(selectedControlNode) ? selectedControlNode : null;
|
const selectedShapeNode = selectedControlNode && isShapeNode(selectedControlNode) ? selectedControlNode : null;
|
||||||
@@ -962,6 +963,16 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
height: selectedImageGeneratorNode.height * viewport.k
|
height: selectedImageGeneratorNode.height * viewport.k
|
||||||
};
|
};
|
||||||
}, [selectedImageGeneratorNode, viewport]);
|
}, [selectedImageGeneratorNode, viewport]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedImageGeneratorNode) return;
|
||||||
|
const targetSize = imageGeneratorTargetSizeFromNode(selectedImageGeneratorNode);
|
||||||
|
setComposerImageSettings((current) => {
|
||||||
|
if (current.width === targetSize.width && current.height === targetSize.height) return current;
|
||||||
|
return { ...current, width: targetSize.width, height: targetSize.height, ratio: "auto" };
|
||||||
|
});
|
||||||
|
}, [selectedImageGeneratorNode?.id]);
|
||||||
|
|
||||||
const selectedFrameBounds = useMemo<NodeScreenBounds | null>(() => {
|
const selectedFrameBounds = useMemo<NodeScreenBounds | null>(() => {
|
||||||
if (!selectedFrameNode) return null;
|
if (!selectedFrameNode) return null;
|
||||||
return {
|
return {
|
||||||
@@ -2105,6 +2116,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
const deleteSelectedNodes = () => {
|
const deleteSelectedNodes = () => {
|
||||||
const ids = new Set(selectedNodes.filter((node) => !lockedNodeIds.has(node.id) && !hiddenNodeIds.has(node.id)).map((node) => node.id));
|
const ids = new Set(selectedNodes.filter((node) => !lockedNodeIds.has(node.id) && !hiddenNodeIds.has(node.id)).map((node) => node.id));
|
||||||
if (ids.size === 0) return;
|
if (ids.size === 0) return;
|
||||||
|
nodesRef.current = removeNodesById(nodesRef.current, ids);
|
||||||
setNodes((current) => removeNodesById(current, ids));
|
setNodes((current) => removeNodesById(current, ids));
|
||||||
setHiddenNodeIds((current) => new Set(Array.from(current).filter((nodeId) => !ids.has(nodeId))));
|
setHiddenNodeIds((current) => new Set(Array.from(current).filter((nodeId) => !ids.has(nodeId))));
|
||||||
setLockedNodeIds((current) => new Set(Array.from(current).filter((nodeId) => !ids.has(nodeId))));
|
setLockedNodeIds((current) => new Set(Array.from(current).filter((nodeId) => !ids.has(nodeId))));
|
||||||
@@ -2227,6 +2239,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
const primary = isPrimaryShortcut(event);
|
const primary = isPrimaryShortcut(event);
|
||||||
const selected = selectedNode;
|
const selected = selectedNode;
|
||||||
const selectedHandlers = selected && !lockedNodeIds.has(selected.id) && canUseNodeContextMenu(selected) ? createNodeContextMenuHandlers(selected) : null;
|
const selectedHandlers = selected && !lockedNodeIds.has(selected.id) && canUseNodeContextMenu(selected) ? createNodeContextMenuHandlers(selected) : null;
|
||||||
|
const selectedCanDelete = selected && !lockedNodeIds.has(selected.id) && !hiddenNodeIds.has(selected.id);
|
||||||
const run = (action: (() => void) | undefined, prevent = true) => {
|
const run = (action: (() => void) | undefined, prevent = true) => {
|
||||||
if (!action) return false;
|
if (!action) return false;
|
||||||
if (prevent) event.preventDefault();
|
if (prevent) event.preventDefault();
|
||||||
@@ -2276,7 +2289,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
if (primary && event.shiftKey && key === "h") return void run(selectedHandlers?.toggleHidden);
|
if (primary && event.shiftKey && key === "h") return void run(selectedHandlers?.toggleHidden);
|
||||||
if (primary && event.shiftKey && key === "l") return void run(selectedHandlers?.toggleLocked);
|
if (primary && event.shiftKey && key === "l") return void run(selectedHandlers?.toggleLocked);
|
||||||
if ((event.key === "Backspace" || event.key === "Delete") && selectedNodes.length > 1) return void run(deleteSelectedNodes);
|
if ((event.key === "Backspace" || event.key === "Delete") && selectedNodes.length > 1) return void run(deleteSelectedNodes);
|
||||||
if ((event.key === "Backspace" || event.key === "Delete") && selected) return void run(selectedHandlers?.deleteNode);
|
if ((event.key === "Backspace" || event.key === "Delete") && selected) return void run(selectedHandlers?.deleteNode ?? (selectedCanDelete ? () => removeNode(selected.id) : undefined));
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
@@ -2335,18 +2348,20 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
};
|
};
|
||||||
const origin = point ?? center;
|
const origin = point ?? center;
|
||||||
const size = imageGeneratorPlaceholderSize(composerImageSettings, viewport.k, { width: stageWidth, height: stageHeight });
|
const size = imageGeneratorPlaceholderSize(composerImageSettings, viewport.k, { width: stageWidth, height: stageHeight });
|
||||||
|
const position = point ? { x: origin.x - size.width / 2, y: origin.y - size.height / 2 } : nextImageGeneratorPosition(origin, size, nodesRef.current, viewport.k);
|
||||||
const node: CanvasNode = {
|
const node: CanvasNode = {
|
||||||
id: cryptoFallbackId(),
|
id: cryptoFallbackId(),
|
||||||
type: "image",
|
type: "image",
|
||||||
title: t("imageGeneratorNode"),
|
title: t("imageGeneratorNode"),
|
||||||
x: origin.x - size.width / 2,
|
x: position.x,
|
||||||
y: origin.y - size.height / 2,
|
y: position.y,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height,
|
height: size.height,
|
||||||
content: "",
|
content: "",
|
||||||
tone: "image-generator",
|
tone: "image-generator",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
layerRole: "image-generator"
|
layerRole: "image-generator",
|
||||||
|
imageAdjustments: serializeImageGeneratorTargetSize(composerImageSettings)
|
||||||
};
|
};
|
||||||
setComposerMode("image");
|
setComposerMode("image");
|
||||||
setActiveTool("select");
|
setActiveTool("select");
|
||||||
@@ -2375,7 +2390,8 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
x: centerX - size.width / 2,
|
x: centerX - size.width / 2,
|
||||||
y: centerY - size.height / 2,
|
y: centerY - size.height / 2,
|
||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height
|
height: size.height,
|
||||||
|
imageAdjustments: serializeImageGeneratorTargetSize(settings, node.imageAdjustments)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
nodesRef.current = next;
|
nodesRef.current = next;
|
||||||
@@ -2577,7 +2593,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
setNodes((current) => {
|
setNodes((current) => {
|
||||||
const next = current.map((node) =>
|
const next = current.map((node) =>
|
||||||
node.id === selectedGeneratorNode.id && node.type === "image" && node.layerRole === "image-generator"
|
node.id === selectedGeneratorNode.id && node.type === "image" && node.layerRole === "image-generator"
|
||||||
? { ...node, content: lockedPrompt, status: "generating" }
|
? { ...node, content: lockedPrompt, status: "generating", imageAdjustments: serializeImageGeneratorTargetSize(composerImageSettings, node.imageAdjustments) }
|
||||||
: node
|
: node
|
||||||
);
|
);
|
||||||
nodesRef.current = next;
|
nodesRef.current = next;
|
||||||
@@ -3437,6 +3453,12 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
const locked = lockedNodeIds.has(node.id);
|
const locked = lockedNodeIds.has(node.id);
|
||||||
const canOperateNode = !locked;
|
const canOperateNode = !locked;
|
||||||
const suppressSelectionFrame = (imageActionMode === "expand" || imageActionMode === "crop") && selectedImageNode?.id === node.id;
|
const suppressSelectionFrame = (imageActionMode === "expand" || imageActionMode === "crop") && selectedImageNode?.id === node.id;
|
||||||
|
const imageGeneratorTargetSize =
|
||||||
|
node.type === "image" && node.layerRole === "image-generator"
|
||||||
|
? node.id === selectedImageGeneratorNode?.id
|
||||||
|
? imageGeneratorTargetSizeFromSettings(composerImageSettings)
|
||||||
|
: imageGeneratorTargetSizeFromNode(node)
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<CanvasNodeView
|
<CanvasNodeView
|
||||||
key={node.id}
|
key={node.id}
|
||||||
@@ -3455,6 +3477,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
}}
|
}}
|
||||||
selectionScale={viewport.k}
|
selectionScale={viewport.k}
|
||||||
viewportScale={viewport.k}
|
viewportScale={viewport.k}
|
||||||
|
imageGeneratorTargetSize={imageGeneratorTargetSize}
|
||||||
hasClipboard
|
hasClipboard
|
||||||
contextMenuHandlers={canOperateNode && canUseNodeContextMenu(node) ? createNodeContextMenuHandlers(node) : null}
|
contextMenuHandlers={canOperateNode && canUseNodeContextMenu(node) ? createNodeContextMenuHandlers(node) : null}
|
||||||
onPointerDown={(event) => handleNodePointerDown(event, node)}
|
onPointerDown={(event) => handleNodePointerDown(event, node)}
|
||||||
@@ -3475,7 +3498,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
<FrameDraftOverlay draft={frameDraft} label={t("newFrameNode")} />
|
<FrameDraftOverlay draft={frameDraft} label={t("newFrameNode")} />
|
||||||
<ShapeDraftOverlay draft={shapeDraft} kind={selectedShapeKind} label={shapeTitle(selectedShapeKind, t)} />
|
<ShapeDraftOverlay draft={shapeDraft} kind={selectedShapeKind} label={shapeTitle(selectedShapeKind, t)} />
|
||||||
<PenStrokeDraftOverlay draft={penDraft} />
|
<PenStrokeDraftOverlay draft={penDraft} />
|
||||||
{selectedImageGeneratorBounds && (
|
{selectedImageGeneratorBounds && selectedImageGeneratorNode?.status === "draft" && (
|
||||||
<ImageGeneratorFloatingComposer
|
<ImageGeneratorFloatingComposer
|
||||||
bounds={selectedImageGeneratorBounds}
|
bounds={selectedImageGeneratorBounds}
|
||||||
stageWidth={canvasStageSize.width}
|
stageWidth={canvasStageSize.width}
|
||||||
@@ -3484,7 +3507,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
|||||||
selectedModel={selectedCanvasModel}
|
selectedModel={selectedCanvasModel}
|
||||||
settings={composerImageSettings}
|
settings={composerImageSettings}
|
||||||
generating={selectedImageGeneratorBusy}
|
generating={selectedImageGeneratorBusy}
|
||||||
lockedPrompt={selectedImageGeneratorNode?.status === "generating" ? cleanImageGeneratorPrompt(selectedImageGeneratorNode.content) : ""}
|
lockedPrompt=""
|
||||||
onSettingsChange={updateComposerImageSettings}
|
onSettingsChange={updateComposerImageSettings}
|
||||||
onModelChange={setSelectedCanvasModelId}
|
onModelChange={setSelectedCanvasModelId}
|
||||||
onUploadFile={uploadReferenceToImageGenerator}
|
onUploadFile={uploadReferenceToImageGenerator}
|
||||||
@@ -4006,6 +4029,7 @@ function CanvasNodeView({
|
|||||||
transform,
|
transform,
|
||||||
selectionScale,
|
selectionScale,
|
||||||
viewportScale,
|
viewportScale,
|
||||||
|
imageGeneratorTargetSize,
|
||||||
hasClipboard,
|
hasClipboard,
|
||||||
contextMenuHandlers,
|
contextMenuHandlers,
|
||||||
onPointerDown,
|
onPointerDown,
|
||||||
@@ -4027,6 +4051,7 @@ function CanvasNodeView({
|
|||||||
transform: NodeTransform;
|
transform: NodeTransform;
|
||||||
selectionScale: number;
|
selectionScale: number;
|
||||||
viewportScale: number;
|
viewportScale: number;
|
||||||
|
imageGeneratorTargetSize?: ImageGeneratorTargetSize;
|
||||||
hasClipboard: boolean;
|
hasClipboard: boolean;
|
||||||
contextMenuHandlers: NodeContextMenuHandlers | null;
|
contextMenuHandlers: NodeContextMenuHandlers | null;
|
||||||
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => void;
|
onPointerDown: (event: React.PointerEvent<HTMLDivElement>) => void;
|
||||||
@@ -4044,11 +4069,13 @@ function CanvasNodeView({
|
|||||||
const booleanShapeGroup = isBooleanShapeGroupNode(node);
|
const booleanShapeGroup = isBooleanShapeGroupNode(node);
|
||||||
const lineShapeNode = isLineShapeNode(node);
|
const lineShapeNode = isLineShapeNode(node);
|
||||||
const penStroke = node.type === "image" && node.layerRole === "pen-stroke";
|
const penStroke = node.type === "image" && node.layerRole === "pen-stroke";
|
||||||
|
const imageGenerator = node.type === "image" && node.layerRole === "image-generator";
|
||||||
const textOverlay = node.type === "text" && node.layerRole === "editable-text";
|
const textOverlay = node.type === "text" && node.layerRole === "editable-text";
|
||||||
const textOverlayStyle = textOverlay ? editableTextLayerStyle(node) : undefined;
|
const textOverlayStyle = textOverlay ? editableTextLayerStyle(node) : undefined;
|
||||||
const textEditorRef = useRef<HTMLTextAreaElement | null>(null);
|
const textEditorRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const selectionTitle = manualFrame ? "Frame" : shapeNode || booleanShapeGroup ? node.title.trim() || "Shape" : penStroke ? node.title.trim() || "Pen stroke" : node.title.trim() || (node.type === "image" ? "Image" : node.type);
|
const selectionTitle = manualFrame ? "Frame" : shapeNode || booleanShapeGroup ? node.title.trim() || "Shape" : penStroke ? node.title.trim() || "Pen stroke" : node.title.trim() || (node.type === "image" ? "Image" : node.type);
|
||||||
const selectionSize = `${Math.round(node.width)} × ${Math.round(node.height)}`;
|
const visibleSelectionSize = imageGenerator && imageGeneratorTargetSize ? imageGeneratorTargetSize : { width: node.width, height: node.height };
|
||||||
|
const selectionSize = `${Math.round(visibleSelectionSize.width)} × ${Math.round(visibleSelectionSize.height)}`;
|
||||||
const SelectionIcon = node.type === "frame" ? Frame : node.type === "text" ? Type : penStroke ? PenLine : ImageIcon;
|
const SelectionIcon = node.type === "frame" ? Frame : node.type === "text" ? Type : penStroke ? PenLine : ImageIcon;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -4080,7 +4107,7 @@ function CanvasNodeView({
|
|||||||
{shapeNode || booleanShapeGroup ? (
|
{shapeNode || booleanShapeGroup ? (
|
||||||
<ShapeNodePreview node={node} />
|
<ShapeNodePreview node={node} />
|
||||||
) : visual ? (
|
) : visual ? (
|
||||||
<ArtboardPreview node={node} viewportScale={viewportScale} />
|
<ArtboardPreview node={node} viewportScale={viewportScale} imageGeneratorTargetSize={imageGeneratorTargetSize} />
|
||||||
) : textOverlay ? (
|
) : textOverlay ? (
|
||||||
editing ? (
|
editing ? (
|
||||||
<textarea
|
<textarea
|
||||||
@@ -4128,7 +4155,7 @@ function CanvasNodeView({
|
|||||||
title={selectionTitle}
|
title={selectionTitle}
|
||||||
size={selectionSize}
|
size={selectionSize}
|
||||||
Icon={SelectionIcon}
|
Icon={SelectionIcon}
|
||||||
variant={manualFrame ? "frame" : penStroke ? "pen" : shapeNode || booleanShapeGroup ? "shape" : textOverlay ? "text" : undefined}
|
variant={manualFrame ? "frame" : imageGenerator ? "image-generator" : penStroke ? "pen" : shapeNode || booleanShapeGroup ? "shape" : textOverlay ? "text" : undefined}
|
||||||
selectionScale={selectionScale}
|
selectionScale={selectionScale}
|
||||||
transform={transform}
|
transform={transform}
|
||||||
onResizePointerDown={onResizePointerDown}
|
onResizePointerDown={onResizePointerDown}
|
||||||
@@ -5404,6 +5431,56 @@ function latestAgentThreadId(project: Project | null, hiddenThreadIds: ReadonlyS
|
|||||||
.sort((a, b) => b.updateTimestamp - a.updateTimestamp)[0]?.agentThreadId || "";
|
.sort((a, b) => b.updateTimestamp - a.updateTimestamp)[0]?.agentThreadId || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preserveLocalDraftImageGenerators(nextProject: Project, currentNodes: CanvasNode[]) {
|
||||||
|
const nextNodeIds = new Set(nextProject.nodes.map((node) => node.id));
|
||||||
|
const localDraftGenerators = currentNodes.filter((node) => node.type === "image" && node.layerRole === "image-generator" && node.status === "draft" && !nextNodeIds.has(node.id));
|
||||||
|
if (localDraftGenerators.length === 0) return nextProject;
|
||||||
|
return {
|
||||||
|
...nextProject,
|
||||||
|
nodes: [...nextProject.nodes, ...localDraftGenerators]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextImageGeneratorPosition(origin: { x: number; y: number }, size: { width: number; height: number }, nodes: CanvasNode[], viewportScale: number) {
|
||||||
|
const base = {
|
||||||
|
x: origin.x - size.width / 2,
|
||||||
|
y: origin.y - size.height / 2
|
||||||
|
};
|
||||||
|
const generators = nodes.filter((node) => node.type === "image" && node.layerRole === "image-generator");
|
||||||
|
const gap = 24 / Math.max(viewportScale, 0.01);
|
||||||
|
const step = {
|
||||||
|
x: size.width + gap,
|
||||||
|
y: size.height + gap
|
||||||
|
};
|
||||||
|
const overlapsAnyGenerator = (candidate: { x: number; y: number }) =>
|
||||||
|
generators.some((node) => rectanglesOverlap({ ...candidate, ...size }, node));
|
||||||
|
|
||||||
|
if (!overlapsAnyGenerator(base)) return base;
|
||||||
|
|
||||||
|
for (let ring = 1; ring <= 6; ring += 1) {
|
||||||
|
for (let row = -ring; row <= ring; row += 1) {
|
||||||
|
for (let column = -ring; column <= ring; column += 1) {
|
||||||
|
if (Math.max(Math.abs(row), Math.abs(column)) !== ring) continue;
|
||||||
|
const candidate = {
|
||||||
|
x: base.x + column * step.x,
|
||||||
|
y: base.y + row * step.y
|
||||||
|
};
|
||||||
|
if (!overlapsAnyGenerator(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackOffset = generators.length * gap;
|
||||||
|
return {
|
||||||
|
x: base.x + fallbackOffset,
|
||||||
|
y: base.y + fallbackOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectanglesOverlap(a: { x: number; y: number; width: number; height: number }, b: CanvasNode) {
|
||||||
|
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
|
||||||
|
}
|
||||||
|
|
||||||
function agentThreadTitleText(content: string) {
|
function agentThreadTitleText(content: string) {
|
||||||
return visiblePromptText(content);
|
return visiblePromptText(content);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2365,6 +2365,14 @@ button:disabled {
|
|||||||
right: calc(10px / var(--selection-scale));
|
right: calc(10px / var(--selection-scale));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-generator-selection-frame {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-generator-selection-frame .moteva-selection-meta {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pen-stroke-selection-frame .moteva-selection-meta {
|
.pen-stroke-selection-frame .moteva-selection-meta {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user