返回 JoyAI-Echo
memory-review.ts
根目录 / echo_longvideo / Director_Agent / webui / src / lib / memory-review.ts
1 import type {
2 MemoryReview,
3 MemoryReviewStatus,
4 MemorySelection,
5 UIMessage,
6 WorkplaceData,
7 } from "@/lib/types";
8
9 const STATUSES = new Set<MemoryReviewStatus>([
10 "awaiting_method",
11 "selecting",
12 "awaiting_review",
13 "reselecting",
14 "approved",
15 "error",
16 ]);
17
18 function isSelection(value: unknown): value is MemorySelection {
19 if (!value || typeof value !== "object") return false;
20 const item = value as Partial<MemorySelection>;
21 return (
22 typeof item.memory_id === "string" &&
23 (item.kind === "character" || item.kind === "previous_shot") &&
24 typeof item.candidate_index === "number" &&
25 typeof item.frame_index === "number" &&
26 typeof item.timestamp_sec === "number" &&
27 typeof item.confidence === "number" &&
28 typeof item.reasoning === "string" &&
29 Boolean(item.image && typeof item.image.url === "string")
30 );
31 }
32
33 function normalizeReview(
34 shotId: number,
35 value: unknown,
36 ): MemoryReview | null {
37 if (!value || typeof value !== "object") return null;
38 const review = value as Partial<MemoryReview>;
39 if (
40 typeof review.review_id !== "string" ||
41 !review.review_id ||
42 typeof review.status !== "string" ||
43 !STATUSES.has(review.status as MemoryReviewStatus) ||
44 typeof review.attempt !== "number" ||
45 !Array.isArray(review.selections) ||
46 !review.selections.every(isSelection)
47 ) {
48 return null;
49 }
50 return {
51 review_id: review.review_id,
52 shot_id: shotId,
53 status: review.status as MemoryReviewStatus,
54 attempt: review.attempt,
55 candidate_count: Number(review.candidate_count ?? 0),
56 rejected_candidate_indices: Array.isArray(
57 review.rejected_candidate_indices,
58 )
59 ? review.rejected_candidate_indices.filter(
60 (item): item is number => typeof item === "number",
61 )
62 : [],
63 selections: review.selections,
64 selection_mode:
65 review.selection_mode === "manual" || review.selection_mode === "vlm"
66 ? review.selection_mode
67 : null,
68 required_memory_ids: Array.isArray(review.required_memory_ids)
69 ? review.required_memory_ids.filter((item): item is string => typeof item === "string")
70 : [],
71 manual_selected_ids: Array.isArray(review.manual_selected_ids)
72 ? review.manual_selected_ids.filter((item): item is string => typeof item === "string")
73 : [],
74 source_video:
75 review.source_video && typeof review.source_video.url === "string"
76 ? review.source_video
77 : null,
78 history: Array.isArray(review.history) ? review.history : [],
79 error: typeof review.error === "string" ? review.error : null,
80 updated_at:
81 typeof review.updated_at === "string" ? review.updated_at : "",
82 };
83 }
84
85 function pendingSelectingReview(shotId: number): MemoryReview {
86 return {
87 review_id: `pending:${shotId}`,
88 shot_id: shotId,
89 status: "selecting",
90 attempt: 1,
91 candidate_count: 0,
92 rejected_candidate_indices: [],
93 selections: [],
94 history: [],
95 error: null,
96 updated_at: "",
97 };
98 }
99
100 function toMessage(
101 workplace: WorkplaceData,
102 shotId: number,
103 review: MemoryReview,
104 ): UIMessage {
105 const parsedTime = Date.parse(review.updated_at);
106 return {
107 id: `memory-review:${workplace.work_id}:${shotId}:${review.review_id}`,
108 role: "assistant",
109 content: "",
110 createdAt: Number.isFinite(parsedTime) ? parsedTime : shotId,
111 memoryReview: review,
112 };
113 }
114
115 export function memoryReviewMessages(workplace: WorkplaceData): UIMessage[] {
116 if (!workplace.work_id) return [];
117 return [...(workplace.shots ?? [])]
118 .sort((left, right) => left.shot_id - right.shot_id)
119 .flatMap((shot) => {
120 // Only surface Memory review after the user accepts the shot video.
121 if (shot.status !== "approved") return [];
122 const review = normalizeReview(shot.shot_id, shot.memory_review);
123 if (
124 workplace.auto_generate &&
125 review?.status !== "awaiting_method" &&
126 review?.status !== "error" &&
127 review?.selection_mode !== "manual"
128 ) {
129 return [];
130 }
131 if (review) return [toMessage(workplace, shot.shot_id, review)];
132 if (workplace.auto_generate) return [];
133 return [
134 toMessage(workplace, shot.shot_id, pendingSelectingReview(shot.shot_id)),
135 ];
136 });
137 }
138
138 lines TYPESCRIPT