返回 JoyAI-Echo
ThreadWorkplaceShell.tsx
根目录 / echo_longvideo / Director_Agent / webui / src / components / director / ThreadWorkplaceShell.tsx
1 import { useCallback, useMemo, useState } from "react";
2
3 import { SplitPane } from "@/components/longvideo/SplitPane";
4 import { ThreadShell } from "@/components/thread/ThreadShell";
5 import { WorkplacePanel } from "@/components/workplace/WorkplacePanel";
6 import { useWorkplaceContext } from "@/providers/WorkplaceProvider";
7 import { useThreadStream } from "@/hooks/useThreadStream";
8 import { memoryReviewMessages } from "@/lib/memory-review";
9 import type { ChatSummary, MemoryReview } from "@/lib/types";
10
11 interface ThreadWorkplaceShellProps {
12 session: ChatSummary | null;
13 title: string;
14 onToggleSidebar: () => void;
15 onGoHome: () => void;
16 onNewChat: () => Promise<string | null>;
17 hideSidebarToggleOnDesktop?: boolean;
18 onReplyEnd?: () => void;
19 quickMode?: boolean;
20 }
21
22 /** Chat and Director workplace shown side by side. */
23 export function ThreadWorkplaceShell({
24 session,
25 title,
26 onToggleSidebar,
27 onGoHome,
28 onNewChat,
29 hideSidebarToggleOnDesktop,
30 onReplyEnd,
31 quickMode = false,
32 }: ThreadWorkplaceShellProps) {
33 const [activeTab, setActiveTab] = useState<"story" | "shots">("shots");
34 const {
35 approveMemoryReview,
36 composerSendBlocked,
37 createShotMemoryAsset,
38 reselectMemoryReview,
39 selectMemoryReviewFrame,
40 selectMemoryReviewMode,
41 setContinuousMode,
42 workplace,
43 refresh,
44 } = useWorkplaceContext();
45 const videoSizeLocked =
46 workplace?.shots.some((shot) =>
47 ["queued", "generated", "review_pass", "approved"].includes(shot.status),
48 ) ?? false;
49 const stream = useThreadStream(session, { onReplyEnd });
50 const autoGenerateMode = Boolean(
51 quickMode || workplace?.auto_generate || session?.autoGenerate,
52 );
53 const streamWithMemoryReviews = useMemo(() => {
54 const reviewMessages = workplace ? memoryReviewMessages(workplace) : [];
55 const regularMessages = stream.messages.filter(
56 (message) => !message.id.startsWith("memory-review:"),
57 );
58 return {
59 ...stream,
60 messages: [...regularMessages, ...reviewMessages],
61 };
62 }, [stream, workplace]);
63 const hasVisibleMemoryReview = streamWithMemoryReviews.messages.some((message) =>
64 message.id.startsWith("memory-review:"),
65 );
66 const hideMemoryReviews = autoGenerateMode && !hasVisibleMemoryReview;
67 const showThinking = stream.messages.length > 0;
68
69 const onMemoryReviewAction = useCallback(
70 async (
71 review: Parameters<typeof approveMemoryReview>[0],
72 action: "approve" | "reselect" | "manual_select" | "select_mode",
73 memoryId?: string,
74 timestampSec?: number,
75 selectionMode?: "manual" | "vlm",
76 retainedMemoryIds?: string[],
77 ) => {
78 if (action === "select_mode" && selectionMode) {
79 await selectMemoryReviewMode(review, selectionMode);
80 return;
81 }
82 if (action === "approve") {
83 await approveMemoryReview(review, retainedMemoryIds ?? []);
84 return;
85 }
86 if (action === "reselect") {
87 await reselectMemoryReview(review, memoryId);
88 return;
89 }
90 if (memoryId !== undefined && timestampSec !== undefined) {
91 await selectMemoryReviewFrame(review, memoryId, timestampSec);
92 }
93 },
94 [
95 approveMemoryReview,
96 reselectMemoryReview,
97 selectMemoryReviewFrame,
98 selectMemoryReviewMode,
99 ],
100 );
101
102 const getNextContinuous = useCallback(
103 (review: MemoryReview) => {
104 if (!workplace) return null;
105 const nextShotId = review.shot_id + 1;
106 const nextShot = workplace.shots.find(
107 (shot) => shot.shot_id === nextShotId,
108 );
109 if (!nextShot) return null;
110 const status = String(nextShot.status || "");
111 const locked =
112 status !== "planned" && status !== "prompt_ready";
113 return {
114 nextShotId,
115 enabled: nextShot.continuous_enabled ?? false,
116 disabled: locked,
117 onToggle: (enabled: boolean) => {
118 void setContinuousMode(nextShotId, enabled);
119 },
120 };
121 },
122 [setContinuousMode, workplace],
123 );
124
125 return (
126 <SplitPane
127 left={
128 <ThreadShell
129 session={session}
130 stream={streamWithMemoryReviews}
131 title={title}
132 onToggleSidebar={onToggleSidebar}
133 onGoHome={onGoHome}
134 onNewChat={onNewChat}
135 hideSidebarToggleOnDesktop={hideSidebarToggleOnDesktop}
136 composerSendDisabled={composerSendBlocked}
137 onMemoryReviewAction={
138 hideMemoryReviews ? undefined : onMemoryReviewAction
139 }
140 memoryAssets={workplace?.memory_workspace_assets ?? []}
141 onCreateMemoryAsset={createShotMemoryAsset}
142 getNextContinuous={
143 hideMemoryReviews ? undefined : getNextContinuous
144 }
145 videoSizeLocked={videoSizeLocked}
146 referenceImageLocked={Boolean(workplace?.reference_image_locked)}
147 persistedReferenceImage={workplace?.reference_image ?? null}
148 persistedAutoGenerate={Boolean(
149 quickMode || workplace?.auto_generate || session?.autoGenerate,
150 )}
151 forceAutoGenerate={quickMode}
152 onReferenceImageSynced={refresh}
153 />
154 }
155 right={
156 <WorkplacePanel
157 showThinking={showThinking}
158 sessionKey={session?.key ?? null}
159 activeTab={activeTab}
160 onTabChange={setActiveTab}
161 onNewChat={onNewChat}
162 chatBusy={stream.isStreaming}
163 />
164 }
165 />
166 );
167 }
168
168 lines Plain Text