auto-save 2026-05-14 03:14 (~2)

This commit is contained in:
2026-05-14 03:15:11 +08:00
parent 79b3f7961b
commit b8fa19aeaa
2 changed files with 33 additions and 59 deletions

View File

@@ -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
}
]
}

View File

@@ -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 节点外的任何位置 → 取消 pincapture 阶段,避免 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" />}