import { Button, Image as AntImage, InputNumber, Modal, Popover } from "antd"; import { Tooltip } from "@/components/ui/base/tooltip"; import { useEffect, useMemo, useRef, useState, type ReactNode, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent } from "react"; import { ArrowLeftRight, ArrowUp, AtSign, Boxes, ChevronDown, FileText, GripVertical, ImageIcon, ImagePlus, LayoutList, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react"; import { ModelPicker } from "@/components/model-picker"; import { defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store"; import { resolveCanvasGenerationModel } from "@/lib/canvas/canvas-project-generation"; import { canonicalGenerationMetadata } from "@/lib/canvas/generation-contract"; import { clampPromptEditorModalSize, PROMPT_EDITOR_VIEWPORT_MARGIN } from "@/lib/canvas/canvas-prompt-editor-size"; import { CreditSymbol, requestCreditCost } from "@/constant/credits"; import { canvasThemes } from "@/lib/canvas-theme"; import { modelQuoteDescription, modelQuoteRequest } from "@/lib/model-pricing"; import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options"; import { modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection"; import { navigateToSettings } from "@/lib/settings-navigation"; import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store"; import { useUserStore } from "@/stores/use-user-store"; import { CanvasCameraControlPopover } from "./canvas-camera-control-popover"; import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover"; import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover"; import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea"; import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover"; import { CanvasVideoPromptTools } from "./canvas-video-prompt-tools"; import { CanvasPresetPicker, type CanvasPromptPreset } from "./canvas-preset-picker"; import { CanvasNineGridPicker } from "./canvas-nine-grid-picker"; import { CanvasChooseImageStylePicker } from "./canvas-choose-image-style-picker"; import { CanvasChooseEffectPicker } from "./canvas-choose-effect-picker"; import { CanvasChooseMotionPicker } from "./canvas-choose-motion-picker"; import { CanvasPortraitTexturePopover } from "./canvas-portrait-texture-popover"; import { CanvasPromptOptimizerDrawer } from "./canvas-prompt-optimizer-drawer"; import { CanvasNodeType, type CanvasGenerationMode, type CanvasNodeData, type CanvasNodeMetadata, type CanvasWorkspaceMode } from "@/types/canvas"; import { applyToolMention, removeToolMentions, autoMentionCanvasResourceReferences, buildToolMentionReference, canvasResourceMentionToken, normalizeCanvasNodeMentionTokens, overwriteSameTypeToolMention, parseToolMentionTokens, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import { promptOptimizerPlugin, PROMPT_OPTIMIZER_PLUGIN_ID } from "@/lib/plugins/builtin/prompt-optimizer"; import { createPluginHostContext } from "@/services/plugin-host"; import { usePluginStore } from "@/stores/use-plugin-store"; import { useResolvedCanvasResourceReferences } from "./use-resolved-canvas-resource-references"; import { quoteModel, type LogicalModelQuote } from "@/services/api/logical-models"; export type CanvasNodeGenerationMode = CanvasGenerationMode; type CanvasNodePromptPanelProps = { projectId: string; node: CanvasNodeData; isRunning: boolean; onPromptChange: (nodeId: string, prompt: string) => void; onConfigChange: (nodeId: string, patch: Partial) => void; onGenerate: (nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => void; mentionReferences?: CanvasResourceReference[]; onAddReference?: (nodeId: string, reference: CanvasResourceReference) => CanvasResourceReference | undefined; onRemoveReference?: (nodeId: string, reference: CanvasResourceReference) => void; onReorderReferences?: (nodeId: string, orderedNodeIds: string[]) => void; onReplaceReference?: (nodeId: string, oldReference: CanvasResourceReference, sourceNodeId: string) => void; onReplaceReferenceFiles?: (nodeId: string, oldReference: CanvasResourceReference, files: File[]) => void; onClose?: () => void; onNodeMouseDown?: (event: ReactPointerEvent, nodeId: string) => void; onImageSettingsOpenChange?: (open: boolean) => void; workspaceMode?: CanvasWorkspaceMode; onListGenerate?: (nodeId: string, prompt: string) => void; }; type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes]; const PROMPT_REFERENCE_SHELF_HEIGHT = 58; // Keep the compact editor readable at rest: three 20px lines plus 12px vertical padding. const PROMPT_EDITOR_MIN_HEIGHT = 72; const PROMPT_EDITOR_EXPANDED_MIN_HEIGHT = 200; const PROMPT_EDITOR_LINE_HEIGHT = 20; const PROMPT_EDITOR_EXPANDED_LINE_HEIGHT = 24; const PROMPT_EDITOR_VERTICAL_PADDING = 12; const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20; 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; export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onAddReference, onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional", onListGenerate }: CanvasNodePromptPanelProps) { const globalConfig = useEffectiveConfig(); const themeName = useActiveTheme(); const theme = canvasThemes[themeName]; const creditsEnabled = useUserStore((state) => state.features.creditsEnabled); const promptOptimizerInstallation = usePluginStore((state) => state.installations.find((item) => item.manifest.id === PROMPT_OPTIMIZER_PLUGIN_ID)); const promptOptimizerEnabled = usePluginStore((state) => state.pluginStates[PROMPT_OPTIMIZER_PLUGIN_ID]?.effectiveEnabled ?? Boolean(state.installations.find((item) => item.manifest.id === PROMPT_OPTIMIZER_PLUGIN_ID)?.enabled)); const simpleMode = workspaceMode === "simple"; const mode = defaultMode(node.type); const showPromptTemplates = !simpleMode && mode !== "image"; node = { ...node, metadata: canonicalGenerationMetadata(node, mode) }; const hasTextContent = node.type === CanvasNodeType.Text && Boolean(node.metadata?.content?.trim()); const hasImageContent = node.type === CanvasNodeType.Image && Boolean(node.metadata?.content); const savedPrompt = node.metadata?.composerContent ?? node.metadata?.prompt ?? ""; const [prompt, setPrompt] = useState(savedPrompt); const promptRef = useRef(savedPrompt); const [presetOpen, setPresetOpen] = useState(false); const [expandedPresetOpen, setExpandedPresetOpen] = useState(false); const [nineGridOpen, setNineGridOpen] = useState(false); const [expandedNineGridOpen, setExpandedNineGridOpen] = useState(false); const [styleToolOpen, setStyleToolOpen] = useState(false); const [expandedStyleToolOpen, setExpandedStyleToolOpen] = useState(false); const [effectToolOpen, setEffectToolOpen] = useState(false); const [expandedEffectToolOpen, setExpandedEffectToolOpen] = useState(false); const [motionToolOpen, setMotionToolOpen] = useState(false); const [expandedMotionToolOpen, setExpandedMotionToolOpen] = useState(false); const [expandedPromptOpen, setExpandedPromptOpen] = useState(false); const [expandedModalSize, setExpandedModalSize] = useState<{ width: number; height: number } | null>(null); const expandedModalRef = useRef(null); const [promptContentHeight, setPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, false)); const [expandedPromptContentHeight, setExpandedPromptContentHeight] = useState(() => estimatePromptContentHeight(savedPrompt, true)); const [manualPromptHeight, setManualPromptHeight] = useState(null); const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴) const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false); const [autoLinkEnabled, setAutoLinkEnabled] = useState(true); const resolvedMentionReferences = useResolvedCanvasResourceReferences(mentionReferences, { projectId }); const promptToolReferences = useMemo(() => parseToolMentionTokens(prompt).map(({toolId, label, type, icon}) => buildToolMentionReference(toolId, label, type, icon)), [prompt]); const textareaReferences = useMemo(() => { if (!promptToolReferences.length) return resolvedMentionReferences; const existingIds = new Set(resolvedMentionReferences.map((r) => r.id)); const extras = promptToolReferences.filter((r) => !existingIds.has(r.id)); return extras.length ? [...resolvedMentionReferences, ...extras] : resolvedMentionReferences; }, [resolvedMentionReferences, promptToolReferences]); // 当前提示词持有的九宫格工具图标,用于触发按钮回显已选工具。 const activeNineGridIcon = useMemo(() => parseToolMentionTokens(prompt).find((tool) => tool.type === "nine_grid")?.icon ?? "Grid3x3", [prompt]); const activeStyleTool = parseToolMentionTokens(prompt).find(t => t.type === "style"); const activeEffectTool = parseToolMentionTokens(prompt).find(t => t.type === "effect"); // 当前提示词持有的运镜工具标签(可多个),用于运镜按钮回显与菜单高亮。 const activeMotionTools = useMemo(() => parseToolMentionTokens(prompt).filter((tool) => tool.type === "motion"), [prompt]); const activeMotionTool = activeMotionTools[0]; const normalizedSavedPrompt = useMemo(() => normalizeCanvasNodeMentionTokens(savedPrompt, mentionReferences), [mentionReferences, savedPrompt]); const activeReferences = resolvedMentionReferences.filter((item) => item.active && item.kind !== "skill" && item.kind !== "tool"); const requirements: ModelRequirements = { capability: mode, input: { textCount: (prompt.trim() ? 1 : 0) + activeReferences.filter((item) => item.kind === "text").length, imageCount: activeReferences.filter((item) => item.kind === "image").length, videoCount: activeReferences.filter((item) => item.kind === "video").length, audioCount: activeReferences.filter((item) => item.kind === "audio").length, characterCount: activeReferences.filter((item) => item.kind === "character").length, }, videoOperation: node.metadata?.videoEditOperation, videoSeconds: mode === "video" ? node.metadata?.seconds ?? globalConfig.videoSeconds : undefined, options: modelRequestOptions({ ...globalConfig, size: node.metadata?.size || globalConfig.size, quality: node.metadata?.quality || globalConfig.quality, count: String(node.metadata?.count || globalConfig.count), transparentBackground: node.metadata?.transparentBackground || globalConfig.transparentBackground, videoSeconds: node.metadata?.seconds ?? globalConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio ?? globalConfig.videoGenerateAudio, videoWatermark: node.metadata?.watermark ?? globalConfig.videoWatermark, audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice, audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat, audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed, }, mode), }; const config = buildNodeConfig(globalConfig, node, mode, requirements); const resolvedRequirements: ModelRequirements = { ...requirements, options: modelRequestOptions(config, mode), videoSeconds: mode === "video" ? config.videoSeconds : undefined, }; const promptOptimizerProvider = useMemo(() => { if (!promptOptimizerEnabled || !promptOptimizerInstallation || !promptOptimizerPlugin.createPromptOptimizer) return null; return promptOptimizerPlugin.createPromptOptimizer(createPluginHostContext(promptOptimizerPlugin, promptOptimizerInstallation, globalConfig)); }, [globalConfig, promptOptimizerEnabled, promptOptimizerInstallation]); const generationCount = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1))); const priceChannel = resolveModelChannel(config, config.model); const configuredCredits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? generationCount : 1, seconds: mode === "video" ? config.videoSeconds : 1, capability: mode, config, requirements: resolvedRequirements, }); const quoteRequest = modelQuoteRequest(config, config.model, mode, resolvedRequirements); const quoteRequestKey = JSON.stringify(quoteRequest || null); const [routeQuote, setRouteQuote] = useState(null); const credits = routeQuote ? routeQuote.amountMicrocredits / 1_000_000 : configuredCredits; const activeReferenceCount = activeReferences.length; const videoFrameOptions = resolvedMentionReferences.filter((item) => item.active && item.kind === "image").map((item) => ({ nodeId: item.nodeId, label: item.label, title: item.title, previewUrl: item.previewUrl })); const hasVideoPromptTools = mode === "video" && !simpleMode && videoFrameOptions.length > 0; const monochromeAccent = theme.node.activeStroke; const composerTokens = { "--canvas-composer-surface": theme.node.panel, "--canvas-composer-control-surface": theme.toolbar.itemHover, "--canvas-composer-control-hover": theme.toolbar.activeBg, "--canvas-composer-shadow": theme.node.shadow, "--cn-text": theme.node.text, } as CSSProperties; const composerSurfaceStyle = { ...composerTokens, background: theme.node.panel, color: theme.node.text, boxShadow: theme.node.shadow, } as CSSProperties; const controlSurface = "var(--canvas-composer-control-surface)"; const promptBounds = promptEditorBounds(false, activeReferenceCount > 0); const expandedPromptBounds = promptEditorBounds(true, activeReferenceCount > 0); const composerHeight = clampPromptHeight(manualPromptHeight ?? promptContentHeight + (activeReferenceCount ? PROMPT_REFERENCE_SHELF_HEIGHT : 0), promptBounds); const expandedComposerHeight = clampPromptHeight(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 canExpandPrompt = mode === "image" || mode === "video"; const canOptimizePrompt = Boolean(promptOptimizerProvider) && canExpandPrompt; const isPortraitTexture = mode === "image" && Boolean(node.metadata?.portraitTexture); const autoMentionedPrompt = useMemo(() => autoMentionCanvasResourceReferences(prompt, resolvedMentionReferences), [prompt, resolvedMentionReferences]); const canAutoMention = autoMentionedPrompt !== prompt; useEffect(() => { promptRef.current = normalizedSavedPrompt; setPrompt(normalizedSavedPrompt); if (normalizedSavedPrompt !== savedPrompt) onPromptChange(node.id, normalizedSavedPrompt); }, [node.id, normalizedSavedPrompt, onPromptChange, savedPrompt]); useEffect(() => { setExpandedPromptOpen(false); setExpandedPresetOpen(false); setExpandedModalSize(null); setPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, false)); setExpandedPromptContentHeight(estimatePromptContentHeight(normalizedSavedPrompt, true)); setManualPromptHeight(null); }, [node.id]); useEffect(() => { if (!expandedPromptOpen) return; const constrainSize = () => setExpandedModalSize((size) => { if (!size) return size; const next = clampExpandedModalSize(size); return next.width === size.width && next.height === size.height ? size : next; }); constrainSize(); window.addEventListener("resize", constrainSize); return () => window.removeEventListener("resize", constrainSize); }, [expandedPromptOpen]); useEffect(() => { if (!creditsEnabled || !quoteRequest) { setRouteQuote(null); return; } const controller = new AbortController(); setRouteQuote(null); quoteModel(quoteRequest, controller.signal) .then(({ quote }) => setRouteQuote(quote)) .catch(() => { if (!controller.signal.aborted) setRouteQuote(null); }); return () => controller.abort(); // quoteRequestKey captures the full normalized request without retriggering on object identity. // eslint-disable-next-line react-hooks/exhaustive-deps }, [creditsEnabled, quoteRequestKey]); const skillReferences = useMemo(() => resolvedMentionReferences.filter((item) => item.kind === "skill"), [resolvedMentionReferences]); const updatePrompt = (value: string) => { promptRef.current = value; setPrompt(value); onPromptChange(node.id, value); if (showPromptTemplates && /(^|\s)\/[\p{L}\p{N}_-]*$/u.test(value)) { if (expandedPromptOpen) setExpandedPresetOpen(true); else setPresetOpen(true); } }; const updatePromptFromCurrent = (updater: (currentPrompt: string) => string) => { updatePrompt(updater(promptRef.current)); }; const applyPreset = (preset: CanvasPromptPreset) => { const withoutSlash = prompt.replace(/(^|\s)\/[\p{L}\p{N}_-]*$/u, "$1").trimEnd(); updatePrompt(withoutSlash ? `${withoutSlash}\n${preset.prompt}` : preset.prompt); }; const insertPromptReference = (reference: CanvasResourceReference) => { const insertText = `${canvasResourceMentionToken(reference)} `; const pendingMentionMatch = /@[^\s@,。!?、,.!?;:]*\s*$/.exec(prompt); const basePrompt = pendingMentionMatch ? prompt.slice(0, pendingMentionMatch.index) : prompt; // 同类型工具标签(style/nine_grid/effect)唯一:已有旧标签时原位覆盖,不再追加。 const overwrittenPrompt = overwriteSameTypeToolMention(basePrompt, reference); if (overwrittenPrompt != null) { updatePrompt(overwrittenPrompt.replace(/\s+$/, "")); return; } const trimmedBase = basePrompt.replace(/\s*$/, ""); updatePrompt(trimmedBase ? `${trimmedBase} ${insertText}` : insertText); }; const removeMotionToolMention = () => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt, "motion")); const submit = () => { const text = prompt.trim(); if (!text || isRunning) return false; if (mode === "text" && node.metadata?.listMode && onListGenerate) onListGenerate(node.id, text); else onGenerate(node.id, mode, text); return true; }; const submitExpandedPrompt = () => { if (submit()) { setExpandedPresetOpen(false); setExpandedNineGridOpen(false); setExpandedPromptOpen(false); } }; const renderComposerHeader = (expanded: boolean) => (
{ const target = event.target instanceof Element ? event.target : null; if (!target?.closest("button, input, textarea, select, a, [contenteditable=\"true\"], [data-canvas-no-drag]")) onNodeMouseDown?.(event, node.id); }} > {isPortraitTexture ? ( <> onConfigChange(node.id, { portraitTexture })} /> {activeReferenceCount > 0 ? {referenceShelfHeading(activeReferences)} : null} ) : (
{modeDisplayName(mode)}生成 {activeReferenceCount > 0 ? {referenceShelfHeading(activeReferences)} : null}
)}
{!simpleMode && (mode === "image" || mode === "video") ?
e.stopPropagation()}> {mode === "image" ? <> updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"style"},"Palette"))} onClear={() => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt,"style"))} /> updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"nine_grid"},icon))} /> : <> updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"effect"},"Sparkles"))} onClear={() => updatePromptFromCurrent((currentPrompt) => removeToolMentions(currentPrompt,"effect"))} /> t.toolId)} activeLabel={activeMotionTool?.label} onSelect={(id,label) => updatePromptFromCurrent((currentPrompt) => applyToolMention(currentPrompt,{id,label,type:"motion"},"Camera"))} onClear={removeMotionToolMention} /> }
: null} {showPromptTemplates ? : null} {canOptimizePrompt ? ( ) : null} {!expanded && canExpandPrompt ? ( ) : null} {!expanded && onClose ? ( ) : null}
); const renderSubmitButton = (expanded: boolean) => { const showCost = creditsEnabled && credits !== null; const formattedCredits = credits?.toLocaleString("zh-CN", { maximumFractionDigits: 6 }); const actionLabel = isRunning ? "生成中" : showCost ? `${routeQuote?.estimated ? "预估" : "消耗"} ${formattedCredits} 积分,生成` : "生成"; return ( ); }; const renderComposerControls = (expanded: boolean) => simpleMode ? (
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"} updatePrompt(autoMentionedPrompt)} onAutoLinkEnabledChange={setAutoLinkEnabled} accent={monochromeAccent} compact /> {renderSubmitButton(expanded)}
) : (
onConfigChange(node.id, mode === "image" ? { model, ...defaultImageParamsForModel(config, model) } : { model })} capability={mode} requirements={resolvedRequirements} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} showOptionPrices={creditsEnabled} variant="creation" showConfiguredModelName />
updatePrompt(autoMentionedPrompt)} onAutoLinkEnabledChange={setAutoLinkEnabled} accent={monochromeAccent} compact={!expanded} /> {mode === "text" ? ( <>
{!node.metadata?.listMode ? ( onConfigChange(node.id, { textCount: Math.max(1, Math.min(15, Math.floor(Math.abs(Number(value) || 1)))), }) } aria-label="文本生成份数" className="!w-14 !h-7 [&_.ant-input-number-input]:!text-[var(--fs-tiny)]" /> ) : 行数和列结构由模型判断} ) : mode === "image" ? ( // 图片模式下,显示相机配置与镜头配置 <> onConfigChange(node.id, { cameraControl: options })} theme={theme} compact={!expanded} /> onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })} onMissingConfig={() => navigateToSettings({ continueCreation: true })} onOpenChange={expanded ? undefined : onImageSettingsOpenChange} /> ) : mode === "video" ? ( onConfigChange(node.id, videoConfigPatch(key, value))} /> ) : mode === "audio" ? ( onConfigChange(node.id, audioConfigPatch(key, value))} /> ) : null} {renderSubmitButton(expanded)}
); const renderPromptEditor = (expanded: boolean, fill = false) => { const bounds = expanded ? expandedPromptBounds : promptBounds; const height = expanded ? expandedComposerHeight : composerHeight; return ( <>
onRemoveReference?.(node.id, reference)} onReorder={onReorderReferences ? (orderedNodeIds) => onReorderReferences(node.id, orderedNodeIds) : undefined} onReplaceReference={onReplaceReference ? (oldReference, sourceNodeId) => onReplaceReference(node.id, oldReference, sourceNodeId) : undefined} onReplaceReferenceFiles={onReplaceReferenceFiles ? (oldReference, files) => onReplaceReferenceFiles(node.id, oldReference, files) : undefined} /> onAddReference(node.id, reference) : undefined} includeAssetLibrary onChange={updatePrompt} autoLinkEnabled={autoLinkEnabled} onReferenceFilesDrop={onReplaceReferenceFiles ? (reference, files) => onReplaceReferenceFiles(node.id, reference, files) : undefined} onContentSizeChange={expanded ? setExpandedPromptContentHeight : setPromptContentHeight} containerClassName="min-h-0 flex-1" className={expanded ? "thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-3 py-2.5 text-[var(--fs-body-lg)] leading-6 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35" : "thin-scrollbar h-full w-full resize-none overflow-y-auto border-none bg-transparent px-2.5 py-1.5 text-[var(--fs-body)] leading-5 !outline-none !ring-0 !shadow-none focus:!outline-none focus:!ring-0 focus:!shadow-none placeholder:text-current placeholder:opacity-35"} style={{ color: theme.node.text, outline: "none", boxShadow: "none" }} placeholder={promptPlaceholder(mode, hasImageContent, hasTextContent)} aria-label={`${modeDisplayName(mode)}提示词`} />
{!expanded && } ); }; return ( item.model === modelOptionName(config.model))?.protocol || priceChannel.interfaceType} config={globalConfig} optimizerModel={globalConfig.textModel} references={activeReferences} provider={promptOptimizerProvider} onClose={() => setPromptOptimizerOpen(false)} onApply={(nextPrompt) => updatePrompt(nextPrompt)} >
event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()} > {renderComposerHeader(false)} {renderPromptEditor(false)} {/* B区 参数区(对应 #98 决策2:默认折叠,手风琴展开)*/} {hasVideoPromptTools ? (
{paramsExpanded ? (
onConfigChange(node.id, patch)} />
) : null}
) : null} {renderComposerControls(false)} { setExpandedPresetOpen(false); setExpandedNineGridOpen(false); setExpandedPromptOpen(false); }} styles={{ container: { border: 0, borderRadius: "var(--canvas-composer-radius)", padding: 0, overflow: "hidden", background: theme.node.panel, boxShadow: theme.node.shadow }, body: { minHeight: 0, padding: 0 }, }} >
{renderComposerHeader(true)}
{renderPromptEditor(true, Boolean(expandedModalSize))} {hasVideoPromptTools ? (
onConfigChange(node.id, patch)} />
) : null}
{renderComposerControls(true)}
); } function ReferenceToolsPopover({ canAutoMention, autoLinkEnabled, onAutoMention, onAutoLinkEnabledChange, accent, compact }: { canAutoMention: boolean; autoLinkEnabled: boolean; onAutoMention: () => void; onAutoLinkEnabledChange: (enabled: boolean) => void; accent: string; compact: boolean }) { return (
智能引用
输入素材序号或名称后按 Tab,可快速引用
AutoLink
} >
); } function GenerationModeIcon({ mode }: { mode: CanvasNodeGenerationMode }) { if (mode === "image") return ; if (mode === "video") return