feat(ui): 自研 Celadon 组件族入库与 AntD 消费全量替换 - base(Kbd/StatusBadge/IconButton/ToolButton)+product(EmptyState/Callout/SegmentedControl/Checkbox/Select/Tooltip/Switch), composio 视觉基底, globals.css 覆盖清理 (ADR-0008)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Alert, Button, Empty, Modal, Tag } from "antd";
|
||||
import { Button, Modal, Tag } from "antd";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
import { ArrowLeft, ArrowRight, CheckCircle2, ChevronDown, ChevronRight, Copy, FileText, Image as ImageIcon, LoaderCircle, RefreshCw, Sparkles, Target, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
@@ -27,6 +28,8 @@ import { modelOptionLabel, useEffectiveConfig } from "@/stores/use-config-store"
|
||||
import { usePluginStore } from "@/stores/use-plugin-store";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
import { IconButton } from "@/components/ui/base/buttons";
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
|
||||
type AiArtCritiqueModalProps = {
|
||||
node: CanvasNodeData | null;
|
||||
@@ -296,7 +299,7 @@ export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdat
|
||||
: "尚未分析"}
|
||||
</span>
|
||||
</div>
|
||||
<Button type="text" size="small" icon={<X className="size-4" />} onClick={close} aria-label="关闭批改报告" />
|
||||
<IconButton variant="ghost" size="sm" icon={X} onClick={close} aria-label="关闭批改报告" />
|
||||
</header>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col lg:grid lg:grid-cols-[minmax(0,1.45fr)_minmax(360px,.85fr)]">
|
||||
@@ -305,7 +308,7 @@ export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdat
|
||||
{previewUrl ? (
|
||||
<img src={previewUrl} alt={input?.title || "输入图片"} className="max-h-full max-w-full rounded-[var(--r-md)] object-contain shadow-[var(--shadow-md)]" draggable={false} />
|
||||
) : (
|
||||
<Empty image={<ImageIcon className="mx-auto size-10 opacity-25" />} description={input ? "正在加载图片" : "还没有图片输入"} />
|
||||
<EmptyState size="compact" icon={ImageIcon} title={input ? "正在加载图片" : "还没有图片输入"} />
|
||||
)}
|
||||
{overlayIssues.length && previewUrl ? <CritiqueOverlay issues={overlayIssues} issueNumberById={issueNumberById} selectedIssueId={overlayIssues[0]?.id || null} labelPlacements={labelPlacements} onSelect={openIssue} /> : null}
|
||||
</div>
|
||||
@@ -333,10 +336,10 @@ export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdat
|
||||
<aside className="min-h-0 flex-1 overflow-y-auto" data-canvas-wheel-scroll>
|
||||
<div className="flex min-h-full flex-col gap-4 p-4">
|
||||
{running || visibleState.status === "running" ? <ArtCritiqueProgress stage={progressStage} theme={theme} /> : null}
|
||||
{draftReportVisible && visibleState.status === "running" && visibleState.report ? <Alert type="info" showIcon message="报告初稿已生成" description="问题和建议已先展示,正在精确定位并复核矩形标注。" /> : null}
|
||||
{!enabled && !visibleState.report ? <Alert type="warning" showIcon message="插件尚未启用" description="请先到插件管理中开启 AI 审美批改,再运行分析。" /> : null}
|
||||
{visibleState.status === "stale" ? <Alert type="warning" showIcon message="输入图片已经变化" description="当前报告针对旧图片生成,请重新批改。" /> : null}
|
||||
{localError || visibleState.errorMessage ? <Alert type="error" showIcon message="批改失败" description={localError || visibleState.errorMessage} /> : null}
|
||||
{draftReportVisible && visibleState.status === "running" && visibleState.report ? <Callout tone="info" title="报告初稿已生成">问题和建议已先展示,正在精确定位并复核矩形标注。</Callout> : null}
|
||||
{!enabled && !visibleState.report ? <Callout tone="warning" title="插件尚未启用">请先到插件管理中开启 AI 审美批改,再运行分析。</Callout> : null}
|
||||
{visibleState.status === "stale" ? <Callout tone="warning" title="输入图片已经变化">当前报告针对旧图片生成,请重新批改。</Callout> : null}
|
||||
{localError || visibleState.errorMessage ? <Callout tone="error" title="批改失败">{localError || visibleState.errorMessage}</Callout> : null}
|
||||
|
||||
{view === "detail" && detailIssue ? (
|
||||
<IssueDetailView
|
||||
@@ -355,7 +358,7 @@ export function AiArtCritiqueModal({ node, upstreamNodes, open, onClose, onUpdat
|
||||
{visibleState.report ? (
|
||||
<ReportSummary report={visibleState.report} theme={theme} isDraft={draftReportVisible && visibleState.status === "running"} />
|
||||
) : (
|
||||
<Empty className="my-auto" description="还没有批改报告" image={<FileText className="mx-auto size-10 opacity-25" />} />
|
||||
<EmptyState className="my-auto" size="compact" icon={FileText} title="还没有批改报告" />
|
||||
)}
|
||||
|
||||
{visibleState.report && !visibleState.report.issues.length ? (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Button } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useRef, useState, type ClipboardEvent as ReactClipboardEvent, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from "react";
|
||||
import { Button, Tooltip } from "antd";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { ArrowUp, CheckCircle2, CircleAlert, ImagePlus, LoaderCircle, RotateCcw, Sparkles, UserRound, Wrench, X, XCircle } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Button, Switch, Tooltip } from "antd";
|
||||
import { Button } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { BookOpenCheck, BookOpenText, Bot, Clapperboard, Focus, Globe2, History, LayoutTemplate, Laptop, PanelRightClose, PanelsTopLeft, Plus, RotateCcw, Workflow } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
@@ -89,7 +92,7 @@ export function AgentPanelChrome({
|
||||
<Button type="text" shape="circle" className="!h-6 !w-6 !min-w-6" disabled={!canUndo} style={{ color: theme.node.muted }} icon={<RotateCcw className="size-3" />} onClick={onUndo} aria-label="撤销最近一批 Agent 写回" />
|
||||
</Tooltip>
|
||||
<label className="flex h-6 cursor-pointer items-center gap-1 rounded-md px-1" style={{ color: theme.node.muted }}>
|
||||
<Switch size="small" checked={confirmTools} onChange={onConfirmToolsChange} />
|
||||
<Switch size="sm" checked={confirmTools} onChange={onConfirmToolsChange} />
|
||||
<span className="whitespace-nowrap">确认</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Button, Modal, Segmented, Select } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { Copy, Cpu, Settings2, Trash2, X } from "lucide-react";
|
||||
import { Button, Modal, Segmented, Select, Tooltip } from "antd";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { modelDisplayName, modelIcon, normalizeModelOptionValue, resolveModelChannel, resolveModelRequestConfig, selectableModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Button, Input, InputNumber, Segmented, Select, Slider } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
|
||||
import { ChevronDown, Dice5, Image as ImageIcon, LoaderCircle, MessageSquare, Music2, Play, Sparkles, Video, Workflow as WorkflowIcon } from "lucide-react";
|
||||
import { Button, Input, InputNumber, Segmented, Select, Slider, Switch, Tooltip } from "antd";
|
||||
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
|
||||
import { configuredModelMatchesCapability, defaultConfig, modelOptionName, normalizeRunningHubCapability, resolveModelChannel, useEffectiveConfig, type AiConfig, type RunningHubCapability, type RunningHubWorkflow, type RunningHubWorkflowKind } from "@/stores/use-config-store";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
@@ -400,7 +403,7 @@ export function WorkflowParameterControls({ fields, node, theme, onConfigChange,
|
||||
const control = fieldType === "SELECT" || selectOptions.length ? (
|
||||
<Select status={valueError ? "error" : undefined} size="small" className="w-full" value={value === "" ? undefined : value as string | number} options={selectOptions.map((option) => ({ label: workflowParameterOptionLabel(option), value: workflowParameterOptionValue(option) }))} onChange={(next) => update(field, next)} />
|
||||
) : fieldType === "BOOLEAN" || typeof value === "boolean" ? (
|
||||
<Switch size="small" checked={value === true || value === "true"} onChange={(checked) => update(field, checked)} />
|
||||
<Switch size="sm" checked={value === true || value === "true"} onChange={(checked) => update(field, checked)} />
|
||||
) : fieldType === "SLIDER" && bounds.min !== undefined && bounds.max !== undefined ? (
|
||||
<div className="grid min-w-0 grid-cols-[minmax(0,1fr)_64px] items-center gap-2">
|
||||
<Slider disabled={randomEnabled} className="m-0" min={bounds.min} max={bounds.max} step={bounds.step || 0.01} value={numericValue ?? bounds.min} tooltip={{ open: false }} onChange={(next) => update(field, next)} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { Bot, PanelBottom, X, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { Tooltip } from "antd";
|
||||
|
||||
|
||||
import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Drawer, Button, Input, Popconfirm } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Drawer, Tag, Button, Input, Popconfirm, Empty, Tooltip } from "antd";
|
||||
|
||||
import { Clock, Clock3, ExternalLink, History, Search, Trash2, X } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
|
||||
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useCanvasHistoryStore } from "@/stores/canvas/use-canvas-history-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { EmptyState } from "@/components/ui/product/empty-state";
|
||||
|
||||
type TimelineItem = {
|
||||
id: string;
|
||||
@@ -140,9 +145,7 @@ export function CanvasHistoryDrawer({ open, onClose }: { open: boolean; onClose:
|
||||
|
||||
{/* 时间线列表 */}
|
||||
{filteredItems.length === 0 ? (
|
||||
<div className="py-12">
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无匹配的历史记录" />
|
||||
</div>
|
||||
<EmptyState description="暂无匹配的历史记录" />
|
||||
) : (
|
||||
<div className="relative border-l-2 border-stone-200 dark:border-stone-800 pl-4 space-y-4 pt-1">
|
||||
{filteredItems.map((item) => {
|
||||
@@ -178,13 +181,9 @@ export function CanvasHistoryDrawer({ open, onClose }: { open: boolean; onClose:
|
||||
</button>
|
||||
)}
|
||||
{isDeleted ? (
|
||||
<Tag color="error" className="m-0 text-[11px] leading-tight px-1.5 py-0.5 font-medium">
|
||||
已删除
|
||||
</Tag>
|
||||
<StatusBadge tone="error" size="sm" label="已删除" className="m-0" />
|
||||
) : (
|
||||
<Tag color="success" className="m-0 text-[11px] leading-tight px-1.5 py-0.5 font-medium">
|
||||
活跃中
|
||||
</Tag>
|
||||
<StatusBadge tone="success" size="sm" label="活跃中" className="m-0" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { App, Button, Segmented } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { App, Button, Segmented, Tooltip } from "antd";
|
||||
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { CheckCircle2, Copy, ExternalLink, FolderOpen, History, LoaderCircle, PlugZap, Plus, RefreshCw, Terminal, Trash2 } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Segmented, Slider } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { Segmented, Slider, Tooltip } from "antd";
|
||||
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { Camera, RotateCcw, Send, X } from "lucide-react";
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Button, Modal, Slider } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { Button, Modal, Slider, Tooltip } from "antd";
|
||||
|
||||
import { Brush, Eraser, Redo2, RotateCcw, Save, Undo2 } from "lucide-react";
|
||||
|
||||
import { imageToDataUrl } from "@/services/image-storage";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent } from "react";
|
||||
import { ArrowUp, AtSign, ChevronDown, FileText, ImageIcon, ImagePlus, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
|
||||
import { Button, Image as AntImage, InputNumber, Modal, Tooltip } from "antd";
|
||||
import { Button, Image as AntImage, InputNumber, Modal } 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, AtSign, Boxes, ChevronDown, FileText, ImageIcon, ImagePlus, 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";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { Alert, App, Button, Dropdown, Input, Popover, Tag, Typography } from "antd";
|
||||
import { App, Button, Dropdown, Input, Popover, Tag, Typography } from "antd";
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
import { Bubble, Sender, type BubbleItemType } from "@ant-design/x";
|
||||
import { ArrowUp, Check, ChevronDown, FileText, Image as ImageIcon, LoaderCircle, Music2, Sparkles, UserRound, Video, X } from "lucide-react";
|
||||
|
||||
@@ -543,7 +544,7 @@ export function CanvasPromptOptimizerDrawer({ open, children, prompt, generation
|
||||
bubbleItems.push({
|
||||
key: "error",
|
||||
role: "system",
|
||||
content: <Alert className="canvas-prompt-optimizer-alert" type="error" showIcon message="优化失败" description={error} />,
|
||||
content: <Callout className="canvas-prompt-optimizer-alert" tone="error" title="优化失败">{error}</Callout>,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Button, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
|
||||
import { Button, Checkbox, Dropdown, Input, InputNumber, Modal, Segmented, Select, Table, Tooltip } from "antd";
|
||||
|
||||
import { CheckboxGroup } from "@/components/ui/base/checkbox";
|
||||
import type { MenuProps } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { ChevronDown, ChevronUp, Clapperboard, Copy, Expand, Film, Grid3X3, Image as ImageIcon, ListTree, Merge, MoreHorizontal, Plus, RefreshCw, Send, Square, Trash2, Video } from "lucide-react";
|
||||
@@ -718,7 +721,7 @@ export function CanvasScriptEditor({
|
||||
<Modal title={node?.title || "分镜脚本"} open={open} onCancel={onClose} footer={null} width="min(1480px, calc(100vw - 40px))" centered destroyOnHidden>
|
||||
<div className="mb-3 flex flex-wrap items-center gap-2">
|
||||
<Input.Search className="w-72" allowClear placeholder="筛选画面、台词或提示词" value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
<Checkbox.Group className="script-column-picker" options={columnOptions} value={visibleColumns} onChange={(values) => onVisibleColumnsChange(values as StoryboardColumn[])} />
|
||||
<CheckboxGroup className="script-column-picker" options={columnOptions} value={visibleColumns} onChange={(values) => onVisibleColumnsChange(values)} />
|
||||
<span className="min-w-0 flex-1" />
|
||||
<Button icon={<Plus className="size-4" />} onClick={() => onUpdateRows([...rows, editorRow(rows.length + 1)])}>
|
||||
新增镜头
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { App, Button, ColorPicker, Input, InputNumber, Modal, Progress, Segmented, Switch } from "antd";
|
||||
import { App, Button, ColorPicker, Input, InputNumber, Modal, Progress, Segmented } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { Captions, FileDown, FileUp, ListPlus, LoaderCircle, Plus, Scissors, Sparkles, Trash2 } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
@@ -394,7 +395,7 @@ export function CanvasSubtitleDialog({ node, open, projectId, config, onClose, o
|
||||
<div className="border-t pt-3" style={{ borderColor: theme.toolbar.border }}>
|
||||
<div className="mb-2 flex items-center justify-between text-xs font-medium opacity-55">
|
||||
<span>关键词高亮</span>
|
||||
<Switch size="small" checked={style.highlightEnabled} onChange={(highlightEnabled) => setStyle((current) => ({ ...current, highlightEnabled }))} />
|
||||
<Switch size="sm" checked={style.highlightEnabled} onChange={(highlightEnabled) => setStyle((current) => ({ ...current, highlightEnabled }))} />
|
||||
</div>
|
||||
<div className="space-y-2.5">
|
||||
<label className="flex items-center justify-between gap-2 text-xs opacity-70">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { App, Button, ColorPicker, Dropdown, Input, Modal, Popover } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { App, Button, ColorPicker, Dropdown, Input, Modal, Popover, Tooltip } from "antd";
|
||||
|
||||
import type { Editor, JSONContent } from "@tiptap/core";
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { App, Button, Dropdown, Input, InputNumber, Modal, Progress } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
// 二期:多轨时间线编辑弹窗。
|
||||
// 数据源是项目级 TimelineProject:视频/音频节点自动入轨,字幕条目转字幕片段。
|
||||
// 交互:拖拽移动片段(吸附播放头/片段边缘)、左右边缘裁剪、删除、播放头跳转。
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from "react";
|
||||
import { App, Button, Dropdown, Input, InputNumber, Modal, Progress, Tooltip } from "antd";
|
||||
|
||||
import { Captions, Clapperboard, FolderOpen, Library, Lock, LockOpen, Maximize2, MoreHorizontal, Music2, Plus, Scissors, Trash2, Upload, Video, Wand2, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { saveAs } from "file-saver";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useRef, useState, type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
|
||||
import { Switch } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { Palette, Info } from "lucide-react";
|
||||
|
||||
import { FloatingDock } from "@/components/ui/aceternity/floating-dock";
|
||||
@@ -207,7 +207,7 @@ export function CanvasToolbar({
|
||||
<CanvasAppearanceControls appearance={appearance} backgroundMode={backgroundMode} colorTheme={colorTheme} theme={theme} onAppearanceChange={onAppearanceChange} onSaveAppearanceDefault={onSaveAppearanceDefault} onBackgroundModeChange={onBackgroundModeChange} />
|
||||
<div className="mt-2.5 flex items-center justify-between gap-2 rounded-[var(--dock-item-radius-labeled)] border px-2.5 py-2" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5 text-[var(--fs-tiny)] font-semibold"><Info className="size-3" />图片信息</span>
|
||||
<Switch size="small" checked={showImageInfo} onChange={onShowImageInfoChange} />
|
||||
<Switch size="sm" checked={showImageInfo} onChange={onShowImageInfoChange} />
|
||||
</div>
|
||||
</SpotlightSurface>
|
||||
</motion.div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { App, Button, ColorPicker, Dropdown, Input, InputNumber, Select, Slider, Switch } from "antd";
|
||||
import { App, Button, ColorPicker, Dropdown, Input, InputNumber, Select, Slider } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import type { MenuProps } from "antd";
|
||||
import { Box, BoxSelect, Camera, Circle, Cuboid, FileUp, Focus, Image as ImageIcon, LampDesk, Lightbulb, Plus, Redo2, RotateCcw, Save, Trash2, Undo2, UserRound, Video, X } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from "react";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
|
||||
|
||||
import { releaseDirectorFocusAfterPointer } from "@/lib/canvas/director/director-shortcuts";
|
||||
import { DIRECTOR_VIEW_MODES, type DirectorViewMode } from "@/lib/canvas/director/director-view-modes";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Tooltip } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
|
||||
import { Bone, Box, Camera, Compass, Crosshair, Layers, Lightbulb, Move3D, Palette, Rotate3D, Scaling, UserRound } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Alert, Button, InputNumber, Modal, Popconfirm, Segmented, Select, Switch, Tag } from "antd";
|
||||
import { Button, InputNumber, Modal, Popconfirm, Segmented, Select, Tag } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { Callout } from "@/components/ui/product/callout";
|
||||
import { StatusBadge } from "@/components/ui/base/badges";
|
||||
import { CircleAlert, Download, History, Image as ImageIcon, Pause, Play, ScanFace, ShieldCheck } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
@@ -300,27 +303,27 @@ export function PortraitClearanceModal({ projectId, node, upstreamNodes, open, o
|
||||
<label className="mb-3 block text-xs">去重方式<Select className="mt-1 w-full" value={state.settings.dedupMode} disabled={!enabled} options={[{ value: "phash", label: "pHash 快速" }, { value: "arcface", label: "ArcFace 双条件" }]} onChange={(value) => patchState({ settings: { ...state.settings, dedupMode: value as "phash" | "arcface" } })} /></label>
|
||||
{state.analysisMode === "local-plus-vision" ? <label className="mb-3 block text-xs">模型并发(最多 10)<InputNumber className="mt-1 w-full" min={1} max={10} precision={0} value={state.settings.modelConcurrency} disabled={!enabled} onChange={(value) => patchState({ settings: { ...state.settings, modelConcurrency: Math.max(1, Math.min(10, Number(value) || 2)) } })} /></label> : null}
|
||||
{state.mode === "network-search" ? <><label className="mb-3 block text-xs">候选上限<InputNumber className="mt-1 w-full" min={1} max={60} value={state.settings.maxCandidates} disabled={!enabled} onChange={(value) => patchState({ settings: { ...state.settings, maxCandidates: Math.max(1, Math.min(60, Number(value) || 30)) } })} /></label><label className="mb-3 block text-xs">百度滚动次数<InputNumber className="mt-1 w-full" min={0} max={20} value={state.settings.searchScrolls} disabled={!enabled} onChange={(value) => patchState({ settings: { ...state.settings, searchScrolls: Math.max(0, Math.min(20, Number(value) || 0)) } })} /></label></> : null}
|
||||
<div className="flex items-center justify-between gap-3 text-xs"><span>浏览器调试</span><Switch size="small" checked={state.settings.showBrowserForDebug} disabled={!enabled} onChange={(checked) => patchState({ settings: { ...state.settings, showBrowserForDebug: checked } })} /></div>
|
||||
<div className="flex items-center justify-between gap-3 text-xs"><span>浏览器调试</span><Switch size="sm" checked={state.settings.showBrowserForDebug} disabled={!enabled} onChange={(checked) => patchState({ settings: { ...state.settings, showBrowserForDebug: checked } })} /></div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main className="min-w-0 space-y-4">
|
||||
{!runtimeReady && enabled ? <Alert type={runtimeConnecting ? "info" : "warning"} showIcon message={runtimeConnecting ? "正在连接本机检测引擎" : runtimeStatus ? "本地模型未就绪" : "本机引擎连接失败"} description={runtimeConnecting ? "正在自动建立本机连接,请稍候。" : runtimeStatus ? (installingModels ? "正在下载并校验本地肖像模型,请稍候。" : runtimeError || "请下载本地肖像模型后再开始排查。") : portraitRuntimeErrorMessage(runtimeError) || "正在自动重连本机引擎,请确认 Canvas Agent 已启动。"} action={runtimeConnecting ? null : runtimeStatus ? <Button size="small" icon={<Download className="size-3.5" />} loading={installingModels} onClick={() => void installModels()}>{installingModels ? "下载中…" : "下载本地模型"}</Button> : <Button size="small" onClick={() => setRuntimeRetryNonce((value) => value + 1)}>立即重连</Button>} /> : null}
|
||||
{runtimeReady && runtimeError ? <Alert type="error" showIcon message="网络排查请求失败" description={runtimeError} closable onClose={() => setRuntimeError("")} /> : null}
|
||||
{state.mode === "network-search" && runtimeStatus?.browser.available === false ? <Alert type="warning" showIcon message="未检测到系统浏览器" description="网络排查需要系统 Chrome、Edge 或 Chromium;也可以先连接手动候选,任务不会静默切换搜索服务。" /> : null}
|
||||
{!runtimeReady && enabled ? <Callout tone={runtimeConnecting ? "info" : "warning"} title={runtimeConnecting ? "正在连接本机检测引擎" : runtimeStatus ? "本地模型未就绪" : "本机引擎连接失败"} action={runtimeConnecting ? null : runtimeStatus ? <Button size="small" icon={<Download className="size-3.5" />} loading={installingModels} onClick={() => void installModels()}>{installingModels ? "下载中…" : "下载本地模型"}</Button> : <Button size="small" onClick={() => setRuntimeRetryNonce((value) => value + 1)}>立即重连</Button>}>{runtimeConnecting ? "正在自动建立本机连接,请稍候。" : runtimeStatus ? (installingModels ? "正在下载并校验本地肖像模型,请稍候。" : runtimeError || "请下载本地肖像模型后再开始排查。") : portraitRuntimeErrorMessage(runtimeError) || "正在自动重连本机引擎,请确认 Canvas Agent 已启动。"}</Callout> : null}
|
||||
{runtimeReady && runtimeError ? <Callout tone="error" title="网络排查请求失败" onClose={() => setRuntimeError("")}>{runtimeError}</Callout> : null}
|
||||
{state.mode === "network-search" && runtimeStatus?.browser.available === false ? <Callout tone="warning" title="未检测到系统浏览器">网络排查需要系统 Chrome、Edge 或 Chromium;也可以先连接手动候选,任务不会静默切换搜索服务。</Callout> : null}
|
||||
|
||||
<section className="rounded-xl border p-4" style={{ borderColor: "var(--border)" }}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div><h3 className="text-sm font-semibold">当前结果</h3><p className="mt-1 text-xs text-foreground/55">完整结果会在本机任务完成后生成;节点 metadata 只保留可恢复摘要。</p></div>
|
||||
{state.task ? <Tag color={state.task.status === "failed" ? "red" : state.task.status === "cancelled" ? "orange" : state.task.status === "completed" ? "green" : "blue"}>{state.task.status === "failed" ? "失败" : state.task.status === "cancelled" ? "已停止" : PORTRAIT_TASK_STAGE_LABELS[state.task.stage] || state.task.stage}</Tag> : null}
|
||||
{state.task ? <StatusBadge tone={state.task.status === "failed" ? "error" : state.task.status === "cancelled" ? "warning" : state.task.status === "completed" ? "success" : "loading"} label={state.task.status === "failed" ? "失败" : state.task.status === "cancelled" ? "已停止" : PORTRAIT_TASK_STAGE_LABELS[state.task.stage] || state.task.stage} /> : null}
|
||||
</div>
|
||||
{state.task?.status === "failed" && state.task.errorMessage ? <Alert className="mt-4" type="error" showIcon message="排查任务失败" description={state.task.errorMessage} /> : null}
|
||||
{state.task?.status === "failed" && state.task.errorMessage ? <Callout className="mt-4" tone="error" title="排查任务失败">{state.task.errorMessage}</Callout> : null}
|
||||
{state.lastResult ? <><div className="mt-4 grid gap-3 sm:grid-cols-4"><Metric label="最高风险" value={PORTRAIT_RISK_LABELS[state.lastResult.highestRisk]} tone={portraitRiskTone(state.lastResult.highestRisk)} /><Metric label="候选数" value={String(state.lastResult.candidateCount)} /><Metric label="已分析" value={String(result?.comparedCount ?? state.lastResult.comparedCount)} /><Metric label="详情" value={state.lastResult.detailsAvailable ? "可用" : "需重新运行"} /></div>{result ? <PortraitResultPanel result={result} taskId={state.lastResult.taskId} onAddCandidate={onAddCandidate} /> : <div className="mt-4 rounded-lg bg-foreground/[.03] p-3 text-xs text-foreground/55">正在读取本机详细结果,图片和完整模型响应不会写入画布 metadata。</div>}</> : <div className="mt-4 grid min-h-44 place-items-center rounded-lg border border-dashed text-center text-sm text-foreground/45" style={{ borderColor: "var(--border)" }}><div><ScanFace className="mx-auto mb-2 size-8 opacity-45" /><p>{canStart ? "准备好开始排查" : "先连接所需图片"}</p><p className="mt-1 text-xs">本地模式会在 Canvas Agent 中执行,关闭工作台不会取消任务。</p></div></div>}
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border p-4" style={{ borderColor: "var(--border)" }}>
|
||||
<div className="mb-3 flex items-center gap-2"><History className="size-4" /><h3 className="text-sm font-semibold">历史运行</h3></div>
|
||||
{history.length ? <div className="space-y-2">{history.map((item) => <div key={item.taskId} className="flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-xs" style={{ borderColor: "var(--border)" }}><span className="min-w-24 font-medium">{item.mode === "direct-compare" ? "直接比对" : "网络排查"}</span><Tag color={item.status === "completed" ? "green" : item.status === "failed" ? "red" : item.status === "partial" ? "orange" : "blue"}>{PORTRAIT_TASK_STAGE_LABELS[item.stage] || item.status}</Tag><span className="text-foreground/55">{item.processedCandidates}{item.totalCandidates ? ` / ${item.totalCandidates}` : ""} 项</span><span className="ml-auto text-foreground/45">{formatDate(item.updatedAt)}</span><Button size="small" type="link" onClick={() => void openHistoryTask(item)}>查看</Button><Popconfirm title="删除本地排查数据?" description="报告、候选和本机任务将不可恢复。" onConfirm={() => void removeHistory(item.taskId)}><Button size="small" type="link" danger loading={busyAction === `delete-${item.taskId}`}>删除</Button></Popconfirm></div>)}</div> : <div className="rounded-lg bg-foreground/[.03] p-3 text-xs leading-5 text-foreground/55">暂无当前节点历史。不会建立跨项目的人脸身份索引。</div>}
|
||||
{history.length ? <div className="space-y-2">{history.map((item) => <div key={item.taskId} className="flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-xs" style={{ borderColor: "var(--border)" }}><span className="min-w-24 font-medium">{item.mode === "direct-compare" ? "直接比对" : "网络排查"}</span><StatusBadge variant="filled" tone={item.status === "completed" ? "success" : item.status === "failed" ? "error" : item.status === "partial" ? "warning" : "loading"} label={PORTRAIT_TASK_STAGE_LABELS[item.stage] || item.status} /><span className="text-foreground/55">{item.processedCandidates}{item.totalCandidates ? ` / ${item.totalCandidates}` : ""} 项</span><span className="ml-auto text-foreground/45">{formatDate(item.updatedAt)}</span><Button size="small" type="link" onClick={() => void openHistoryTask(item)}>查看</Button><Popconfirm title="删除本地排查数据?" description="报告、候选和本机任务将不可恢复。" onConfirm={() => void removeHistory(item.taskId)}><Button size="small" type="link" danger loading={busyAction === `delete-${item.taskId}`}>删除</Button></Popconfirm></div>)}</div> : <div className="rounded-lg bg-foreground/[.03] p-3 text-xs leading-5 text-foreground/55">暂无当前节点历史。不会建立跨项目的人脸身份索引。</div>}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
@@ -350,7 +353,7 @@ function PortraitInputCard({ label, node, ready }: { label: string; node?: Canva
|
||||
<div className="text-xs font-medium">{label}</div>
|
||||
<div className="truncate text-[11px] text-foreground/50">{node?.title || (ready ? "已连接图片" : "待连接")}</div>
|
||||
</div>
|
||||
<Tag color={ready ? "green" : "orange"}>{ready ? "已连接" : "待连接"}</Tag>
|
||||
<StatusBadge tone={ready ? "success" : "warning"} label={ready ? "已连接" : "待连接"} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -417,7 +420,7 @@ function ResultPairCard({ taskId, pair, candidate, onAddCandidate }: { taskId: s
|
||||
<div className="flex gap-3 p-3">
|
||||
<div className="grid size-24 shrink-0 place-items-center overflow-hidden rounded-md bg-foreground/[.04]">{image ? <img src={image} alt="" className="size-full object-contain" /> : <ScanFace className="size-6 opacity-35" />}</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2"><span className="truncate text-sm font-semibold">{candidate?.title || pair.comparisonImageId}</span><Tag color="default" style={portraitRiskTagStyle(pair.riskLevel)}>{PORTRAIT_RISK_LABELS[pair.riskLevel]}</Tag><Tag color={pair.status === "failed" ? "red" : pair.status === "partial" ? "orange" : "green"}>{pair.status === "failed" ? "失败/跳过" : pair.status === "partial" ? "部分完成" : "完成"}</Tag></div>
|
||||
<div className="flex flex-wrap items-center gap-2"><span className="truncate text-sm font-semibold">{candidate?.title || pair.comparisonImageId}</span><Tag color="default" style={portraitRiskTagStyle(pair.riskLevel)}>{PORTRAIT_RISK_LABELS[pair.riskLevel]}</Tag><StatusBadge variant="filled" tone={pair.status === "failed" ? "error" : pair.status === "partial" ? "warning" : "success"} label={pair.status === "failed" ? "失败/跳过" : pair.status === "partial" ? "部分完成" : "完成"} /></div>
|
||||
<div className="mt-1 text-xs text-foreground/55">{candidate?.sourceDomain || pair.source} · {pair.analysisPath === "unable" ? "无法形成可靠路径" : `Path ${pair.analysisPath}`}{pair.overallSimilarity === undefined ? "" : ` · 相似度 ${pair.overallSimilarity.toFixed(4)}`}</div>
|
||||
<div className="mt-1 flex flex-wrap gap-2">{candidate?.sourcePageUrl ? <button type="button" className="text-xs text-[var(--workspace-accent)] underline" onClick={() => window.open(candidate.sourcePageUrl, "_blank", "noopener,noreferrer")}>打开来源页面</button> : null}{onAddCandidate && candidate && image ? <button type="button" className="text-xs text-[var(--workspace-accent)] underline" onClick={() => void onAddCandidate({ id: candidate.id, title: candidate.title, imageArtifactId: candidate.imageArtifactId }, image)}>添加到画布</button> : null}</div>
|
||||
</div>
|
||||
@@ -427,7 +430,7 @@ function ResultPairCard({ taskId, pair, candidate, onAddCandidate }: { taskId: s
|
||||
{pair.localPrecheck.reliabilityIssues.length ? <div className="mt-1 text-foreground/55"><span className="text-foreground/50">可靠性问题:</span>{pair.localPrecheck.reliabilityIssues.join(";")}</div> : null}
|
||||
{pair.error ? <div className="mt-1 text-red-600"><span className="font-medium">处理说明:</span>{pair.error.message}</div> : null}
|
||||
{vision ? <>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2"><span className="font-medium">多模态面部特征分析</span><Tag color={vision.status === "success" ? "green" : "gold"}>{vision.status === "success" ? "已完成" : "无法可靠判断"}</Tag>{vision.manualReviewRecommended ? <Tag color="orange">建议人工复核</Tag> : null}</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2"><span className="font-medium">多模态面部特征分析</span><StatusBadge variant="filled" tone={vision.status === "success" ? "success" : "warning"} label={vision.status === "success" ? "已完成" : "无法可靠判断"} />{vision.manualReviewRecommended ? <StatusBadge variant="filled" tone="warning" label="建议人工复核" /> : null}</div>
|
||||
<div className="mt-2 overflow-x-auto rounded-md border" style={{ borderColor: "var(--border)" }}><table className="w-full min-w-[520px] text-left text-[11px]"><thead className="bg-foreground/[.04]"><tr><th className="px-2 py-1.5 font-medium">特征维度</th><th className="px-2 py-1.5 font-medium">相似度</th><th className="px-2 py-1.5 font-medium">分析说明</th></tr></thead><tbody>{PORTRAIT_FEATURES.map(({ key, label }) => { const feature = vision.featureComparison[key]; return <tr key={key} className="border-t" style={{ borderColor: "var(--border)" }}><td className="px-2 py-1.5">{label}</td><td className="px-2 py-1.5"><Tag color="default" style={portraitFeatureSimilarityStyle(feature.similarity)}>{PORTRAIT_FEATURE_SIMILARITY_LABELS[feature.similarity]}</Tag></td><td className="px-2 py-1.5 text-foreground/60">{feature.note || "-"}</td></tr>; })}</tbody></table></div>
|
||||
<div className="mt-2"><span className="text-foreground/50">视觉依据:</span>{vision.basis.join(";") || "无"}</div>
|
||||
</> : <div className="mt-1 text-foreground/55">{pair.basis.join(";") || "暂无结构化依据"}</div>}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Modal } from "antd";
|
||||
import { Tooltip } from "@/components/ui/base/tooltip";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Modal, Tooltip } from "antd";
|
||||
|
||||
import { Image as ImageIcon, Music2, Play, UserRound } from "lucide-react";
|
||||
|
||||
import { canvasNodeVideoPreviewUrl } from "@/lib/canvas/canvas-media-preview";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { Button, Input, InputNumber, Modal, Select, Switch } from "antd";
|
||||
import { Button, Input, InputNumber, Modal, Select } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { AlertTriangle, Box, Check, FileImage, Link2, Plus, Save, SlidersHorizontal, Trash2 } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
@@ -198,7 +199,7 @@ function AssetEditor({ asset, onChange, onDelete }: { asset: StyleAssetBinding;
|
||||
<Field label="启用状态">
|
||||
<div className="flex h-8 items-center justify-between border-b border-border">
|
||||
<span className="text-xs text-foreground/55">参与生成执行计划</span>
|
||||
<Switch size="small" checked={asset.enabled !== false} onChange={(enabled) => onChange({ enabled })} />
|
||||
<Switch size="sm" checked={asset.enabled !== false} onChange={(enabled) => onChange({ enabled })} />
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="资产名称">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Modal, Switch } from "antd";
|
||||
import { Modal } from "antd";
|
||||
import { Switch } from "@/components/ui/base/switch";
|
||||
import { GripVertical, RotateCcw, X } from "lucide-react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -218,7 +219,7 @@ function ToolbarSettingsItem({ item, reducedMotion, theme, dragging, onToggleVis
|
||||
>
|
||||
<GripVertical className="size-4" />
|
||||
</button>
|
||||
<Switch size="small" checked={item.visible} onChange={(checked) => onToggleVisible(item.id, checked)} aria-label={`${item.visible ? "隐藏" : "显示"}${item.label}`} />
|
||||
<Switch size="sm" checked={item.visible} onChange={(checked) => onToggleVisible(item.id, checked)} aria-label={`${item.visible ? "隐藏" : "显示"}${item.label}`} />
|
||||
</div>
|
||||
<div className="canvas-toolbar-settings-card-content mt-auto flex min-w-0 flex-col items-center gap-1">
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-[var(--r-md)]" style={{ background: theme.toolbar.itemHover, color: theme.node.muted }}>
|
||||
|
||||
Reference in New Issue
Block a user