| 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 |