Skip to content

Commit d329588

Browse files
committed
fix(tts): early exit
1 parent f1247a6 commit d329588

File tree

10 files changed

+193
-71
lines changed

10 files changed

+193
-71
lines changed

src/components/plugins-guard/CsUiPluginsGuard.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
checkPluginDependencies,
1414
checkIncognito,
1515
checkLocation,
16+
checkBrowser,
1617
} from "@/components/plugins-guard/guards";
1718
import { usePluginGuardsStore } from "@/components/plugins-guard/store";
1819
import {
@@ -78,6 +79,7 @@ function useGuardConditions(props: CsUiPluginsGuardProps) {
7879
currentLocation,
7980
});
8081
const incognitoValid = checkIncognito(props, { isIncognito });
82+
const browserValid = checkBrowser(props);
8183

8284
return {
8385
deviceValid,
@@ -86,6 +88,7 @@ function useGuardConditions(props: CsUiPluginsGuardProps) {
8688
dependenciesValid,
8789
locationValid,
8890
incognitoValid,
91+
browserValid,
8992
pluginsEnableStates,
9093
};
9194
}
@@ -98,6 +101,7 @@ export default function CsUiPluginsGuard(props: CsUiPluginsGuardProps) {
98101
dependenciesValid,
99102
locationValid,
100103
incognitoValid,
104+
browserValid,
101105
pluginsEnableStates,
102106
} = useGuardConditions(props);
103107

@@ -117,6 +121,7 @@ export default function CsUiPluginsGuard(props: CsUiPluginsGuardProps) {
117121
dependenciesValid,
118122
locationValid,
119123
incognitoValid,
124+
browserValid,
120125
additionalCheckValid,
121126
].every(Boolean);
122127

src/components/plugins-guard/guards.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
22

