auto-save 2026-05-14 03:14 (~2)
This commit is contained in:
@@ -3011,6 +3011,19 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 3 项未提交变更 · 最近提交:auto-save 2026-05-14 03:03 (~3)",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-14T03:09:40+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-14 03:09 (~3)",
|
||||
"hash": "79b3f79",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-13T19:13:12Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-14 03:09 (~3)",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { type NodeProps, useReactFlow } from "@xyflow/react"
|
||||
import {
|
||||
Link2, Upload, Download, Scissors, Image as ImageIcon,
|
||||
Mic, Languages, FileEdit, Film, FileVideo, Loader2, Plus, X, LayoutGrid, Pin, Maximize2,
|
||||
Copy, Trash2,
|
||||
Copy, Trash2, Move, PanelLeft, PanelRight, PanelBottom,
|
||||
} from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { NodeShell, type NodeStatus, type NodeKind } from "./node-shell"
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
} from "@/lib/api"
|
||||
import { FrameLightbox } from "@/components/lightbox"
|
||||
|
||||
export type CanvasPanelDock = "canvas" | "left" | "right" | "bottom"
|
||||
|
||||
export interface NodeData {
|
||||
job: Job | null // 当前 active job
|
||||
jobs: Job[] // 所有 job 列表
|
||||
@@ -31,6 +33,9 @@ export interface NodeData {
|
||||
expandedFrame: number | null
|
||||
framePanelScale?: number
|
||||
framePanelPinned?: boolean
|
||||
videoPanelJobId?: string | null
|
||||
videoPanelScale?: number
|
||||
videoPanelDock?: CanvasPanelDock
|
||||
onSubmitUrl: (url: string) => void
|
||||
onUploadFile: (file: File) => void
|
||||
onAnalyze: () => void
|
||||
@@ -41,6 +46,11 @@ export interface NodeData {
|
||||
onFramePanelPinnedChange?: (pinned: boolean) => void
|
||||
onCloseExpandedFrame: () => void
|
||||
onAddManualFrame: (t: number) => void
|
||||
onAddManualFrameForJob?: (jobId: string, t: number) => Promise<void> | void
|
||||
onOpenVideoPanel?: (jobId: string) => void
|
||||
onCloseVideoPanel?: () => void
|
||||
onVideoPanelScaleChange?: (scale: number) => void
|
||||
onVideoPanelDockChange?: (dock: CanvasPanelDock) => void
|
||||
onOpenVideoLightbox: () => void
|
||||
onSwitchJob: (id: string) => void
|
||||
onJobUpdate: (j: Job) => void
|
||||
@@ -384,15 +394,11 @@ function DeleteConfirmDialog({
|
||||
export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | any) {
|
||||
const d: NodeData = data
|
||||
const [url, setUrl] = useState("")
|
||||
const [videoT, setVideoT] = useState(0)
|
||||
const [addingFrame, setAddingFrame] = useState(false)
|
||||
const [videoExpanded, setVideoExpanded] = useState(false)
|
||||
const [hoverPreviewJob, setHoverPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
||||
const [pinnedPreviewJob, setPinnedPreviewJob] = useState<PreviewAnchor<string> | null>(null)
|
||||
const [deleteJobTarget, setDeleteJobTarget] = useState<Job | null>(null)
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const job = d.job
|
||||
|
||||
// 点击 input 节点外的任何位置 → 取消 pin(capture 阶段,避免 ReactFlow pane 拦截)
|
||||
@@ -419,7 +425,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
<div ref={rootRef} className="relative" style={{ width: "100%", height: "100%" }}>
|
||||
{/* 多视频缩略图浮条 — 「+」在最左,job 按时间倒序(最新靠左高亮),统一高度 64,宽度按视频原比例,一行横滚。
|
||||
浮条宽度 = 节点宽度(节点拖宽后浮条同步变宽,可见更多缩略图,少滚动)。 */}
|
||||
{!videoExpanded && d.jobs.length > 0 && (
|
||||
{d.jobs.length > 0 && (
|
||||
<FloatingThumbnailStrip label="输入视频缩略图横向滑动条">
|
||||
{/* + 再上传一个(放在最前面) */}
|
||||
<button
|
||||
@@ -456,9 +462,13 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (ready) setVideoExpanded(true)
|
||||
if (ready) {
|
||||
setPinnedPreviewJob(null)
|
||||
if (!isActive) d.onSwitchJob(j.id)
|
||||
d.onOpenVideoPanel?.(j.id)
|
||||
}
|
||||
}}
|
||||
title={ready ? `${j.width}×${j.height} · ${j.duration.toFixed(1)}s · 单击钉住大预览 · 双击展开播放` : "下载中…"}
|
||||
title={ready ? `${j.width}×${j.height} · ${(j.duration ?? 0).toFixed(1)}s · 单击钉住大预览 · 双击打开抽帧面板` : "下载中…"}
|
||||
className="absolute inset-0 w-full h-full overflow-hidden rounded-md"
|
||||
>
|
||||
{ready ? (
|
||||
@@ -477,7 +487,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0.5 right-0.5 bg-black/70 text-white text-[9px] font-mono px-1 py-0.5 rounded">
|
||||
{ready ? `${j.duration.toFixed(1)}s` : "…"}
|
||||
{ready ? `${(j.duration ?? 0).toFixed(1)}s` : "…"}
|
||||
</div>
|
||||
</button>
|
||||
{d.onDeleteJob && (
|
||||
@@ -515,7 +525,7 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
|
||||
{(() => {
|
||||
const anchor = pinnedPreviewJob ?? hoverPreviewJob
|
||||
if (!anchor || videoExpanded) return null
|
||||
if (!anchor) return null
|
||||
const previewJob = d.jobs.find((j) => j.id === anchor.id)
|
||||
if (!previewJob?.video_url) return null
|
||||
const aspectStr = previewJob.height ? `${previewJob.width}/${previewJob.height}` : "9/16"
|
||||
@@ -535,55 +545,6 @@ export function InputNode({ data, selected }: NodeProps<{ data: NodeData }> | an
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* 展开态 — 稍微放大(360 宽),含 controls + 加帧按钮,不全屏 */}
|
||||
{hasVideo && job && videoExpanded && (
|
||||
<div
|
||||
className="absolute left-0 right-0 flex justify-center"
|
||||
style={{ bottom: "calc(100% + 12px)" }}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="relative rounded-xl overflow-hidden border border-white/25 shadow-2xl bg-black"
|
||||
style={{ width: 360, animation: "drawer-in 0.18s cubic-bezier(0.32, 0.72, 0, 1)" }}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl(job.id)}
|
||||
controls
|
||||
autoPlay
|
||||
playsInline
|
||||
preload="auto"
|
||||
onTimeUpdate={(e) => setVideoT((e.target as HTMLVideoElement).currentTime)}
|
||||
className="block w-full bg-black"
|
||||
style={{ aspectRatio: `${job.width}/${job.height}`, maxHeight: "60vh" }}
|
||||
/>
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-black/70 backdrop-blur-md gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={addingFrame}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
const t = videoRef.current?.currentTime ?? videoT
|
||||
setAddingFrame(true)
|
||||
try { await d.onAddManualFrame(t) } finally { setAddingFrame(false) }
|
||||
}}
|
||||
className="flex-1 text-[11.5px] py-1.5 rounded-md bg-emerald-500 hover:bg-emerald-400 text-white disabled:opacity-50 inline-flex items-center justify-center gap-1.5 font-medium"
|
||||
>
|
||||
{addingFrame ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
|
||||
+ 把 {videoT.toFixed(1)}s 加为关键帧
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); setVideoExpanded(false) }}
|
||||
className="px-2.5 py-1.5 text-[11px] rounded-md bg-white/10 hover:bg-white/20 text-white"
|
||||
>
|
||||
收起
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NodeShell
|
||||
type="input" status={inputStatus(job)}
|
||||
icon={<Link2 className="h-4 w-4" />}
|
||||
|
||||
Reference in New Issue
Block a user