feat(canvas): 画布工作区 - 整合侧栏与风格预设并修复提示词回填
选择性整合 #547,保留主分支节点与工具条设计。新增默认折叠的节点、资产、任务和历史面板,风格与预设保留在节点内,移除重复入口。 补齐工具权限、提示词解析、资源引用保护及数据库迁移,修复提示词清空和连续编辑后的生成配置同步。 验证:前端全量 1925 项测试、构建、类型检查及 lint 通过;后端 go test ./... 通过。真实模型、PostgreSQL 实库与登录态页面验收仍待进行。 Refs #547 Co-authored-by: bensharp <bensharp@qq.com>
This commit is contained in:
1 parent
be5f2733b6
commit
e93bf3ca39
66 files changed
+6127
-97
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Button, Image as AntImage, InputNumber, Modal, Popover } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { ArrowLeftRight, ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText, GripVertical, ImageIcon, ImagePlus, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
|
||||
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, 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";
|
||||
@@ -23,10 +23,14 @@ import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textare
|
||||
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 { autoMentionCanvasResourceReferences, canvasResourceMentionToken, normalizeCanvasNodeMentionTokens, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
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";
|
||||
@@ -79,6 +83,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
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);
|
||||
@@ -86,6 +91,14 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const [prompt, setPrompt] = useState(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<HTMLDivElement>(null);
|
||||
@@ -96,8 +109,22 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
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");
|
||||
const activeReferences = resolvedMentionReferences.filter((item) => item.active && item.kind !== "skill" && item.kind !== "tool");
|
||||
const requirements: ModelRequirements = {
|
||||
capability: mode,
|
||||
input: {
|
||||
@@ -232,7 +259,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const updatePrompt = (value: string) => {
|
||||
setPrompt(value);
|
||||
onPromptChange(node.id, value);
|
||||
if (/(^|\s)\/[\p{L}\p{N}_-]*$/u.test(value)) {
|
||||
if (showPromptTemplates && /(^|\s)\/[\p{L}\p{N}_-]*$/u.test(value)) {
|
||||
if (expandedPromptOpen) setExpandedPresetOpen(true);
|
||||
else setPresetOpen(true);
|
||||
}
|
||||
@@ -246,15 +273,19 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const insertPromptReference = (reference: CanvasResourceReference) => {
|
||||
const insertText = `${canvasResourceMentionToken(reference)} `;
|
||||
const pendingMentionMatch = /@[^\s@,。!?、,.!?;:]*\s*$/.exec(prompt);
|
||||
if (pendingMentionMatch) {
|
||||
const prefix = prompt.slice(0, pendingMentionMatch.index).replace(/\s*$/, "");
|
||||
updatePrompt(prefix ? `${prefix} ${insertText}` : insertText);
|
||||
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 basePrompt = prompt.replace(/\s*$/, "");
|
||||
updatePrompt(basePrompt ? `${basePrompt} ${insertText}` : insertText);
|
||||
const trimmedBase = basePrompt.replace(/\s*$/, "");
|
||||
updatePrompt(trimmedBase ? `${trimmedBase} ${insertText}` : insertText);
|
||||
};
|
||||
|
||||
const removeMotionToolMention = () => updatePrompt(removeToolMentions(prompt, "motion"));
|
||||
|
||||
const submit = () => {
|
||||
const text = prompt.trim();
|
||||
if (!text || isRunning) return false;
|
||||
@@ -265,6 +296,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
const submitExpandedPrompt = () => {
|
||||
if (submit()) {
|
||||
setExpandedPresetOpen(false);
|
||||
setExpandedNineGridOpen(false);
|
||||
setExpandedPromptOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -293,8 +325,17 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
{activeReferenceCount > 0 ? <span className="canvas-node-composer-reference-heading">{referenceShelfHeading(activeReferences)}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-auto flex shrink-0 items-center justify-end gap-1">
|
||||
{!simpleMode ? <CanvasPresetPicker mode={mode} skillReferences={skillReferences} open={expanded ? expandedPresetOpen : presetOpen} onOpenChange={expanded ? setExpandedPresetOpen : setPresetOpen} onSelect={applyPreset} dense appearance="quiet" /> : null}
|
||||
<div className="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-1">
|
||||
{!simpleMode && (mode === "image" || mode === "video") ? <div className="canvas-node-tool-controls canvas-node-tool-controls-inline flex items-center gap-1" data-canvas-no-zoom data-canvas-wheel-scroll onPointerDown={e => e.stopPropagation()}>
|
||||
{mode === "image" ? <>
|
||||
<CanvasChooseImageStylePicker open={expanded ? expandedStyleToolOpen : styleToolOpen} onOpenChange={expanded ? setExpandedStyleToolOpen : setStyleToolOpen} activeToolId={activeStyleTool?.toolId} activeLabel={activeStyleTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"style"},"Palette"))} onClear={() => updatePrompt(removeToolMentions(prompt,"style"))} />
|
||||
<CanvasNineGridPicker open={expanded ? expandedNineGridOpen : nineGridOpen} onOpenChange={expanded ? setExpandedNineGridOpen : setNineGridOpen} icon={activeNineGridIcon} onSelect={(id,label,icon) => updatePrompt(applyToolMention(prompt,{id,label,type:"nine_grid"},icon))} />
|
||||
</> : <>
|
||||
<CanvasChooseEffectPicker open={expanded ? expandedEffectToolOpen : effectToolOpen} onOpenChange={expanded ? setExpandedEffectToolOpen : setEffectToolOpen} activeToolId={activeEffectTool?.toolId} activeLabel={activeEffectTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"effect"},"Sparkles"))} onClear={() => updatePrompt(removeToolMentions(prompt,"effect"))} />
|
||||
<CanvasChooseMotionPicker open={expanded ? expandedMotionToolOpen : motionToolOpen} onOpenChange={expanded ? setExpandedMotionToolOpen : setMotionToolOpen} activeToolIds={activeMotionTools.map(t => t.toolId)} activeLabel={activeMotionTool?.label} onSelect={(id,label) => updatePrompt(applyToolMention(prompt,{id,label,type:"motion"},"Camera"))} onClear={removeMotionToolMention} />
|
||||
</>}
|
||||
</div> : null}
|
||||
{showPromptTemplates ? <CanvasPresetPicker mode={mode} skillReferences={skillReferences} open={expanded ? expandedPresetOpen : presetOpen} onOpenChange={expanded ? setExpandedPresetOpen : setPresetOpen} onSelect={applyPreset} dense appearance="quiet" /> : null}
|
||||
{canOptimizePrompt ? (
|
||||
<Tooltip title="用 AI 润色提示词">
|
||||
<button
|
||||
@@ -478,7 +519,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
/>
|
||||
<CanvasResourceMentionTextarea
|
||||
value={prompt}
|
||||
references={resolvedMentionReferences}
|
||||
references={textareaReferences}
|
||||
onSelectReference={onAddReference ? (reference) => onAddReference(node.id, reference) : undefined}
|
||||
includeAssetLibrary
|
||||
onChange={updatePrompt}
|
||||
@@ -565,6 +606,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
|
||||
destroyOnHidden
|
||||
onCancel={() => {
|
||||
setExpandedPresetOpen(false);
|
||||
setExpandedNineGridOpen(false);
|
||||
setExpandedPromptOpen(false);
|
||||
}}
|
||||
styles={{
|
||||
@@ -684,7 +726,7 @@ function ConnectedReferenceShelf({
|
||||
onReplaceReference?: (oldReference: CanvasResourceReference, sourceNodeId: string) => void;
|
||||
onReplaceReferenceFiles?: (oldReference: CanvasResourceReference, files: File[]) => void;
|
||||
}) {
|
||||
const activeReferences = references.filter((item) => item.active && item.kind !== "skill");
|
||||
const activeReferences = references.filter((item) => item.active && item.kind !== "skill" && item.kind !== "tool");
|
||||
const [imagePreview, setImagePreview] = useState<CanvasResourceReference | null>(null);
|
||||
const [draggedReferenceId, setDraggedReferenceId] = useState<string | null>(null);
|
||||
const [dropTargetReferenceId, setDropTargetReferenceId] = useState<string | null>(null);
|
||||
|
||||
Reference in new issue
Block a user