import { useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties, KeyboardEvent, MouseEvent, PointerEvent } from "react"; import { Button, Image } from "antd"; import { FileText, Image as ImageIcon, Music2, Video, X } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { useThemeStore } from "@/stores/use-theme-store"; import type { NodeGenerationInput } from "./canvas-node-generation"; type CanvasConfigComposerProps = { value: string; inputs: NodeGenerationInput[]; onChange: (value: string) => void; onClose: () => void; }; type Token = | { type: "text"; value: string } | { type: "reference"; nodeId: string }; type MentionState = { query: string; }; export const CONFIG_REFERENCE_PATTERN = /@\[node:([^\]]+)\]/g; export function CanvasConfigComposer({ value, inputs, onChange, onClose }: CanvasConfigComposerProps) { const theme = canvasThemes[useThemeStore((state) => state.theme)]; const editorRef = useRef(null); const composingRef = useRef(false); const [mention, setMention] = useState(null); const [activeIndex, setActiveIndex] = useState(0); const [imagePreview, setImagePreview] = useState(null); const tokens = useMemo(() => parseComposerTokens(value), [value]); const referenceById = useMemo(() => new Map(inputs.map((input) => [input.nodeId, input])), [inputs]); const candidates = useMemo(() => { if (!mention) return []; const query = (mention.query || "").trim().toLowerCase(); if (!query) return inputs; return inputs.filter((input) => `${resourceLabel(input, inputs)} ${input.title} ${input.text || ""}`.toLowerCase().includes(query)); }, [inputs, mention]); useEffect(() => { if (document.activeElement === editorRef.current) return; const editor = editorRef.current; if (!editor) return; editor.textContent = ""; tokens.forEach((token) => { if (token.type === "text") { editor.append(document.createTextNode(token.value)); return; } const input = referenceById.get(token.nodeId); if (input) editor.append(createReferenceChip(input, inputs, theme, setImagePreview)); }); }, [inputs, referenceById, theme, tokens]); const syncFromEditor = () => { const editor = editorRef.current; if (!editor) return; const next = serializeEditor(editor); onChange(next); syncMention(); }; const syncMention = () => { const text = textBeforeCaret(); const match = /@([^\s@]*)$/.exec(text); if (!match || !inputs.length) { closeMention(); return; } setMention({ query: match[1] || "" }); setActiveIndex(0); }; const closeMention = () => { setMention(null); setActiveIndex(0); }; const insertReference = (input: NodeGenerationInput) => { const editor = editorRef.current; if (!editor) return; removeActiveMention(); const chip = createReferenceChip(input, inputs, theme, setImagePreview); const space = document.createTextNode(" "); const selection = window.getSelection(); const range = selection?.rangeCount ? selection.getRangeAt(0) : null; if (range) { range.insertNode(space); range.insertNode(chip); range.setStartAfter(space); range.collapse(true); selection?.removeAllRanges(); selection?.addRange(range); } else { editor.append(chip, space); placeCaretAtEnd(editor); } closeMention(); onChange(serializeEditor(editor)); }; const stopCanvasInteraction = (event: PointerEvent | MouseEvent) => event.stopPropagation(); return (
event.stopPropagation()} >
组装提示词
@ 引用已连接资产,发送前按当前连接重新编号
{!value.trim() ?
输入提示词,按 @ 引用连接的图片或文本
: null}
{ if (!composingRef.current) syncFromEditor(); }} onCompositionStart={() => { composingRef.current = true; }} onCompositionEnd={() => { composingRef.current = false; syncFromEditor(); }} onKeyDown={(event: KeyboardEvent) => { event.stopPropagation(); if (mention && candidates.length) { if (event.key === "ArrowDown") { event.preventDefault(); setActiveIndex((index) => (index + 1) % candidates.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); setActiveIndex((index) => (index - 1 + candidates.length) % candidates.length); return; } if (event.key === "Enter") { event.preventDefault(); insertReference(candidates[Math.min(activeIndex, candidates.length - 1)]); return; } if (event.key === "Escape") { event.preventDefault(); closeMention(); return; } } if ((event.key === "Backspace" || event.key === "Delete") && deleteAdjacentReference(event.key)) { event.preventDefault(); requestAnimationFrame(syncFromEditor); return; } requestAnimationFrame(syncMention); }} onBlur={() => window.setTimeout(closeMention, 120)} /> {mention && candidates.length ? : null}
{imagePreview ? 引用图片预览 !visible && setImagePreview(null) }} /> : null}
); } function MentionMenu({ inputs, allInputs, activeIndex, theme, onSelect }: { inputs: NodeGenerationInput[]; allInputs: NodeGenerationInput[]; activeIndex: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onSelect: (input: NodeGenerationInput) => void }) { const selectedRef = useRef(false); const activeItemRef = useRef(null); useEffect(() => { activeItemRef.current?.scrollIntoView({ block: "nearest" }); }, [activeIndex, inputs]); const selectInput = (input: NodeGenerationInput) => { if (selectedRef.current) return; selectedRef.current = true; onSelect(input); }; return (
{inputs.map((input, index) => ( ))}
); } function ResourcePreview({ input }: { input: NodeGenerationInput }) { if (input.type === "image" && input.image) return ; if (input.type === "video" && input.video) return