refactor: share canvas mention textarea
This commit is contained in:
@@ -186,7 +186,7 @@ import {
|
|||||||
} from "./canvasWorkflowDeserialize";
|
} from "./canvasWorkflowDeserialize";
|
||||||
import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents";
|
import { CanvasNodeToolbar, CanvasNodeVideoPlayer, CanvasSelectChip } from "./canvasComponents";
|
||||||
import type { CanvasNodeToolbarAction } from "./canvasComponents";
|
import type { CanvasNodeToolbarAction } from "./canvasComponents";
|
||||||
import { CanvasTextPromptComposer } from "./CanvasTextPromptComposer";
|
import { CanvasPromptMentionTextarea, CanvasTextPromptComposer } from "./CanvasTextPromptComposer";
|
||||||
import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels";
|
import { CanvasMultiGridPanel, CanvasUpscalePanel, CanvasInpaintPanel } from "./canvasToolPanels";
|
||||||
import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing";
|
import { CanvasSmoothedProgressRing } from "./CanvasSmoothedProgressRing";
|
||||||
|
|
||||||
@@ -198,7 +198,6 @@ const canvasEnterpriseVideoModelOptions: CanvasOption[] = ENTERPRISE_VIDEO_MODEL
|
|||||||
// --- Canvas generation keep-alive (survives page refresh / view switch) ---
|
// --- Canvas generation keep-alive (survives page refresh / view switch) ---
|
||||||
|
|
||||||
const MENTION_TOKEN_RE = /@(?:图片|视频|文本)\d+/g;
|
const MENTION_TOKEN_RE = /@(?:图片|视频|文本)\d+/g;
|
||||||
const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/;
|
|
||||||
|
|
||||||
function buildNodeMentionOptions(
|
function buildNodeMentionOptions(
|
||||||
kind: CanvasNodeKind,
|
kind: CanvasNodeKind,
|
||||||
@@ -4434,38 +4433,7 @@ function CanvasPage({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{imageNodeActive && !isCanvasNodeMoving && !imageNodeFocusActive ? (() => {
|
{imageNodeActive && !isCanvasNodeMoving && !imageNodeFocusActive ? (
|
||||||
const imgMentionOptions = buildNodeMentionOptions("image", imageNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks);
|
|
||||||
const imgMentionState = textNodeMentionStates[imageNode.id] || { open: false, query: "", start: 0, caret: 0, activeIndex: 0 };
|
|
||||||
const imgFilteredMentions = imgMentionState.open
|
|
||||||
? imgMentionOptions.filter((o) => !imgMentionState.query || o.searchText.includes(imgMentionState.query.toLowerCase()))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleImagePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const caret = e.target.selectionStart || 0;
|
|
||||||
updateImageNodePrompt(imageNode.id, value);
|
|
||||||
const beforeCaret = value.slice(0, caret);
|
|
||||||
const atIdx = beforeCaret.lastIndexOf("@");
|
|
||||||
if (atIdx >= 0) {
|
|
||||||
const query = beforeCaret.slice(atIdx + 1);
|
|
||||||
if (!MENTION_BOUNDARY_RE.test(query) && !query.includes(" ")) {
|
|
||||||
setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { open: true, query, start: atIdx, caret, activeIndex: 0 } }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closeTextNodeMention(imageNode.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImagePromptKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (!imgMentionState.open || imgFilteredMentions.length === 0) return;
|
|
||||||
if (e.key === "ArrowDown") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { ...imgMentionState, activeIndex: (imgMentionState.activeIndex + 1) % imgFilteredMentions.length } })); }
|
|
||||||
else if (e.key === "ArrowUp") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [imageNode.id]: { ...imgMentionState, activeIndex: (imgMentionState.activeIndex - 1 + imgFilteredMentions.length) % imgFilteredMentions.length } })); }
|
|
||||||
else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const opt = imgFilteredMentions[imgMentionState.activeIndex]; if (opt) insertTextNodeMention(imageNode.id, opt, e.currentTarget, "image"); }
|
|
||||||
else if (e.key === "Escape") { e.preventDefault(); closeTextNodeMention(imageNode.id); }
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="studio-canvas-image-composer">
|
<div className="studio-canvas-image-composer">
|
||||||
<div className="studio-canvas-image-composer__tools">
|
<div className="studio-canvas-image-composer__tools">
|
||||||
<button
|
<button
|
||||||
@@ -4586,28 +4554,18 @@ function CanvasPage({
|
|||||||
</button>
|
</button>
|
||||||
<button type="button" className="studio-canvas-image-composer__expand" aria-label="展开">↗</button>
|
<button type="button" className="studio-canvas-image-composer__expand" aria-label="展开">↗</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="studio-canvas-text-composer__input-wrap">
|
<CanvasPromptMentionTextarea
|
||||||
<textarea
|
nodeId={imageNode.id}
|
||||||
value={imageNode.prompt}
|
value={imageNode.prompt}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
|
||||||
onChange={handleImagePromptChange}
|
|
||||||
onKeyDown={handleImagePromptKeyDown}
|
|
||||||
placeholder="描述你想要生成的画面内容,按/呼出指令,@引用素材"
|
placeholder="描述你想要生成的画面内容,按/呼出指令,@引用素材"
|
||||||
|
mentionOptions={buildNodeMentionOptions("image", imageNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks)}
|
||||||
|
mentionState={textNodeMentionStates[imageNode.id]}
|
||||||
|
mentionKind="image"
|
||||||
|
onPromptChange={updateImageNodePrompt}
|
||||||
|
onMentionStateChange={setTextNodeMentionStates}
|
||||||
|
onCloseMention={closeTextNodeMention}
|
||||||
|
onInsertMention={insertTextNodeMention}
|
||||||
/>
|
/>
|
||||||
{imgMentionState.open && (
|
|
||||||
<div className="studio-canvas-mention-panel">
|
|
||||||
{imgFilteredMentions.length > 0 ? imgFilteredMentions.map((opt, idx) => (
|
|
||||||
<button key={opt.token} type="button" className={`studio-canvas-mention-item${idx === imgMentionState.activeIndex ? " is-active" : ""}`} onMouseDown={(e) => { e.preventDefault(); insertTextNodeMention(imageNode.id, opt, e.currentTarget.closest(".studio-canvas-image-composer")?.querySelector("textarea") || null, "image"); }}>
|
|
||||||
<span className="studio-canvas-mention-thumb">{opt.kind === "image" && opt.previewUrl ? <img src={opt.previewUrl} alt="" /> : opt.kind === "image" ? "🖼" : opt.kind === "video" ? "🎬" : "📝"}</span>
|
|
||||||
<span className="studio-canvas-mention-label">{opt.nodeTitle}</span>
|
|
||||||
<span className="studio-canvas-mention-token">{opt.token}</span>
|
|
||||||
</button>
|
|
||||||
)) : (
|
|
||||||
<div className="studio-canvas-mention-item" style={{ opacity: 0.5, pointerEvents: "none" }}><span className="studio-canvas-mention-label">没有可引用的连接节点</span></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="studio-canvas-image-composer__footer">
|
<div className="studio-canvas-image-composer__footer">
|
||||||
<CanvasSelectChip
|
<CanvasSelectChip
|
||||||
ariaLabel="选择生图模型"
|
ariaLabel="选择生图模型"
|
||||||
@@ -4675,7 +4633,7 @@ function CanvasPage({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
); })() : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -4830,38 +4788,7 @@ function CanvasPage({
|
|||||||
onMouseDown={(event) => handleNodeResizeStart(event, "video", videoNode.id, videoNode.size)}
|
onMouseDown={(event) => handleNodeResizeStart(event, "video", videoNode.id, videoNode.size)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{videoNodeActive && !isCanvasNodeMoving ? (() => {
|
{videoNodeActive && !isCanvasNodeMoving ? (
|
||||||
const vidMentionOptions = buildNodeMentionOptions("video", videoNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks);
|
|
||||||
const vidMentionState = textNodeMentionStates[videoNode.id] || { open: false, query: "", start: 0, caret: 0, activeIndex: 0 };
|
|
||||||
const vidFilteredMentions = vidMentionState.open
|
|
||||||
? vidMentionOptions.filter((o) => !vidMentionState.query || o.searchText.includes(vidMentionState.query.toLowerCase()))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleVideoPromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const value = e.target.value;
|
|
||||||
const caret = e.target.selectionStart || 0;
|
|
||||||
updateVideoNodePrompt(videoNode.id, value);
|
|
||||||
const beforeCaret = value.slice(0, caret);
|
|
||||||
const atIdx = beforeCaret.lastIndexOf("@");
|
|
||||||
if (atIdx >= 0) {
|
|
||||||
const query = beforeCaret.slice(atIdx + 1);
|
|
||||||
if (!MENTION_BOUNDARY_RE.test(query) && !query.includes(" ")) {
|
|
||||||
setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { open: true, query, start: atIdx, caret, activeIndex: 0 } }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
closeTextNodeMention(videoNode.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVideoPromptKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (!vidMentionState.open || vidFilteredMentions.length === 0) return;
|
|
||||||
if (e.key === "ArrowDown") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { ...vidMentionState, activeIndex: (vidMentionState.activeIndex + 1) % vidFilteredMentions.length } })); }
|
|
||||||
else if (e.key === "ArrowUp") { e.preventDefault(); setTextNodeMentionStates((prev) => ({ ...prev, [videoNode.id]: { ...vidMentionState, activeIndex: (vidMentionState.activeIndex - 1 + vidFilteredMentions.length) % vidFilteredMentions.length } })); }
|
|
||||||
else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); const opt = vidFilteredMentions[vidMentionState.activeIndex]; if (opt) insertTextNodeMention(videoNode.id, opt, e.currentTarget, "video"); }
|
|
||||||
else if (e.key === "Escape") { e.preventDefault(); closeTextNodeMention(videoNode.id); }
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="studio-canvas-video-composer">
|
<div className="studio-canvas-video-composer">
|
||||||
<div className="studio-canvas-video-composer__tabs studio-canvas-video-composer__mode-tabs">
|
<div className="studio-canvas-video-composer__tabs studio-canvas-video-composer__mode-tabs">
|
||||||
<button
|
<button
|
||||||
@@ -4983,37 +4910,18 @@ function CanvasPage({
|
|||||||
<button type="button">角色库</button>
|
<button type="button">角色库</button>
|
||||||
<button type="button" className="is-active">文本</button>
|
<button type="button" className="is-active">文本</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="studio-canvas-text-composer__input-wrap">
|
<CanvasPromptMentionTextarea
|
||||||
<textarea
|
nodeId={videoNode.id}
|
||||||
value={videoNode.prompt}
|
value={videoNode.prompt}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
|
||||||
onChange={handleVideoPromptChange}
|
|
||||||
onKeyDown={handleVideoPromptKeyDown}
|
|
||||||
placeholder="根据文字描述生成视频。"
|
placeholder="根据文字描述生成视频。"
|
||||||
|
mentionOptions={buildNodeMentionOptions("video", videoNode.id, imageNodes, videoNodes, textNodes, getIncomingCanvasPorts, manualLinks)}
|
||||||
|
mentionState={textNodeMentionStates[videoNode.id]}
|
||||||
|
mentionKind="video"
|
||||||
|
onPromptChange={updateVideoNodePrompt}
|
||||||
|
onMentionStateChange={setTextNodeMentionStates}
|
||||||
|
onCloseMention={closeTextNodeMention}
|
||||||
|
onInsertMention={insertTextNodeMention}
|
||||||
/>
|
/>
|
||||||
{vidMentionState.open ? (
|
|
||||||
<div className="studio-canvas-mention-panel">
|
|
||||||
{vidFilteredMentions.length > 0 ? vidFilteredMentions.map((opt, idx) => (
|
|
||||||
<button
|
|
||||||
key={opt.token}
|
|
||||||
type="button"
|
|
||||||
className={`studio-canvas-mention-item${idx === vidMentionState.activeIndex ? " is-active" : ""}`}
|
|
||||||
onMouseDown={(ev) => { ev.preventDefault(); insertTextNodeMention(videoNode.id, opt, ev.currentTarget.closest(".studio-canvas-text-composer__input-wrap")?.querySelector("textarea")!, "video"); }}
|
|
||||||
>
|
|
||||||
<span className={`studio-canvas-mention-icon studio-canvas-mention-icon--${opt.kind}`}>
|
|
||||||
{opt.kind === "image" ? "🖼" : opt.kind === "video" ? "🎬" : "📝"}
|
|
||||||
</span>
|
|
||||||
<span className="studio-canvas-mention-label">{opt.nodeTitle}</span>
|
|
||||||
<span className="studio-canvas-mention-token">{opt.token}</span>
|
|
||||||
</button>
|
|
||||||
)) : (
|
|
||||||
<div className="studio-canvas-mention-item" style={{ opacity: 0.5, pointerEvents: "none" }}>
|
|
||||||
<span className="studio-canvas-mention-label">没有可引用的连接节点</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="studio-canvas-video-composer__footer studio-canvas-video-composer__settings">
|
<div className="studio-canvas-video-composer__footer studio-canvas-video-composer__settings">
|
||||||
<CanvasSelectChip
|
<CanvasSelectChip
|
||||||
ariaLabel="选择视频模型"
|
ariaLabel="选择视频模型"
|
||||||
@@ -5097,7 +5005,7 @@ function CanvasPage({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
); })() : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SendOutlined } from "@ant-design/icons";
|
import { SendOutlined } from "@ant-design/icons";
|
||||||
import type { CSSProperties, Dispatch, SetStateAction } from "react";
|
import { useRef, type CSSProperties, type Dispatch, type SetStateAction } from "react";
|
||||||
import type { CanvasPromptMentionOption, CanvasPromptMentionState } from "./canvasTypes";
|
import type { CanvasNodeKind, CanvasPromptMentionOption, CanvasPromptMentionState } from "./canvasTypes";
|
||||||
|
|
||||||
const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/;
|
const MENTION_BOUNDARY_RE = /\s|[,。、;:!??(){}[\]<>]/;
|
||||||
const EMPTY_MENTION_STYLE: CSSProperties = { opacity: 0.5, pointerEvents: "none" };
|
const EMPTY_MENTION_STYLE: CSSProperties = { opacity: 0.5, pointerEvents: "none" };
|
||||||
@@ -12,11 +12,10 @@ const DEFAULT_MENTION_STATE: CanvasPromptMentionState = {
|
|||||||
activeIndex: 0,
|
activeIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CanvasTextPromptComposerProps {
|
interface CanvasPromptMentionTextareaProps {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
prompt: string;
|
value: string;
|
||||||
canGenerate: boolean;
|
placeholder: string;
|
||||||
isGenerating: boolean;
|
|
||||||
mentionOptions: CanvasPromptMentionOption[];
|
mentionOptions: CanvasPromptMentionOption[];
|
||||||
mentionState?: CanvasPromptMentionState;
|
mentionState?: CanvasPromptMentionState;
|
||||||
onPromptChange: (nodeId: string, prompt: string) => void;
|
onPromptChange: (nodeId: string, prompt: string) => void;
|
||||||
@@ -26,23 +25,24 @@ interface CanvasTextPromptComposerProps {
|
|||||||
nodeId: string,
|
nodeId: string,
|
||||||
option: CanvasPromptMentionOption,
|
option: CanvasPromptMentionOption,
|
||||||
textarea: HTMLTextAreaElement | null,
|
textarea: HTMLTextAreaElement | null,
|
||||||
|
kind?: CanvasNodeKind,
|
||||||
) => void;
|
) => void;
|
||||||
onGenerate: (nodeId: string) => void | Promise<void>;
|
mentionKind?: CanvasNodeKind;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CanvasTextPromptComposer({
|
export function CanvasPromptMentionTextarea({
|
||||||
nodeId,
|
nodeId,
|
||||||
prompt,
|
value,
|
||||||
canGenerate,
|
placeholder,
|
||||||
isGenerating,
|
|
||||||
mentionOptions,
|
mentionOptions,
|
||||||
mentionState = DEFAULT_MENTION_STATE,
|
mentionState = DEFAULT_MENTION_STATE,
|
||||||
onPromptChange,
|
onPromptChange,
|
||||||
onMentionStateChange,
|
onMentionStateChange,
|
||||||
onCloseMention,
|
onCloseMention,
|
||||||
onInsertMention,
|
onInsertMention,
|
||||||
onGenerate,
|
mentionKind,
|
||||||
}: CanvasTextPromptComposerProps) {
|
}: CanvasPromptMentionTextareaProps) {
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const filteredMentions = mentionState.open
|
const filteredMentions = mentionState.open
|
||||||
? mentionOptions.filter((option) => !mentionState.query || option.searchText.includes(mentionState.query.toLowerCase()))
|
? mentionOptions.filter((option) => !mentionState.query || option.searchText.includes(mentionState.query.toLowerCase()))
|
||||||
: [];
|
: [];
|
||||||
@@ -88,7 +88,7 @@ export function CanvasTextPromptComposer({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const option = filteredMentions[mentionState.activeIndex];
|
const option = filteredMentions[mentionState.activeIndex];
|
||||||
if (option) {
|
if (option) {
|
||||||
onInsertMention(nodeId, option, event.currentTarget);
|
onInsertMention(nodeId, option, event.currentTarget, mentionKind);
|
||||||
}
|
}
|
||||||
} else if (event.key === "Escape") {
|
} else if (event.key === "Escape") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -106,15 +106,15 @@ export function CanvasTextPromptComposer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="studio-canvas-text-composer">
|
|
||||||
<div className="studio-canvas-text-composer__input-wrap">
|
<div className="studio-canvas-text-composer__input-wrap">
|
||||||
<textarea
|
<textarea
|
||||||
value={prompt}
|
ref={textareaRef}
|
||||||
|
value={value}
|
||||||
onMouseDown={(event) => event.stopPropagation()}
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
onChange={handlePromptChange}
|
onChange={handlePromptChange}
|
||||||
onKeyDown={handlePromptKeyDown}
|
onKeyDown={handlePromptKeyDown}
|
||||||
onSelect={handlePromptSelect}
|
onSelect={handlePromptSelect}
|
||||||
placeholder="写下你想讲的故事、场景或角色设定。@引用连接的节点"
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
{mentionState.open ? (
|
{mentionState.open ? (
|
||||||
<div className="studio-canvas-mention-panel">
|
<div className="studio-canvas-mention-panel">
|
||||||
@@ -125,10 +125,7 @@ export function CanvasTextPromptComposer({
|
|||||||
className={`studio-canvas-mention-item${index === mentionState.activeIndex ? " is-active" : ""}`}
|
className={`studio-canvas-mention-item${index === mentionState.activeIndex ? " is-active" : ""}`}
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const textarea = event.currentTarget
|
onInsertMention(nodeId, option, textareaRef.current, mentionKind);
|
||||||
.closest(".studio-canvas-text-composer")
|
|
||||||
?.querySelector("textarea") ?? null;
|
|
||||||
onInsertMention(nodeId, option, textarea);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="studio-canvas-mention-thumb">
|
<span className="studio-canvas-mention-thumb">
|
||||||
@@ -147,6 +144,54 @@ export function CanvasTextPromptComposer({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CanvasTextPromptComposerProps {
|
||||||
|
nodeId: string;
|
||||||
|
prompt: string;
|
||||||
|
canGenerate: boolean;
|
||||||
|
isGenerating: boolean;
|
||||||
|
mentionOptions: CanvasPromptMentionOption[];
|
||||||
|
mentionState?: CanvasPromptMentionState;
|
||||||
|
onPromptChange: (nodeId: string, prompt: string) => void;
|
||||||
|
onMentionStateChange: Dispatch<SetStateAction<Record<string, CanvasPromptMentionState>>>;
|
||||||
|
onCloseMention: (nodeId: string) => void;
|
||||||
|
onInsertMention: (
|
||||||
|
nodeId: string,
|
||||||
|
option: CanvasPromptMentionOption,
|
||||||
|
textarea: HTMLTextAreaElement | null,
|
||||||
|
kind?: CanvasNodeKind,
|
||||||
|
) => void;
|
||||||
|
onGenerate: (nodeId: string) => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanvasTextPromptComposer({
|
||||||
|
nodeId,
|
||||||
|
prompt,
|
||||||
|
canGenerate,
|
||||||
|
isGenerating,
|
||||||
|
mentionOptions,
|
||||||
|
mentionState,
|
||||||
|
onPromptChange,
|
||||||
|
onMentionStateChange,
|
||||||
|
onCloseMention,
|
||||||
|
onInsertMention,
|
||||||
|
onGenerate,
|
||||||
|
}: CanvasTextPromptComposerProps) {
|
||||||
|
return (
|
||||||
|
<div className="studio-canvas-text-composer">
|
||||||
|
<CanvasPromptMentionTextarea
|
||||||
|
nodeId={nodeId}
|
||||||
|
value={prompt}
|
||||||
|
placeholder="写下你想讲的故事、场景或角色设定。@引用连接的节点"
|
||||||
|
mentionOptions={mentionOptions}
|
||||||
|
mentionState={mentionState}
|
||||||
|
onPromptChange={onPromptChange}
|
||||||
|
onMentionStateChange={onMentionStateChange}
|
||||||
|
onCloseMention={onCloseMention}
|
||||||
|
onInsertMention={onInsertMention}
|
||||||
|
/>
|
||||||
<div className="studio-canvas-text-composer__footer">
|
<div className="studio-canvas-text-composer__footer">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user