feat(canvas): 提示词编辑 - 支持放大窗口调整尺寸与键盘微调
This commit is contained in:
@@ -57,12 +57,19 @@ type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes];
|
|||||||
const PROMPT_REFERENCE_SHELF_HEIGHT = 58;
|
const PROMPT_REFERENCE_SHELF_HEIGHT = 58;
|
||||||
// Keep the compact editor readable at rest: three 20px lines plus 12px vertical padding.
|
// Keep the compact editor readable at rest: three 20px lines plus 12px vertical padding.
|
||||||
const PROMPT_EDITOR_MIN_HEIGHT = 72;
|
const PROMPT_EDITOR_MIN_HEIGHT = 72;
|
||||||
const PROMPT_EDITOR_EXPANDED_MIN_HEIGHT = 76;
|
const PROMPT_EDITOR_EXPANDED_MIN_HEIGHT = 200;
|
||||||
const PROMPT_EDITOR_LINE_HEIGHT = 20;
|
const PROMPT_EDITOR_LINE_HEIGHT = 20;
|
||||||
const PROMPT_EDITOR_EXPANDED_LINE_HEIGHT = 24;
|
const PROMPT_EDITOR_EXPANDED_LINE_HEIGHT = 24;
|
||||||
const PROMPT_EDITOR_VERTICAL_PADDING = 12;
|
const PROMPT_EDITOR_VERTICAL_PADDING = 12;
|
||||||
const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20;
|
const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20;
|
||||||
const PROMPT_EDITOR_MAX_LINES = 8;
|
const PROMPT_EDITOR_MAX_LINES = 8;
|
||||||
|
const PROMPT_EDITOR_EXPANDED_MAX_LINES = 14;
|
||||||
|
const PROMPT_EDITOR_MODAL_WIDTH = "min(1200px, 92vw)";
|
||||||
|
const PROMPT_EDITOR_MODAL_DEFAULT_WIDTH = 1200;
|
||||||
|
const PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT = 420;
|
||||||
|
const PROMPT_EDITOR_MODAL_MIN_WIDTH = 560;
|
||||||
|
const PROMPT_EDITOR_MODAL_MIN_HEIGHT = 320;
|
||||||
|
const PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN = 24;
|
||||||
|
|
||||||
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
|
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
|
||||||
const globalConfig = useEffectiveConfig();
|
const globalConfig = useEffectiveConfig();
|
||||||
@@ -80,6 +87,8 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
const [presetOpen, setPresetOpen] = useState(false);
|
const [presetOpen, setPresetOpen] = useState(false);
|
||||||
const [expandedPresetOpen, setExpandedPresetOpen] = useState(false);
|
const [expandedPresetOpen, setExpandedPresetOpen] = useState(false);
|
||||||
const [expandedPromptOpen, setExpandedPromptOpen] = useState(false);
|
const [expandedPromptOpen, setExpandedPromptOpen] = useState(false);
|
||||||
|
const [expandedModalSize, setExpandedModalSize] = useState<{ width: number; height: number } | null>(null);
|
||||||
|
const expandedModalRef = useRef<HTMLDivElement>(null);
|
||||||
const [promptContentHeight, setPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, false));
|
const [promptContentHeight, setPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, false));
|
||||||
const [expandedPromptContentHeight, setExpandedPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, true));
|
const [expandedPromptContentHeight, setExpandedPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, true));
|
||||||
const [manualPromptHeight, setManualPromptHeight] = useState<number | null>(null);
|
const [manualPromptHeight, setManualPromptHeight] = useState<number | null>(null);
|
||||||
@@ -164,6 +173,11 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
const expandedPromptBounds = promptEditorBounds(true, activeReferenceCount > 0);
|
const expandedPromptBounds = promptEditorBounds(true, activeReferenceCount > 0);
|
||||||
const composerHeight = clampPromptHeight(manualPromptHeight ?? promptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), promptBounds);
|
const composerHeight = clampPromptHeight(manualPromptHeight ?? promptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), promptBounds);
|
||||||
const expandedComposerHeight = clampPromptHeight(manualExpandedPromptHeight ?? expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds);
|
const expandedComposerHeight = clampPromptHeight(manualExpandedPromptHeight ?? expandedPromptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), expandedPromptBounds);
|
||||||
|
const measureExpandedModalSize = () => {
|
||||||
|
const rect = expandedModalRef.current?.getBoundingClientRect();
|
||||||
|
if (!rect?.width || !rect.height) return { width: PROMPT_EDITOR_MODAL_DEFAULT_WIDTH, height: PROMPT_EDITOR_MODAL_DEFAULT_HEIGHT };
|
||||||
|
return { width: Math.round(rect.width), height: Math.round(rect.height) };
|
||||||
|
};
|
||||||
const isSubmitDisabled = !isRunning && !prompt.trim();
|
const isSubmitDisabled = !isRunning && !prompt.trim();
|
||||||
const canExpandPrompt = mode === "image" || mode === "video";
|
const canExpandPrompt = mode === "image" || mode === "video";
|
||||||
const canOptimizePrompt = Boolean(promptOptimizerProvider) && canExpandPrompt;
|
const canOptimizePrompt = Boolean(promptOptimizerProvider) && canExpandPrompt;
|
||||||
@@ -179,6 +193,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpandedPromptOpen(false);
|
setExpandedPromptOpen(false);
|
||||||
setExpandedPresetOpen(false);
|
setExpandedPresetOpen(false);
|
||||||
|
setExpandedModalSize(null);
|
||||||
setPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, false));
|
setPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, false));
|
||||||
setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true));
|
setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true));
|
||||||
setManualPromptHeight(null);
|
setManualPromptHeight(null);
|
||||||
@@ -435,12 +450,12 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPromptEditor = (expanded: boolean) => {
|
const renderPromptEditor = (expanded: boolean, fill = false) => {
|
||||||
const bounds = expanded ? expandedPromptBounds : promptBounds;
|
const bounds = expanded ? expandedPromptBounds : promptBounds;
|
||||||
const height = expanded ? expandedComposerHeight : composerHeight;
|
const height = expanded ? expandedComposerHeight : composerHeight;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="canvas-node-composer-editor" style={{ height }}>
|
<div className={fill ? "canvas-node-composer-editor min-h-0 flex-1" : "canvas-node-composer-editor"} style={fill ? undefined : { height }}>
|
||||||
<ConnectedReferenceShelf
|
<ConnectedReferenceShelf
|
||||||
targetNodeId={node.id}
|
targetNodeId={node.id}
|
||||||
references={resolvedMentionReferences}
|
references={resolvedMentionReferences}
|
||||||
@@ -535,7 +550,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
title={null}
|
title={null}
|
||||||
footer={null}
|
footer={null}
|
||||||
centered
|
centered
|
||||||
width={920}
|
width={expandedModalSize ? expandedModalSize.width : PROMPT_EDITOR_MODAL_WIDTH}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setExpandedPresetOpen(false);
|
setExpandedPresetOpen(false);
|
||||||
@@ -546,15 +561,16 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
|||||||
body: { minHeight: 0, padding: 0 },
|
body: { minHeight: 0, padding: 0 },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative flex min-h-0 flex-col gap-2.5 overflow-visible p-3" style={{ ...composerTokens, color: theme.node.text }}>
|
<div ref={expandedModalRef} className="relative flex min-h-0 flex-col gap-2.5 overflow-visible p-3" style={{ ...composerTokens, color: theme.node.text, ...(expandedModalSize ? { height: expandedModalSize.height } : null) }}>
|
||||||
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
|
<div className="shrink-0 pr-8">{renderComposerHeader(true)}</div>
|
||||||
{renderPromptEditor(true)}
|
{renderPromptEditor(true, Boolean(expandedModalSize))}
|
||||||
{hasVideoPromptTools ? (
|
{hasVideoPromptTools ? (
|
||||||
<div className="canvas-node-composer-parameters shrink-0">
|
<div className="canvas-node-composer-parameters shrink-0">
|
||||||
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
|
<CanvasVideoPromptTools metadata={node.metadata} frameOptions={videoFrameOptions} onMetadataChange={(patch) => onConfigChange(node.id, patch)} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="shrink-0">{renderComposerControls(true)}</div>
|
<div className="shrink-0">{renderComposerControls(true)}</div>
|
||||||
|
<PromptModalResizeHandle size={expandedModalSize} measure={measureExpandedModalSize} onResize={setExpandedModalSize} accent={theme.node.muted} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -916,10 +932,80 @@ function PromptResizeHandle({ height, min, max, onResize }: { height: number; mi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampExpandedModalSize(size: { width: number; height: number }) {
|
||||||
|
const maxWidth = Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, window.innerWidth - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN);
|
||||||
|
const maxHeight = Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, window.innerHeight - PROMPT_EDITOR_MODAL_VIEWPORT_MARGIN);
|
||||||
|
return {
|
||||||
|
width: Math.min(maxWidth, Math.max(PROMPT_EDITOR_MODAL_MIN_WIDTH, Math.round(size.width))),
|
||||||
|
height: Math.min(maxHeight, Math.max(PROMPT_EDITOR_MODAL_MIN_HEIGHT, Math.round(size.height))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromptModalResizeHandle({ size, measure, onResize, accent }: { size: { width: number; height: number } | null; measure: () => { width: number; height: number }; onResize: (size: { width: number; height: number }) => void; accent: string }) {
|
||||||
|
const dragRef = useRef<{ pointerId: number; startX: number; startY: number; width: number; height: number } | null>(null);
|
||||||
|
|
||||||
|
const finishResize = (event: ReactPointerEvent<HTMLButtonElement>) => {
|
||||||
|
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||||
|
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
dragRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
|
||||||
|
const step = event.shiftKey ? 40 : 12;
|
||||||
|
const widthDelta = event.key === "ArrowLeft" ? -step : event.key === "ArrowRight" ? step : 0;
|
||||||
|
const heightDelta = event.key === "ArrowUp" ? -step : event.key === "ArrowDown" ? step : 0;
|
||||||
|
if (!widthDelta && !heightDelta) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const base = size ?? measure();
|
||||||
|
onResize(clampExpandedModalSize({ width: base.width + widthDelta, height: base.height + heightDelta }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute bottom-1.5 right-1.5 z-10 grid size-5 cursor-nwse-resize touch-none place-items-center opacity-60 transition-opacity hover:opacity-100 focus-visible:outline focus-visible:outline-2"
|
||||||
|
aria-label="拖动调整窗口大小"
|
||||||
|
title="拖动调整窗口宽高,也可用方向键调整"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onPointerDown={(event) => {
|
||||||
|
if (event.button !== 0 || !event.isPrimary) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const base = measure();
|
||||||
|
dragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, ...base };
|
||||||
|
onResize(clampExpandedModalSize(base));
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
}}
|
||||||
|
onPointerMove={(event) => {
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||||
|
if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||||
|
dragRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((event.buttons & 1) === 0) {
|
||||||
|
finishResize(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
onResize(clampExpandedModalSize({ width: drag.width + event.clientX - drag.startX, height: drag.height + event.clientY - drag.startY }));
|
||||||
|
}}
|
||||||
|
onPointerUp={finishResize}
|
||||||
|
onPointerCancel={finishResize}
|
||||||
|
onLostPointerCapture={(event) => {
|
||||||
|
if (dragRef.current?.pointerId === event.pointerId) dragRef.current = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span aria-hidden className="absolute bottom-1 right-1 size-2 rounded-br border-b-2 border-r-2" style={{ borderColor: accent }} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function promptEditorBounds(expanded: boolean, hasReferences: boolean) {
|
function promptEditorBounds(expanded: boolean, hasReferences: boolean) {
|
||||||
const shelfHeight = hasReferences ? PROMPT_REFERENCE_SHELF_HEIGHT : 0;
|
const shelfHeight = hasReferences ? PROMPT_REFERENCE_SHELF_HEIGHT : 0;
|
||||||
const min = (expanded ? PROMPT_EDITOR_EXPANDED_MIN_HEIGHT : PROMPT_EDITOR_MIN_HEIGHT) + shelfHeight;
|
const min = (expanded ? PROMPT_EDITOR_EXPANDED_MIN_HEIGHT : PROMPT_EDITOR_MIN_HEIGHT) + shelfHeight;
|
||||||
const max = (expanded ? PROMPT_EDITOR_EXPANDED_LINE_HEIGHT * PROMPT_EDITOR_MAX_LINES + PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING : PROMPT_EDITOR_LINE_HEIGHT * PROMPT_EDITOR_MAX_LINES + PROMPT_EDITOR_VERTICAL_PADDING) + shelfHeight;
|
const max = (expanded ? PROMPT_EDITOR_EXPANDED_LINE_HEIGHT * PROMPT_EDITOR_EXPANDED_MAX_LINES + PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING : PROMPT_EDITOR_LINE_HEIGHT * PROMPT_EDITOR_MAX_LINES + PROMPT_EDITOR_VERTICAL_PADDING) + shelfHeight;
|
||||||
return { min, max };
|
return { min, max };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user