33
import {
44
checkDeviceType,
@@ -7,11 +7,19 @@ import {
77
checkPluginDependencies,
88
checkLocation,
99
checkIncognito,
10+
checkBrowser,
1011
type GuardConditions,
1112
type GuardCheckParams,
1213
} from "@/components/plugins-guard/guards";
1314
import { PluginId } from "@/services/extension-local-storage/plugins.types";
1415

16+
// Mock APP_CONFIG
17+
vi.mock("@/app.config", () => ({
18+
APP_CONFIG: {
19+
BROWSER: "chrome",
20+
},
21+
}));
22+
1523
describe("Guard Functions", () => {
1624
describe("checkDeviceType", () => {
1725
it("should return true when no device restrictions", () => {
@@ -163,4 +171,26 @@ describe("Guard Functions", () => {
163171
expect(checkIncognito(conditions, params)).toBe(true);
164172
});
165173
});
174+
175+
describe("checkBrowser", () => {
176+
it("should return true when no browser restrictions", () => {
177+
const conditions: GuardConditions = {};
178+
expect(checkBrowser(conditions)).toBe(true);
179+
});
180+
181+
it("should return true when current browser is in allowed list", () => {
182+
const conditions: GuardConditions = { browser: ["chrome", "firefox"] };
183+
expect(checkBrowser(conditions)).toBe(true);
184+
});
185+
186+
it("should return false when current browser is not in allowed list", () => {
187+
const conditions: GuardConditions = { browser: ["firefox"] };
188+
expect(checkBrowser(conditions)).toBe(false);
189+
});
190+
191+
it("should return true when browser list is empty", () => {
192+
const conditions: GuardConditions = { browser: [] };
193+
expect(checkBrowser(conditions)).toBe(true);
194+
});
195+
});
166196
});

src/components/plugins-guard/guards.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { APP_CONFIG } from "@/app.config";
12
import { ExtensionLocalStorage } from "@/services/extension-local-storage/extension-local-storage.types";
23
import { PluginId } from "@/services/extension-local-storage/plugins.types";
34
import { whereAmI } from "@/utils/utils";
@@ -10,6 +11,7 @@ export type GuardConditions = {
1011
requiresLoggedIn?: boolean;
1112
allowIncognito?: boolean;
1213
allowedAccountTypes?: ("free" | "pro" | "enterprise")[][];
14+
browser?: ("chrome" | "firefox")[];
1315
};
1416

1517
export type GuardCheckParams = {
@@ -88,6 +90,11 @@ export function checkIncognito(
8890
return true;
8991
}
9092

93+
export function checkBrowser({ browser }: GuardConditions): boolean {
94+
if (!browser || !browser?.length) return true;
95+
return browser.includes(APP_CONFIG.BROWSER);
96+
}
97+
9198
export type AdditionalCheckParams = GuardConditions & {
9299
pluginsEnableStates: Record<PluginId, boolean>;
93100
settings: ExtensionLocalStorage;

src/data/plugins-data/plugins-data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export const PLUGINS_METADATA: CplxPluginMetadata = {
122122
routeSegment: "thread-message-tts",
123123
title: "Thread Message TTS",
124124
description: "Enable text-to-speech for messages in threads",
125-
tags: ["new", "ui", "ux"],
125+
tags: ["new", "experimental", "ui", "ux"],
126126
dependentDomObservers: ["coreDomObserver:thread:messageBlocks"],
127127
dependentCorePlugins: ["spaRouter", "reactVdom"],
128128
},

src/data/plugins-data/plugins-tags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ export const PLUGIN_TAGS = {
4747
description:
4848
"May have a noticeable impact on performance in large threads with a large amount of code blocks.",
4949
},
50+
chromiumOnly: {
51+
label: "Chromium Only",
52+
description: "Can only be used on Chromium-based browsers",
53+
},
5054
} as const;

src/plugins/_api/web-socket/internal-web-socket-manager.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ export class InternalWebSocketManager {
1717
return InternalWebSocketManager.instance;
1818
}
1919

20-
public async handShake(
21-
id: string = nanoid(),
22-
upgrade = true,
23-
): Promise<Socket> {
20+
public async handShake(params?: {
21+
id?: string;
22+
upgrade?: boolean;
23+
}): Promise<Socket> {
24+
const { id = nanoid(), upgrade = true } = params ?? {};
25+
2426
return new Promise((resolve, reject) => {
2527
const socket = io("", {
26-
transports: upgrade ? ["polling", "websocket"] : ["polling"],
28+
transports: ["polling", "websocket"],
2729
upgrade,
2830
reconnection: false,
2931
});
@@ -65,7 +67,7 @@ export class InternalWebSocketManager {
6567
let socket = this.getSocket(id);
6668

6769
if (socket == null) {
68-
socket = await this.handShake(id);
70+
socket = await this.handShake({ id });
6971
}
7072

7173
if (socket.io.engine.readyState === "opening") {

src/plugins/thread-message-tts/hooks/usePplxTtsRequest.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMutation } from "@tanstack/react-query";
22
import { Socket } from "socket.io-client";
33

4+
import { APP_CONFIG } from "@/app.config";
45
import { TtsVoice } from "@/data/plugins/thread-message-tts/types";
56
import { InternalWebSocketManager } from "@/plugins/_api/web-socket/internal-web-socket-manager";
67

@@ -25,7 +26,9 @@ export default function usePplxTtsRequest() {
2526
onBufferUpdate?: (chunk: Int16Array) => void;
2627
}) => {
2728
socketRef.current =
28-
await InternalWebSocketManager.getInstance().handShake();
29+
await InternalWebSocketManager.getInstance().handShake({
30+
upgrade: APP_CONFIG.BROWSER === "chrome",
31+
});
2932

3033
const socket = socketRef.current;
3134

src/plugins/thread-message-tts/index.tsx

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,81 +20,90 @@ export default function ThreadMessageTtsButton({
2020
}: {
2121
messageBlockIndex: number;
2222
}) {
23-
const [isOpen, setIsOpen] = useState(false);
24-
const [isPlaying, setIsPlaying] = useState(false);
23+
const [menuOpen, setMenuOpen] = useState(false);
24+
const [playing, setPlaying] = useState(false);
2525
const [firstChunkArrived, setFirstChunkArrived] = useState(false);
2626

2727
const {
28-
mutation: { mutateAsync: playTts },
28+
mutation: { mutateAsync: playTts, isPending },
2929
abort,
3030
} = usePplxTtsRequest();
3131

32+
const pendingRef = useRef(isPending);
33+
34+
useEffect(() => {
35+
pendingRef.current = isPending;
36+
}, [isPending]);
37+
3238
const [player] = useState(() =>
3339
PplxTtsPlayerCoordinator.getInstance().createPlayer({
3440
onStart: () => {
3541
setFirstChunkArrived(true);
3642
},
3743
onComplete: () => {
38-
setIsPlaying(false);
44+
if (pendingRef.current) return;
45+
setPlaying(false);
46+
setFirstChunkArrived(false);
47+
abort();
3948
},
4049
stop: () => {
41-
setIsPlaying(false);
50+
setPlaying(false);
4251
setFirstChunkArrived(false);
4352
abort();
4453
},
4554
}),
4655
);
4756

48-
const stop = useCallback(() => {
57+
const stopTts = useCallback(() => {
4958
PplxTtsPlayerCoordinator.getInstance().stopAllPlayers();
59+
setPlaying(false);
60+
setFirstChunkArrived(false);
5061
}, []);
5162

5263
const initTts = useCallback(
5364
async (params?: { voice: TtsVoice }) => {
54-
stop();
55-
56-
if (isPlaying) {
65+
if (playing) {
66+
stopTts();
5767
return;
5868
}
5969

60-
setIsPlaying(true);
70+
stopTts();
71+
player.startSession();
72+
setPlaying(true);
6173

6274
const backendUuid = await sendMessage(
6375
"reactVdom:getMessageBackendUuid",
64-
{
65-
index: messageBlockIndex,
66-
},
76+
{ index: messageBlockIndex },
6777
"window",
6878
);
6979

70-
if (backendUuid == null) {
71-
setIsPlaying(false);
80+
if (!backendUuid) {
81+
setPlaying(false);
7282
return;
7383
}
7484

85+
const extensionSettings = await ExtensionLocalStorageService.get();
86+
const selectedVoice =
87+
params?.voice || extensionSettings.plugins["thread:messageTts"].voice;
88+
7589
playTts({
7690
backendUuid,
77-
voice:
78-
params?.voice ??
79-
(await ExtensionLocalStorageService.get()).plugins[
80-
"thread:messageTts"
81-
].voice,
82-
onBufferUpdate(chunk) {
83-
player.addChunk(chunk);
84-
},
91+
voice: selectedVoice,
92+
onBufferUpdate: (chunk: Int16Array) => player.addChunk(chunk),
8593
});
8694
},
87-
[isPlaying, stop, messageBlockIndex, playTts, player],
95+
[playing, player, messageBlockIndex, stopTts, playTts],
8896
);
8997

9098
useEffect(() => {
9199
return () => {
92-
stop();
100+
stopTts();
101+
player.clearBuffer();
93102
PplxTtsPlayerCoordinator.getInstance().removePlayer(player);
94103
};
95-
}, [player, stop]);
104+
}, [player, stopTts]);
96105

97-
if (isPlaying && !firstChunkArrived) {
106+
if (playing && !firstChunkArrived) {
98107
return (
99108
<div className="x-rounded-md x-p-2 x-text-muted-foreground">
100109
<LuLoaderCircle className="x-size-4 x-animate-spin" />
@@ -106,37 +115,37 @@ export default function ThreadMessageTtsButton({
106115
<DropdownMenu
107116
lazyMount
108117
unmountOnExit
109-
open={isOpen}
110-
onOpenChange={({ open }) => setIsOpen(open)}
118+
open={menuOpen}
119+
onOpenChange={({ open }) => setMenuOpen(open)}
111120
onSelect={({ value }) => {
112121
initTts({ voice: value as TtsVoice });
113122
ExtensionLocalStorageService.set((draft) => {
114123
draft.plugins["thread:messageTts"].voice = value as TtsVoice;
115124
});
116125
}}
117126
>
118-
<Tooltip content={isPlaying ? t("misc.stop") : t("misc.speakAloud")}>
127+
<Tooltip content={playing ? t("misc.stop") : t("misc.speakAloud")}>
119128
<DropdownMenuTrigger asChild>
120129
<div
121130
className="x-cursor-pointer x-rounded-md x-p-2 x-text-muted-foreground x-transition-all hover:x-bg-secondary hover:x-text-foreground active:x-scale-95"
122131
onClick={(e) => {
123132
e.preventDefault();
124133
e.stopPropagation();
125-
if (isOpen) {
134+
if (menuOpen) {
126135
return;
127136
}
128137
initTts();
129138
}}
130139
onContextMenu={(e) => {
131-
if (isPlaying) {
140+
if (playing) {
132141
return;
133142
}
134143

135144
e.preventDefault();
136-
setIsOpen(true);
145+
setMenuOpen(true);
137146
}}
138147
>
139-
{isPlaying ? (
148+
{playing ? (
140149
<FaStopCircle className="x-size-4 x-text-primary" />
141150
) : (
142151
<HiOutlineSpeakerWave className="x-size-4" />

src/plugins/thread-message-tts/utils/coordinator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ export class PplxTtsPlayerCoordinator {
3737
public stopAllPlayers(): void {
3838
this.players.forEach((player, index) => {
3939
player.stop();
40+
player.clearBuffer();
4041
this.stopFns[index]?.();
4142
});
4243
}
4344

4445
public removePlayer(player: PplxStreamingTtsPlayer): void {
4546
const index = this.players.indexOf(player);
4647
if (index !== -1) {
48+
player.stop();
49+
player.clearBuffer();
4750
this.players.splice(index, 1);
4851
this.stopFns.splice(index, 1);
4952
}

0 commit comments

Comments
 (0)