= [
+ { key: "all", label: "全部" },
+ { key: "inflight", label: "生成中" },
+ { key: "succeeded", label: "已完成" },
+ { key: "failed", label: "失败" }
+];
+
+const GROUP_LABEL: Record<"today" | "yesterday" | "earlier", string> = {
+ today: "今天",
+ yesterday: "昨天",
+ earlier: "更早"
+};
+
+function chinaDay(iso: string | null) {
+ if (!iso) return "";
+ return new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai", year: "numeric", month: "2-digit", day: "2-digit" }).format(new Date(iso));
+}
+
+function dayBucket(iso: string | null): "today" | "yesterday" | "earlier" {
+ const day = chinaDay(iso);
+ if (!day) return "earlier";
+ const today = chinaDay(new Date().toISOString());
+ if (day === today) return "today";
+ const yesterday = chinaDay(new Date(Date.now() - 86400000).toISOString());
+ return day === yesterday ? "yesterday" : "earlier";
+}
+
+function matchesFeedFilter(task: FreeVideoTask, filter: FeedFilter) {
+ if (filter === "all") return true;
+ if (filter === "inflight") return isInFlight(task.status);
+ if (filter === "succeeded") return task.status === "succeeded";
+ return task.status === "failed" || task.status === "cancelled";
+}
+
function loadProgress(): Record
{
try {
return JSON.parse(sessionStorage.getItem(PROGRESS_KEY) || "{}") as Record;
@@ -97,12 +134,21 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
const [submitting, setSubmitting] = useState(false);
const promptRef = useRef(null);
+ useEffect(() => {
+ const draft = sessionStorage.getItem(REMIX_PROMPT_KEY);
+ if (!draft) return;
+ sessionStorage.removeItem(REMIX_PROMPT_KEY);
+ window.setTimeout(() => promptRef.current?.setContent(draft, []), 0);
+ notify("info", "已填入复刻提示词,可改完再生成");
+ }, [notify]);
+
// —— 弹窗 ——
const [detailId, setDetailId] = useState(null);
const [libraryOpen, setLibraryOpen] = useState(false);
const [platformOpen, setPlatformOpen] = useState(false);
const [libraryTargetRole, setLibraryTargetRole] = useState<"first_frame" | "last_frame" | null>(null);
const [deleteTarget, setDeleteTarget] = useState(null);
+ const [feedFilter, setFeedFilter] = useState("all");
// —— 轮询/进度 ——
const pollTimersRef = useRef(new Map());
@@ -522,6 +568,26 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
setRefs([]);
}, []);
+ const filterCounts = useMemo(() => ({
+ all: tasks.length,
+ inflight: tasks.filter((t) => isInFlight(t.status)).length,
+ succeeded: tasks.filter((t) => t.status === "succeeded").length,
+ failed: tasks.filter((t) => t.status === "failed" || t.status === "cancelled").length
+ }), [tasks]);
+
+ const feedGroups = useMemo(() => {
+ const visible = tasks.filter((t) => matchesFeedFilter(t, feedFilter));
+ const groups: Array<{ key: "today" | "yesterday" | "earlier"; items: FreeVideoTask[] }> = [
+ { key: "today", items: [] },
+ { key: "yesterday", items: [] },
+ { key: "earlier", items: [] }
+ ];
+ visible.forEach((task) => {
+ groups.find((g) => g.key === dayBucket(task.created_at))?.items.push(task);
+ });
+ return groups.filter((g) => g.items.length > 0);
+ }, [tasks, feedFilter]);
+
// 详情弹窗的上一条/下一条(只在已完成的任务间切换)
const doneTasks = tasks.filter((t) => t.status === "succeeded" && t.video_url);
const detailTask = detailId ? tasks.find((t) => t.id === detailId) || null : null;
@@ -554,6 +620,22 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
+ {tasks.length > 0 && (
+
{loading ? (
@@ -563,21 +645,35 @@ export function FreeCreatePage({ modelConfigs, onNotify, onTaskSettled, onBack }
准备生成新视频
在下方输入提示词,或从素材库引用参考素材
- ) : (
-