import { memo, useEffect, useMemo, useState, type KeyboardEvent } from "react"; import { Input, Modal } from "antd"; import { AudioLines, BookOpenText, Clock3, FileText, Image, Pencil, Search, Video } from "lucide-react"; import { WorkspaceState } from "@/components/layout/workspace-state"; import { canvasNodeMaterialSummary, canvasNodeSearchContext, canvasNodeSearchTimes, searchCanvasNodes } from "@/lib/canvas/canvas-node-search"; import { canvasNodeVideoPreviewUrl } from "@/lib/canvas/canvas-media-preview"; import { getNodeListLabel } from "@/lib/canvas/node-registry"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; const RESULT_LIST_ID = "canvas-node-search-results"; export function CanvasNodeSearchModal({ open, nodes, onClose, onFocus }: { open: boolean; nodes: CanvasNodeData[]; onClose: () => void; onFocus: (nodeId: string) => void }) { const [query, setQuery] = useState(""); const [activeIndex, setActiveIndex] = useState(0); const results = useMemo(() => searchCanvasNodes(nodes, query), [nodes, query]); useEffect(() => setActiveIndex(0), [query, open]); useEffect(() => setActiveIndex((current) => Math.min(current, Math.max(0, results.length - 1))), [results.length]); const focusNode = (node: CanvasNodeData | undefined) => { if (!node) return; onFocus(node.id); onClose(); }; const handleInputKeyDown = (event: KeyboardEvent) => { if (event.key === "ArrowDown") { event.preventDefault(); setActiveIndex((current) => Math.min(results.length - 1, current + 1)); } else if (event.key === "ArrowUp") { event.preventDefault(); setActiveIndex((current) => Math.max(0, current - 1)); } else if (event.key === "Enter") { event.preventDefault(); focusNode(results[activeIndex]); } }; return ( { setQuery(""); setActiveIndex(0); }} styles={{ body: { paddingTop: 8 } }} centered > setQuery(event.target.value)} onKeyDown={handleInputKeyDown} prefix={} placeholder="搜索节点、章节、镜头、模型或标签…" aria-label="搜索画布节点" aria-controls={RESULT_LIST_ID} aria-activedescendant={results[activeIndex] ? `canvas-node-search-result-${results[activeIndex].id}` : undefined} />
{query.trim() ? `找到 ${results.length} 个节点` : `最近编辑 · ${results.length} 个节点`} ↑↓ 选择 · Enter 定位 · Esc 关闭
{results.length ? results.map((node, index) => ( setActiveIndex(index)} onSelect={() => focusNode(node)} /> )) : }
); } const CanvasNodeSearchResult = memo(function CanvasNodeSearchResult({ node, active, onActivate, onSelect }: { node: CanvasNodeData; active: boolean; onActivate: () => void; onSelect: () => void }) { const times = canvasNodeSearchTimes(node); const materialSummary = canvasNodeMaterialSummary(node); const context = canvasNodeSearchContext(node); return ( ); }); function CanvasNodeSearchThumbnail({ node }: { node: CanvasNodeData }) { const [failed, setFailed] = useState(false); const mediaSource = node.type === CanvasNodeType.Video ? canvasNodeVideoPreviewUrl(node) : node.metadata?.drawingPreviewUrl || node.metadata?.characterCoverUrl || node.metadata?.folder?.themeCover || ((node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Panorama || node.type === CanvasNodeType.ColorGrade) ? node.metadata?.content : undefined); const commonClass = "h-11 w-16 rounded-[var(--r-sm)] border object-cover"; const commonStyle = { borderColor: "color-mix(in srgb, var(--foreground) 9%, transparent)", background: "color-mix(in srgb, var(--foreground) 5%, transparent)" }; if (mediaSource && !failed) { return setFailed(true)} />; } const textPreview = node.metadata?.previewContent || node.metadata?.composerContent || node.metadata?.prompt || node.metadata?.content; if (textPreview && (node.type === CanvasNodeType.Text || node.type === CanvasNodeType.Markdown || node.type === CanvasNodeType.Script)) { return ; } return ( ); } function fullTime(value?: string) { if (!value) return "时间未记录"; return new Date(value).toLocaleString("zh-CN", { hour12: false }); }