Browse Source

提示词

feature/add_provider_popi
TianYun 2 months ago
parent
commit
3abfbd92e9
  1. 116
      src/components/AnnotationModal.tsx
  2. 25
      src/components/composer/GenerationComposer.tsx
  3. 2
      src/types/annotation.ts

116
src/components/AnnotationModal.tsx

@ -1,9 +1,10 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Stage, Layer, Image as KonvaImage, Rect, Ellipse, Arrow, Line, Text, Transformer } from "react-konva";
import { useAnnotationStore } from "@/store/annotationStore";
import { useWorkflowStore } from "@/store/workflowStore";
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults";
import { isHttpMediaUrl } from "@/utils/mediaResolver";
import { useI18n } from "@/i18n";
import {
@ -13,6 +14,7 @@ import {
ArrowShape,
FreehandShape,
TextShape,
NodeType,
ToolType,
} from "@/types";
import Konva from "konva";
@ -29,6 +31,40 @@ const COLORS = [
];
const STROKE_WIDTHS = [2, 4, 8];
const DEFAULT_TEXT_BOX_WIDTH = 180;
const DEFAULT_TEXT_BOX_HEIGHT = 32;
const FIXED_TEXT_FONT_SIZE = 24;
function readPositiveDimension(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
}
function scaleShape(shape: AnnotationShape, scaleX: number, scaleY: number): AnnotationShape {
const base = {
...shape,
x: shape.x * scaleX,
y: shape.y * scaleY,
strokeWidth: shape.strokeWidth * Math.max(scaleX, scaleY),
};
switch (shape.type) {
case "rectangle":
return { ...base, width: shape.width * scaleX, height: shape.height * scaleY } as RectangleShape;
case "circle":
return { ...base, radiusX: shape.radiusX * scaleX, radiusY: shape.radiusY * scaleY } as CircleShape;
case "arrow":
return { ...base, points: shape.points.map((point, index) => point * (index % 2 === 0 ? scaleX : scaleY)) } as ArrowShape;
case "freehand":
return { ...base, points: shape.points.map((point, index) => point * (index % 2 === 0 ? scaleX : scaleY)) } as FreehandShape;
case "text":
return {
...base,
fontSize: shape.fontSize * Math.max(scaleX, scaleY),
width: (shape.width ?? DEFAULT_TEXT_BOX_WIDTH) * scaleX,
height: (shape.height ?? DEFAULT_TEXT_BOX_HEIGHT) * scaleY,
} as TextShape;
}
}
async function getCanvasSafeImageSource(source: string): Promise<{ src: string; revoke?: () => void }> {
if (!isHttpMediaUrl(source)) return { src: source };
@ -62,6 +98,25 @@ export function AnnotationModal() {
} = useAnnotationStore();
const updateNodeData = useWorkflowStore((state) => state.updateNodeData);
const nodes = useWorkflowStore((state) => state.nodes);
const nodeDisplaySize = useMemo(() => {
const sourceNode = sourceNodeId ? nodes.find((node) => node.id === sourceNodeId) : null;
const nodeType = sourceNode?.type as NodeType | undefined;
const defaults = nodeType ? defaultNodeDimensions[nodeType] : { width: 800, height: 600 };
return {
width:
readPositiveDimension(sourceNode?.width) ??
readPositiveDimension(sourceNode?.style?.width) ??
readPositiveDimension(sourceNode?.measured?.width) ??
defaults.width,
height:
readPositiveDimension(sourceNode?.height) ??
readPositiveDimension(sourceNode?.style?.height) ??
readPositiveDimension(sourceNode?.measured?.height) ??
defaults.height,
};
}, [nodes, sourceNodeId]);
const stageRef = useRef<Konva.Stage>(null);
const transformerRef = useRef<Konva.Transformer>(null);
@ -95,16 +150,13 @@ export function AnnotationModal() {
if (cancelled) return;
setImage(img);
if (containerRef.current) {
const containerWidth = containerRef.current.clientWidth - 100;
const containerHeight = containerRef.current.clientHeight - 100;
const scaleX = containerWidth / img.width;
const scaleY = containerHeight / img.height;
const newScale = Math.min(scaleX, scaleY, 1);
setScale(newScale);
setStageSize({ width: img.width, height: img.height });
const containerWidth = containerRef.current.clientWidth;
const containerHeight = containerRef.current.clientHeight;
setScale(1);
setStageSize(nodeDisplaySize);
setPosition({
x: (containerWidth - img.width * newScale) / 2 + 50,
y: (containerHeight - img.height * newScale) / 2 + 50,
x: (containerWidth - nodeDisplaySize.width) / 2,
y: (containerHeight - nodeDisplaySize.height) / 2,
});
}
};
@ -123,7 +175,7 @@ export function AnnotationModal() {
cancelled = true;
revokeImageSource?.();
};
}, [sourceImage]);
}, [sourceImage, nodeDisplaySize]);
useEffect(() => {
if (transformerRef.current && stageRef.current) {
@ -317,7 +369,11 @@ export function AnnotationModal() {
const konvaImage = new Konva.Image({ image, width: image.width, height: image.height });
tempLayer.add(konvaImage);
annotations.forEach((shape) => {
const scaleX = image.width / stageSize.width;
const scaleY = image.height / stageSize.height;
annotations.forEach((annotation) => {
const shape = scaleShape(annotation, scaleX, scaleY);
let konvaShape: Konva.Shape | null = null;
switch (shape.type) {
case "rectangle": {
@ -342,7 +398,7 @@ export function AnnotationModal() {
}
case "text": {
const text = shape as TextShape;
konvaShape = new Konva.Text({ x: text.x, y: text.y, text: text.text, fontSize: text.fontSize, fill: text.fill, opacity: text.opacity });
konvaShape = new Konva.Text({ x: text.x, y: text.y, text: text.text, fontSize: text.fontSize, fill: text.fill, opacity: text.opacity, width: text.width, height: text.height });
break;
}
}
@ -353,7 +409,7 @@ export function AnnotationModal() {
const dataUrl = tempStage.toDataURL({ pixelRatio: 1 });
tempStage.destroy();
return dataUrl;
}, [image, annotations]);
}, [image, annotations, stageSize]);
const handleDone = useCallback(() => {
if (!sourceNodeId) return;
@ -390,6 +446,8 @@ export function AnnotationModal() {
}
case "text": {
const text = shape as TextShape;
const textWidth = text.width ?? DEFAULT_TEXT_BOX_WIDTH;
const textHeight = text.height ?? DEFAULT_TEXT_BOX_HEIGHT;
return (
<Text
key={shape.id}
@ -399,18 +457,19 @@ export function AnnotationModal() {
text={text.text || " "}
fontSize={text.fontSize}
fill={text.fill}
width={textWidth}
height={textHeight}
onTransformEnd={(e) => {
const node = e.target;
const scaleX = node.scaleX();
const scaleY = node.scaleY();
// Reset scale and apply it to fontSize instead
node.scaleX(1);
node.scaleY(1);
const newFontSize = Math.round(text.fontSize * Math.max(scaleX, scaleY));
updateAnnotation(shape.id, {
x: node.x(),
y: node.y(),
fontSize: newFontSize,
width: Math.max(20, textWidth * scaleX),
height: Math.max(FIXED_TEXT_FONT_SIZE, textHeight * scaleY),
});
}}
onDblClick={() => {
@ -442,6 +501,12 @@ export function AnnotationModal() {
{ type: "freehand", label: t("annotation.tool.draw") },
{ type: "text", label: t("annotation.tool.text") },
];
const editingText = editingTextId && editingTextId !== "new"
? annotations.find((annotation): annotation is TextShape => annotation.id === editingTextId && annotation.type === "text")
: null;
const editingTextWidth = (editingText?.width ?? DEFAULT_TEXT_BOX_WIDTH) * scale;
const editingTextHeight = (editingText?.height ?? DEFAULT_TEXT_BOX_HEIGHT) * scale;
const editingFontSize = (editingText?.fontSize ?? FIXED_TEXT_FONT_SIZE) * scale;
return (
<div className="fixed inset-0 z-[100] bg-neutral-950 flex flex-col">
@ -575,9 +640,14 @@ export function AnnotationModal() {
style={{
left: textInputPosition.x,
top: textInputPosition.y,
fontSize: `${toolOptions.fontSize * scale}px`,
width: `${editingTextWidth}px`,
height: `${editingTextHeight}px`,
fontSize: `${editingFontSize}px`,
lineHeight: `${editingTextHeight}px`,
color: editingTextId === "new" ? toolOptions.strokeColor : ((annotations.find((a) => a.id === editingTextId) as TextShape)?.fill || toolOptions.strokeColor),
minWidth: "100px",
minWidth: `${DEFAULT_TEXT_BOX_WIDTH * scale}px`,
padding: 0,
boxSizing: "border-box",
caretColor: "white",
}}
onKeyDown={(e) => {
@ -592,8 +662,10 @@ export function AnnotationModal() {
x: pendingTextPosition.x,
y: pendingTextPosition.y,
text: value,
fontSize: toolOptions.fontSize,
fontSize: FIXED_TEXT_FONT_SIZE,
fill: toolOptions.strokeColor,
width: DEFAULT_TEXT_BOX_WIDTH,
height: DEFAULT_TEXT_BOX_HEIGHT,
stroke: toolOptions.strokeColor,
strokeWidth: toolOptions.strokeWidth,
opacity: toolOptions.opacity,
@ -639,8 +711,10 @@ export function AnnotationModal() {
x: pendingTextPosition.x,
y: pendingTextPosition.y,
text: value,
fontSize: toolOptions.fontSize,
fontSize: FIXED_TEXT_FONT_SIZE,
fill: toolOptions.strokeColor,
width: DEFAULT_TEXT_BOX_WIDTH,
height: DEFAULT_TEXT_BOX_HEIGHT,
stroke: toolOptions.strokeColor,
strokeWidth: toolOptions.strokeWidth,
opacity: toolOptions.opacity,

25
src/components/composer/GenerationComposer.tsx

@ -1290,7 +1290,6 @@ export function GenerationComposer() {
const connectedReferenceImages = context.mode === "node-edit" ? connectedInputs?.images ?? [] : [];
const referenceImages = connectedReferenceImages.length > 0 ? connectedReferenceImages : draft.inputImages;
const primaryReference = referenceImages[0];
const previewReferenceImage =
referencePreviewIndex === null ? null : referenceImages[referencePreviewIndex] ?? null;
const canEditReferenceImages =
@ -2121,15 +2120,21 @@ export function GenerationComposer() {
</div>
) : (
<div className="py-1">
{primaryReference ? (
<button
type="button"
onClick={() => insertReference(`@${t("composer.currentReference")} `)}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-neutral-300 transition-colors hover:bg-neutral-700 hover:text-neutral-100"
>
<img src={primaryReference} alt="" className="h-8 w-8 rounded object-cover" />
<span>{t("composer.currentReference")}</span>
</button>
{referenceImages.length > 0 ? (
referenceImages.map((referenceImage, imageIndex) => {
const referenceLabel = `${imageIndex + 1}`;
return (
<button
key={`${imageIndex}-${referenceImage.slice(0, 24)}`}
type="button"
onClick={() => insertReference(`@${referenceLabel} `)}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs text-neutral-300 transition-colors hover:bg-neutral-700 hover:text-neutral-100"
>
<img src={referenceImage} alt="" className="h-8 w-8 rounded object-cover" />
<span>{referenceLabel}</span>
</button>
);
})
) : (
<button
type="button"

2
src/types/annotation.ts

@ -75,6 +75,8 @@ export interface TextShape extends BaseShape {
text: string;
fontSize: number;
fill: string;
width?: number;
height?: number;
}
/**

Loading…
Cancel
Save