feat(canvas): 智能引用 - 支持素材自动补全与全文引用
This commit is contained in:
1 parent
bacd502f5c
commit
0198458551
7 files changed
+402
-16
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { Button, Image as AntImage, InputNumber, Modal } from "antd";
|
||||
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 { ArrowUp, Boxes, ChevronDown, FileText, ImageIcon, ImagePlus, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
|
||||
import { ArrowUp, AtSign, Boxes, ChevronDown, FileText, 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,7 +23,7 @@ import { CanvasPresetPicker, type CanvasPromptPreset } from "./canvas-preset-pic
|
||||
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 { canvasResourceMentionToken, normalizeCanvasNodeMentionTokens, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { autoMentionCanvasResourceReferences, canvasResourceMentionToken, normalizeCanvasNodeMentionTokens, 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 +79,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
const [manualExpandedPromptHeight, setManualExpandedPromptHeight] = useState<number | null>(null);
|
||||
const [paramsExpanded, setParamsExpanded] = useState(false); // #98 决策2:B区参数区折叠状态(手风琴)
|
||||
const [promptOptimizerOpen, setPromptOptimizerOpen] = useState(false);
|
||||
const [autoLinkEnabled, setAutoLinkEnabled] = useState(true);
|
||||
const resolvedMentionReferences = useResolvedCanvasResourceReferences(mentionReferences);
|
||||
const normalizedSavedPrompt = useMemo(() => normalizeCanvasNodeMentionTokens(savedPrompt, mentionReferences), [mentionReferences, savedPrompt]);
|
||||
const activeReferences = resolvedMentionReferences.filter((item) => item.active && item.kind !== "skill");
|
||||
@@ -159,6 +160,8 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
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(() => {
|
||||
setPrompt(normalizedSavedPrompt);
|
||||
@@ -339,6 +342,14 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
<span className="min-w-0 truncate px-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
|
||||
{activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"}
|
||||
</span>
|
||||
<ReferenceToolsPopover
|
||||
canAutoMention={canAutoMention}
|
||||
autoLinkEnabled={autoLinkEnabled}
|
||||
onAutoMention={() => updatePrompt(autoMentionedPrompt)}
|
||||
onAutoLinkEnabledChange={setAutoLinkEnabled}
|
||||
accent={monochromeAccent}
|
||||
compact
|
||||
/>
|
||||
{renderSubmitButton(expanded)}
|
||||
</div>
|
||||
) : (
|
||||
@@ -360,6 +371,14 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-1">
|
||||
<ReferenceToolsPopover
|
||||
canAutoMention={canAutoMention}
|
||||
autoLinkEnabled={autoLinkEnabled}
|
||||
onAutoMention={() => updatePrompt(autoMentionedPrompt)}
|
||||
onAutoLinkEnabledChange={setAutoLinkEnabled}
|
||||
accent={monochromeAccent}
|
||||
compact={!expanded}
|
||||
/>
|
||||
{mode === "text" ? (
|
||||
<Tooltip title={`文本生成份数(默认 1,可在生成配置中调整)`}>
|
||||
<InputNumber
|
||||
@@ -411,6 +430,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
references={resolvedMentionReferences}
|
||||
includeAssetLibrary
|
||||
onChange={updatePrompt}
|
||||
autoLinkEnabled={autoLinkEnabled}
|
||||
onContentSizeChange={expanded ? setExpandedPromptContentHeight : setPromptContentHeight}
|
||||
containerClassName="min-h-0 flex-1"
|
||||
className={expanded
|
||||
@@ -515,6 +535,59 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
);
|
||||
}
|
||||
|
||||
function ReferenceToolsPopover({ canAutoMention, autoLinkEnabled, onAutoMention, onAutoLinkEnabledChange, accent, compact }: { canAutoMention: boolean; autoLinkEnabled: boolean; onAutoMention: () => void; onAutoLinkEnabledChange: (enabled: boolean) => void; accent: string; compact: boolean }) {
|
||||
return (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="topRight"
|
||||
rootClassName="canvas-reference-tools-popover"
|
||||
arrow={false}
|
||||
align={{ offset: [0, -8] }}
|
||||
styles={{ root: { width: "min(280px, calc(100vw - 24px))" }, container: { width: "100%" }, content: { width: "100%", padding: 12 } }}
|
||||
content={
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">智能引用</div>
|
||||
<div className="mt-1 text-xs text-black/50 dark:text-white/50">输入素材序号或名称后按 Tab,可快速引用</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-sm"><Link2 className="size-3.5" />AutoLink</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={autoLinkEnabled}
|
||||
aria-label={autoLinkEnabled ? "关闭 AutoLink" : "开启 AutoLink"}
|
||||
className={`relative h-5 w-9 rounded-full transition-colors ${autoLinkEnabled ? "" : "bg-black/20 dark:bg-white/20"}`}
|
||||
style={autoLinkEnabled ? { background: accent } : undefined}
|
||||
onClick={() => onAutoLinkEnabledChange(!autoLinkEnabled)}
|
||||
>
|
||||
<span className={`absolute left-0.5 top-0.5 size-4 rounded-full bg-white shadow-sm transition-transform ${autoLinkEnabled ? "translate-x-4" : "translate-x-0"}`} />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-1.5 rounded-md bg-[var(--canvas-composer-control-surface)] px-2 py-1.5 text-sm transition-colors hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
disabled={!canAutoMention}
|
||||
onClick={onAutoMention}
|
||||
>
|
||||
<AtSign className="size-3.5" />一键引用全文
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`canvas-node-composer-settings-trigger canvas-node-composer-reference-tools-trigger inline-flex shrink-0 items-center gap-1 ${compact ? "is-compact" : ""}`}
|
||||
aria-label="打开智能引用"
|
||||
title="智能引用"
|
||||
>
|
||||
<SlidersHorizontal className="size-3" />
|
||||
{!compact ? <span>引用</span> : null}
|
||||
</button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function GenerationModeIcon({ mode }: { mode: CanvasNodeGenerationMode }) {
|
||||
if (mode === "image") return <ImagePlus className="size-3" />;
|
||||
if (mode === "video") return <Video className="size-3" />;
|
||||
@@ -543,10 +616,11 @@ function ConnectedReferenceShelf({ references, theme, onInsert, onRemove }: { re
|
||||
<>
|
||||
<div className="canvas-node-composer-references" role="group" aria-label="已连接素材">
|
||||
<div className="canvas-node-composer-references-track thin-scrollbar">
|
||||
{activeReferences.map((reference) => {
|
||||
{activeReferences.map((reference, index) => {
|
||||
const canPreview = Boolean(reference.previewUrl) && (reference.kind === "image" || reference.kind === "character" || reference.kind === "video");
|
||||
return (
|
||||
<span key={reference.id} className="canvas-node-reference-chip">
|
||||
<span className="canvas-node-reference-order" aria-hidden>{index + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="canvas-node-reference-preview"
|
||||
|
||||
Reference in new issue
Block a user