Skip to content

Commit 3ea3abb

Browse files
fix(desktop): preserve batch transcript panel (#5283)
Hide record-only bottom accessory chrome while keeping batch regeneration progress visible in the transcript panel.
1 parent 41631df commit 3ea3abb

4 files changed

Lines changed: 155 additions & 62 deletions

File tree

apps/desktop/src/session/components/bottom-accessory/during-session.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ describe("DuringSessionAccessory", () => {
6262
});
6363
});
6464

65+
it("does not render a footer while recording for batch transcription", () => {
66+
useListenerMock.mockImplementation((selector) =>
67+
selector({
68+
live: {
69+
requestedLiveTranscription: false,
70+
liveTranscriptionActive: false,
71+
},
72+
liveSegments,
73+
}),
74+
);
75+
76+
const view = render(<DuringSessionAccessory sessionId="session-1" />);
77+
78+
expect(view.container.firstChild).toBeNull();
79+
});
80+
6581
it("lets manual scrolling override expanded live transcript bottom pinning", () => {
6682
const view = render(
6783
<DuringSessionAccessory sessionId="session-1" isExpanded />,

apps/desktop/src/session/components/bottom-accessory/during-session.tsx

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,45 @@ function LiveTranscriptFooter({
6464
fillHeight: boolean;
6565
isExpanded?: boolean;
6666
}) {
67-
const store = main.UI.useStore(main.STORE_ID);
68-
const segments = useLiveTranscriptSegments(sessionId);
6967
const requestedLiveTranscription = useListener(
7068
(state) => state.live.requestedLiveTranscription,
7169
);
7270
const liveTranscriptionActive = useListener(
7371
(state) => state.live.liveTranscriptionActive,
7472
);
75-
const labelContext = useMemo(
76-
() => (store ? defaultRenderLabelContext(store) : undefined),
77-
[store],
78-
);
7973
const captureMode = getLiveCaptureUiMode({
8074
requestedLiveTranscription,
8175
liveTranscriptionActive,
8276
});
83-
const mode =
84-
captureMode === "live"
85-
? { kind: "live" as const }
86-
: {
87-
kind: "record_only" as const,
88-
isFallbackFromLive: captureMode === "fallback_record_only",
89-
};
77+
78+
if (captureMode !== "live") {
79+
return null;
80+
}
81+
82+
return (
83+
<LiveTranscriptFooterContent
84+
sessionId={sessionId}
85+
fillHeight={fillHeight}
86+
isExpanded={isExpanded}
87+
/>
88+
);
89+
}
90+
91+
function LiveTranscriptFooterContent({
92+
sessionId,
93+
fillHeight,
94+
isExpanded = false,
95+
}: {
96+
sessionId: string;
97+
fillHeight: boolean;
98+
isExpanded?: boolean;
99+
}) {
100+
const store = main.UI.useStore(main.STORE_ID);
101+
const segments = useLiveTranscriptSegments(sessionId);
102+
const labelContext = useMemo(
103+
() => (store ? defaultRenderLabelContext(store) : undefined),
104+
[store],
105+
);
90106

91107
const speakerLabelManager = useMemo(() => {
92108
if (!store) {
@@ -107,40 +123,20 @@ function LiveTranscriptFooter({
107123
fillHeight && "h-full min-h-0",
108124
])}
109125
>
110-
{mode.kind === "record_only" ? (
111-
<RecordOnlyFooter isFallbackFromLive={mode.isFallbackFromLive} />
112-
) : (
113-
<LiveTranscriptContent
114-
fillHeight={fillHeight}
115-
isExpanded={isExpanded}
116-
previewText={previewText}
117-
scrollRef={scrollRef}
118-
segments={segments}
119-
labelContext={labelContext}
120-
speakerLabelManager={speakerLabelManager}
121-
/>
122-
)}
126+
<LiveTranscriptContent
127+
fillHeight={fillHeight}
128+
isExpanded={isExpanded}
129+
previewText={previewText}
130+
scrollRef={scrollRef}
131+
segments={segments}
132+
labelContext={labelContext}
133+
speakerLabelManager={speakerLabelManager}
134+
/>
123135
</div>
124136
</div>
125137
);
126138
}
127139

128-
function RecordOnlyFooter({
129-
isFallbackFromLive,
130-
}: {
131-
isFallbackFromLive: boolean;
132-
}) {
133-
return (
134-
<div className="flex items-center justify-center px-4 py-1">
135-
<p className="text-[11px] leading-none text-neutral-400">
136-
{isFallbackFromLive
137-
? "Live transcription stopped. Transcript will be created after you stop."
138-
: "Recording only. Transcript will be created after you stop."}
139-
</p>
140-
</div>
141-
);
142-
}
143-
144140
function LiveTranscriptContent({
145141
fillHeight,
146142
isExpanded,

apps/desktop/src/session/components/bottom-accessory/index.test.tsx

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const hoisted = vi.hoisted(() => ({
1212
};
1313
}
1414
>(),
15+
live: {
16+
requestedLiveTranscription: true as boolean | null,
17+
liveTranscriptionActive: true as boolean | null,
18+
},
1519
}));
1620

1721
vi.mock("react-hotkeys-hook", () => ({
@@ -44,10 +48,7 @@ vi.mock("~/stt/contexts", () => ({
4448
}) => unknown,
4549
) =>
4650
selector({
47-
live: {
48-
requestedLiveTranscription: true,
49-
liveTranscriptionActive: true,
50-
},
51+
live: hoisted.live,
5152
}),
5253
}));
5354

@@ -64,6 +65,8 @@ import { useSessionBottomAccessory } from "./index";
6465
describe("useSessionBottomAccessory", () => {
6566
beforeEach(() => {
6667
hoisted.hotkeys.clear();
68+
hoisted.live.requestedLiveTranscription = true;
69+
hoisted.live.liveTranscriptionActive = true;
6770
useShellMock.mockReturnValue({
6871
chat: {
6972
mode: "Closed",
@@ -164,6 +167,82 @@ describe("useSessionBottomAccessory", () => {
164167
expect(result.current.bottomAccessory).not.toBeNull();
165168
});
166169

170+
it("hides the bottom accessory while recording for batch transcription", () => {
171+
hoisted.live.requestedLiveTranscription = false;
172+
hoisted.live.liveTranscriptionActive = false;
173+
174+
const { result } = renderHook(() =>
175+
useSessionBottomAccessory({
176+
sessionId: "session-1",
177+
sessionMode: "active",
178+
audioUrl: null,
179+
hasTranscript: false,
180+
}),
181+
);
182+
183+
expect(result.current.bottomAccessoryState).toBeNull();
184+
expect(result.current.bottomAccessory).toBeNull();
185+
expect(result.current.bottomBorderHandle).toBeNull();
186+
});
187+
188+
it("keeps batch progress visible while batch transcription is running", () => {
189+
const { result } = renderHook(() =>
190+
useSessionBottomAccessory({
191+
sessionId: "session-1",
192+
sessionMode: "running_batch",
193+
audioUrl: "file:///session.wav",
194+
hasTranscript: true,
195+
}),
196+
);
197+
198+
expect(result.current.bottomAccessoryState).toEqual({
199+
mode: "playback",
200+
expanded: false,
201+
});
202+
expect(result.current.bottomAccessory).not.toBeNull();
203+
expect(result.current.bottomBorderHandle).not.toBeNull();
204+
});
205+
206+
it("keeps the transcript panel expanded when regeneration starts", () => {
207+
const { result, rerender } = renderHook(
208+
({ sessionMode }: { sessionMode: string }) =>
209+
useSessionBottomAccessory({
210+
sessionId: "session-1",
211+
sessionMode,
212+
audioUrl: "file:///session.wav",
213+
hasTranscript: true,
214+
}),
215+
{
216+
initialProps: {
217+
sessionMode: "inactive",
218+
},
219+
},
220+
);
221+
222+
const toggle = result.current.bottomBorderHandle;
223+
expect(isValidElement<{ onToggle: () => void }>(toggle)).toBe(true);
224+
if (!isValidElement<{ onToggle: () => void }>(toggle)) {
225+
return;
226+
}
227+
228+
act(() => {
229+
toggle.props.onToggle();
230+
});
231+
232+
expect(result.current.bottomAccessoryState).toEqual({
233+
mode: "playback",
234+
expanded: true,
235+
});
236+
237+
rerender({ sessionMode: "running_batch" });
238+
239+
expect(result.current.bottomAccessoryState).toEqual({
240+
mode: "playback",
241+
expanded: true,
242+
});
243+
expect(result.current.bottomAccessory).not.toBeNull();
244+
});
245+
167246
it("keeps the expanded live handle on neutral 50", () => {
168247
const { result } = renderHook(() =>
169248
useSessionBottomAccessory({

apps/desktop/src/session/components/bottom-accessory/index.tsx

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ export function useSessionBottomAccessory({
3232
const [isExpanded, setIsExpanded] = useState(false);
3333
const isLive = sessionMode === "active";
3434
const isFinalizing = sessionMode === "finalizing";
35-
const isBatching = sessionMode === "running_batch";
36-
const isInactive = sessionMode === "inactive" || isBatching;
37-
const hasAudio = Boolean(audioUrl) && isInactive;
35+
const isInactive = sessionMode === "inactive";
36+
const isRunningBatch = sessionMode === "running_batch";
37+
const hasAudio = Boolean(audioUrl) && (isInactive || isRunningBatch);
3838
const live = useListener((state) => state.live);
3939
const { chat } = useShell();
4040
const liveCaptureMode = getLiveCaptureUiMode(live);
41-
const canExpandLiveTranscript = isLive && liveCaptureMode === "live";
41+
const showLiveAccessory = isLive && liveCaptureMode === "live";
42+
const canExpandLiveTranscript = showLiveAccessory;
4243
const effectiveExpanded =
4344
isLive && !canExpandLiveTranscript ? false : isExpanded;
4445
const isChatVisible = chat.mode === "RightPanelOpen";
@@ -58,7 +59,7 @@ export function useSessionBottomAccessory({
5859
}, [isLive, canExpandLiveTranscript, isExpanded]);
5960

6061
const showPostSession =
61-
isInactive && (isBatching || hasAudio || hasTranscript);
62+
(isInactive && (hasAudio || hasTranscript)) || isRunningBatch;
6263

6364
useHotkeys(
6465
"esc",
@@ -74,22 +75,23 @@ export function useSessionBottomAccessory({
7475
[showPostSession, isExpanded, isChatVisible],
7576
);
7677

77-
const mode: NonNullable<BottomAccessoryState>["mode"] | null = isLive
78-
? "live"
79-
: isFinalizing
80-
? "finalizing"
81-
: showPostSession
82-
? hasAudio
83-
? "playback"
84-
: "transcript_only"
85-
: null;
78+
const mode: NonNullable<BottomAccessoryState>["mode"] | null =
79+
showLiveAccessory
80+
? "live"
81+
: isFinalizing
82+
? "finalizing"
83+
: showPostSession
84+
? hasAudio || isRunningBatch
85+
? "playback"
86+
: "transcript_only"
87+
: null;
8688

8789
const bottomAccessoryState: BottomAccessoryState = useMemo(
8890
() => (mode ? { mode, expanded: effectiveExpanded } : null),
8991
[effectiveExpanded, mode],
9092
);
9193

92-
if (isLive || isFinalizing) {
94+
if (showLiveAccessory || isFinalizing) {
9395
return {
9496
bottomAccessory: (
9597
<DuringSessionAccessory
@@ -114,7 +116,7 @@ export function useSessionBottomAccessory({
114116
}
115117

116118
if (showPostSession) {
117-
const hasAccessoryContent = isExpanded || isBatching || hasAudio;
119+
const hasAccessoryContent = isExpanded || hasAudio || isRunningBatch;
118120
return {
119121
bottomAccessory: hasAccessoryContent ? (
120122
<PostSessionAccessory

0 commit comments

Comments
 (0)