Skip to content

Commit e92deea

Browse files
fix(desktop): show live transcript across tabs
Mount and frame the active meeting live transcript in the main shell for non-active tabs, suppress duplicate per-session transcript controls, and keep batch progress visible for other sessions.
1 parent 957259a commit e92deea

11 files changed

Lines changed: 530 additions & 9 deletions

File tree

apps/desktop/src/main/body.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
import { useClassicMainTabsShortcuts } from "./useTabsShortcuts";
2323

2424
import { useShell } from "~/contexts/shell";
25+
import { GlobalLiveTranscriptAccessory } from "~/session/components/bottom-accessory/global-live";
2526
import { useConfigValue } from "~/shared/config";
2627
import {
2728
hasCustomSidebarTab,
@@ -148,12 +149,14 @@ export function ClassicMainBody() {
148149
onPointerMove={mainAreaTopDrag.onPointerMove}
149150
onPointerUp={mainAreaTopDrag.onPointerEnd}
150151
>
151-
{currentTab ? (
152-
<ClassicMainTabContent
153-
key={uniqueIdfromTab(currentTab)}
154-
tab={currentTab as Tab}
155-
/>
156-
) : null}
152+
<GlobalLiveTranscriptAccessory currentTab={currentTab}>
153+
{currentTab ? (
154+
<ClassicMainTabContent
155+
key={uniqueIdfromTab(currentTab)}
156+
tab={currentTab as Tab}
157+
/>
158+
) : null}
159+
</GlobalLiveTranscriptAccessory>
157160
</div>
158161
</div>
159162
</div>
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
4+
const mocks = vi.hoisted(() => ({
5+
live: {
6+
status: "active" as "inactive" | "active" | "finalizing",
7+
sessionId: "live-session" as string | null,
8+
requestedLiveTranscription: true as boolean | null,
9+
liveTranscriptionActive: true as boolean | null,
10+
},
11+
resize: vi.fn(),
12+
}));
13+
14+
vi.mock("@hypr/ui/components/ui/resizable", async () => {
15+
const React = await vi.importActual<typeof import("react")>("react");
16+
17+
return {
18+
ResizablePanelGroup: ({
19+
children,
20+
direction,
21+
}: {
22+
children: React.ReactNode;
23+
direction: string;
24+
}) => (
25+
<div data-direction={direction} data-testid="panel-group">
26+
{children}
27+
</div>
28+
),
29+
ResizablePanel: React.forwardRef<
30+
{ resize: (size: number) => void },
31+
{
32+
children: React.ReactNode;
33+
className?: string;
34+
defaultSize?: number;
35+
maxSize?: number;
36+
minSize?: number;
37+
}
38+
>(function ResizablePanel(
39+
{ children, className, defaultSize, maxSize, minSize },
40+
ref,
41+
) {
42+
React.useImperativeHandle(ref, () => ({
43+
resize: mocks.resize,
44+
}));
45+
46+
return (
47+
<div
48+
data-class-name={className}
49+
data-default-size={defaultSize}
50+
data-max-size={maxSize}
51+
data-min-size={minSize}
52+
data-testid="panel"
53+
>
54+
{children}
55+
</div>
56+
);
57+
}),
58+
ResizableHandle: ({ className }: { className?: string }) => (
59+
<div data-class-name={className} data-testid="resize-handle" />
60+
),
61+
};
62+
});
63+
64+
vi.mock("./during-session", () => ({
65+
DuringSessionAccessory: ({
66+
fillHeight,
67+
isExpanded,
68+
isFinalizing,
69+
sessionId,
70+
}: {
71+
fillHeight?: boolean;
72+
isExpanded?: boolean;
73+
isFinalizing?: boolean;
74+
sessionId: string;
75+
}) => (
76+
<div
77+
data-fill-height={String(fillHeight)}
78+
data-finalizing={String(isFinalizing)}
79+
data-is-expanded={String(isExpanded)}
80+
data-session-id={sessionId}
81+
data-testid="during-session-accessory"
82+
/>
83+
),
84+
}));
85+
86+
vi.mock("~/stt/contexts", () => ({
87+
useListener: (
88+
selector: (state: {
89+
live: {
90+
status: "inactive" | "active" | "finalizing";
91+
sessionId: string | null;
92+
requestedLiveTranscription: boolean | null;
93+
liveTranscriptionActive: boolean | null;
94+
};
95+
}) => unknown,
96+
) =>
97+
selector({
98+
live: mocks.live,
99+
}),
100+
}));
101+
102+
import { GlobalLiveTranscriptAccessory } from "./global-live";
103+
104+
describe("GlobalLiveTranscriptAccessory", () => {
105+
beforeEach(() => {
106+
cleanup();
107+
mocks.live.status = "active";
108+
mocks.live.sessionId = "live-session";
109+
mocks.live.requestedLiveTranscription = true;
110+
mocks.live.liveTranscriptionActive = true;
111+
mocks.resize.mockClear();
112+
});
113+
114+
it("does not duplicate the live transcript on the active session tab", () => {
115+
render(
116+
<GlobalLiveTranscriptAccessory
117+
currentTab={{ type: "sessions", id: "live-session" } as any}
118+
>
119+
<div data-testid="tab-content" />
120+
</GlobalLiveTranscriptAccessory>,
121+
);
122+
123+
expect(screen.getByTestId("tab-content")).toBeTruthy();
124+
expect(screen.queryByTestId("during-session-accessory")).toBeNull();
125+
expect(screen.queryByRole("button", { name: "Expand Live" })).toBeNull();
126+
});
127+
128+
it("shows and expands the live transcript for other tabs", () => {
129+
render(
130+
<GlobalLiveTranscriptAccessory
131+
currentTab={{ type: "sessions", id: "other-session" } as any}
132+
>
133+
<div data-testid="tab-content" />
134+
</GlobalLiveTranscriptAccessory>,
135+
);
136+
137+
expect(
138+
screen.getByTestId("during-session-accessory").dataset,
139+
).toMatchObject({
140+
sessionId: "live-session",
141+
isExpanded: "false",
142+
fillHeight: "false",
143+
});
144+
145+
fireEvent.click(screen.getByRole("button", { name: "Expand Live" }));
146+
147+
expect(
148+
screen.getByTestId("during-session-accessory").dataset,
149+
).toMatchObject({
150+
sessionId: "live-session",
151+
isExpanded: "true",
152+
fillHeight: "true",
153+
});
154+
expect(screen.getByTestId("resize-handle")).toBeTruthy();
155+
expect(mocks.resize).toHaveBeenCalledWith(22);
156+
});
157+
158+
it("keeps the live panel flush against the bottom divider", () => {
159+
render(
160+
<GlobalLiveTranscriptAccessory currentTab={{ type: "settings" } as any}>
161+
<div data-testid="tab-content" />
162+
</GlobalLiveTranscriptAccessory>,
163+
);
164+
165+
const panel = screen.getByTestId("during-session-accessory");
166+
const afterBorderContent = panel.parentElement;
167+
168+
expect(afterBorderContent?.className).not.toContain("pt-1.5");
169+
expect(afterBorderContent?.className).not.toContain("mt-1");
170+
});
171+
172+
it("frames the global live transcript panel when shown outside the active session tab", () => {
173+
render(
174+
<GlobalLiveTranscriptAccessory currentTab={{ type: "settings" } as any}>
175+
<div data-testid="tab-content" />
176+
</GlobalLiveTranscriptAccessory>,
177+
);
178+
179+
const transcriptCard = screen.getByTestId(
180+
"during-session-accessory",
181+
).parentElement;
182+
183+
expect(
184+
transcriptCard?.hasAttribute("data-global-live-transcript-card"),
185+
).toBe(true);
186+
expect(transcriptCard?.className).toContain("border-x");
187+
expect(transcriptCard?.className).toContain("border-b");
188+
expect(transcriptCard?.className).toContain("border-neutral-200");
189+
expect(transcriptCard?.className).toContain("rounded-b-xl");
190+
});
191+
192+
it("does not show a global live transcript while recording without live transcription", () => {
193+
mocks.live.requestedLiveTranscription = false;
194+
mocks.live.liveTranscriptionActive = false;
195+
196+
render(
197+
<GlobalLiveTranscriptAccessory currentTab={{ type: "settings" } as any}>
198+
<div data-testid="tab-content" />
199+
</GlobalLiveTranscriptAccessory>,
200+
);
201+
202+
expect(screen.getByTestId("tab-content")).toBeTruthy();
203+
expect(screen.queryByTestId("during-session-accessory")).toBeNull();
204+
});
205+
206+
it("keeps finalizing visible outside the active session tab", () => {
207+
mocks.live.status = "finalizing";
208+
mocks.live.requestedLiveTranscription = false;
209+
mocks.live.liveTranscriptionActive = false;
210+
211+
render(
212+
<GlobalLiveTranscriptAccessory currentTab={{ type: "settings" } as any}>
213+
<div data-testid="tab-content" />
214+
</GlobalLiveTranscriptAccessory>,
215+
);
216+
217+
expect(
218+
screen.getByTestId("during-session-accessory").dataset,
219+
).toMatchObject({
220+
sessionId: "live-session",
221+
finalizing: "true",
222+
});
223+
expect(screen.queryByRole("button", { name: "Expand Live" })).toBeNull();
224+
});
225+
});

0 commit comments

Comments
 (0)