Skip to content

Commit

Permalink
feat: text to speech
Browse files Browse the repository at this point in the history
  • Loading branch information
pnd280 committed Oct 26, 2024
1 parent aa71a6f commit 61399ce
Show file tree
Hide file tree
Showing 27 changed files with 641 additions and 99 deletions.
22 changes: 22 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ Consider giving a star ⭐ on [Github](https://github.com/pnd280/complexity).

💖 Support the development via [Ko-fi](https://ko-fi.com/pnd280) or [Paypal](https://paypal.me/pnd280).

**EXPERIMENTAL** features are subjected to change/removal without prior notice.

## v0.0.5.1

_Release date: 26th Oct, 2024_

- **NEW** | **EXPERIMENTAL**: Text-to-Speech. Credit to `@asura0_00` for helping with the implementation.
![TTS](https://i.imgur.com/BglHpbJ.png)

- This feature **only works on CHROMIUM browsers**, and only available on Perplexity Pro/Enterprise accounts.
- Support 4 different voices (exactly the same as the mobile/mac app). Right click the "Headphones" icon to open the dropdown menu.
- Play/pause, seek, volume control, download will be added soon.

- **FIX**: Fixed minor bugs.

## v0.0.4.8

_Release date: 25th Oct, 2024_

- **FIX**: Fixed crashing issue on certain scenarios.
- **FIX**: Fixed store url on Firefox browsers.

## v0.0.4.7

_Release date: 24th Oct, 2024_
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "complexity",
"displayName": "Complexity - Perplexity AI Supercharged",
"version": "0.0.4.8",
"version": "0.0.5.1",
"author": "pnd280",
"description": "⚡ Supercharge your Perplexity AI",
"type": "module",
Expand Down Expand Up @@ -38,6 +38,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dompurify": "^3.1.7",
"engine.io-parser": "^5.2.3",
"immer": "^10.1.1",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions src/content-script/components/QueryBox/QueryBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@/content-script/components/QueryBox/context";
import ImageModelSelector from "@/content-script/components/QueryBox/ImageModelSelector";
import useFetchUserSettings from "@/content-script/hooks/useFetchUserSettings";
import { validateHasActivePplxSub } from "@/content-script/hooks/useHasActivePplxSub";
import useInitQueryBoxSessionStore from "@/content-script/hooks/useInitQueryBoxSessionStore";
import useQueryBoxObserver from "@/content-script/hooks/useQueryBoxObserver";
import useCplxGeneralSettings from "@/cplx-user-settings/hooks/useCplxGeneralSettings";
Expand All @@ -23,12 +24,10 @@ export default function QueryBox() {
error: userSettingsFetchError,
} = useFetchUserSettings();

useInitQueryBoxSessionStore();

const hasActivePplxSub =
userSettings &&
(userSettings.subscriptionStatus === "active" ||
userSettings.subscriptionStatus === "trialing");
userSettings && validateHasActivePplxSub(userSettings);

useInitQueryBoxSessionStore();

const [containers, setContainers] = useState<HTMLElement[]>([]);
const [followUpContainers, setFollowUpContainers] = useState<HTMLElement[]>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import { FaEllipsis as Ellipsis } from "react-icons/fa6";
import {
LuListOrdered as ListOrdered,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
type SpeakingAnimationProps = {
isActive?: boolean;
rows?: number;
cols?: number;
};

const SpeakingAnimation: React.FC<SpeakingAnimationProps> = ({
isActive = true,
rows = 4,
cols = 12,
}) => {
const [activeDots, setActiveDots] = useState<number[]>([]);

useEffect(() => {
if (!isActive) {
setActiveDots([]);
return;
}

const interval = setInterval(() => {
const totalDots = rows * cols;
const newActiveDots = Array.from({
length: Math.floor(totalDots / 3),
}).map(() => Math.floor(Math.random() * totalDots));
setActiveDots(newActiveDots);
}, 400);

return () => clearInterval(interval);
}, [isActive, rows, cols]);

return (
<div
className="tw-grid"
style={{
gridTemplateColumns: `repeat(${cols}, 1fr)`,
gridTemplateRows: `repeat(${rows}, 1fr)`,
gap: "3px",
}}
>
{Array.from({ length: rows * cols }).map((_, i) => (
<div
key={i}
className={cn(
"tw-h-[3px] tw-w-[3px] tw-rounded-full tw-transition-colors tw-duration-300", // Increased duration from 300 to 500
activeDots.includes(i)
? "tw-bg-foreground"
: "tw-bg-muted-foreground",
)}
/>
))}
</div>
);
};

export default SpeakingAnimation;
101 changes: 101 additions & 0 deletions src/content-script/components/ThreadMessageStickyToolbar/TtsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { LuHeadphones, LuPause } from "react-icons/lu";

import { Container } from "@/content-script/components/ThreadMessageStickyToolbar";
import SpeakingAnimation from "@/content-script/components/ThreadMessageStickyToolbar/TtsAnimation";
import useTextToSpeech from "@/content-script/hooks/useTextToSpeech";
import CplxUserSettings from "@/cplx-user-settings/CplxUserSettings";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuContext,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/components/DropdownMenu";
import { DomSelectors } from "@/utils/DomSelectors";
import { TTS_VOICES, TtsVoice } from "@/utils/tts";

export default function TtsButton({
containers,
containerIndex,
}: {
containers: Container[];
containerIndex: number;
}) {
const { isSpeaking, startPlaying, stopPlaying } = useTextToSpeech({
messageIndex: containerIndex + 1,
});

const $bottomButtonBar = $(containers?.[containerIndex]?.messageBlock).find(
DomSelectors.THREAD.MESSAGE.BOTTOM_BAR,
);

const play = async (voice: TtsVoice) => {
if (isSpeaking) {
stopPlaying();
} else {
startPlaying({
voice,
});
}
};

if (!$bottomButtonBar.length) return null;

return (
<DropdownMenu>
<DropdownMenuContext>
{(context) => (
<DropdownMenuTrigger>
<div
className={cn(
"tw-flex tw-w-max tw-items-center tw-gap-2 tw-rounded-md tw-p-3 tw-transition-all tw-animate-in tw-fade-in hover:tw-cursor-pointer hover:tw-bg-secondary active:tw-scale-95",
{
"tw-bg-secondary tw-px-4 tw-shadow-lg": isSpeaking,
},
)}
onClick={(e) => {
e.stopPropagation();

if (context.open) context.setOpen(false);

play(CplxUserSettings.get().defaultTtsVoice ?? TTS_VOICES[0]);
}}
onContextMenu={(e) => {
e.preventDefault();

if (isSpeaking) return;

context.setOpen(!context.open);
}}
>
{isSpeaking ? (
<>
<SpeakingAnimation isActive rows={3} cols={15} />
<LuPause className="tw-size-4" />
</>
) : (
<LuHeadphones className="tw-size-4" />
)}
</div>
</DropdownMenuTrigger>
)}
</DropdownMenuContext>
<DropdownMenuContent>
{TTS_VOICES.map((voice) => (
<DropdownMenuItem
key={voice}
value={voice}
onClick={() => {
play(voice);
CplxUserSettings.set((state) => {
state.defaultTtsVoice = voice;
});
}}
>
{voice}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
37 changes: 26 additions & 11 deletions src/content-script/components/ThreadMessageStickyToolbar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import debounce from "lodash/debounce";
import {
Fragment,
useCallback,
useDeferredValue,
useEffect,
Expand All @@ -9,8 +10,11 @@ import {
} from "react";
import { Updater, useImmer } from "use-immer";

import appConfig from "@/app.config";
import Toolbar from "@/content-script/components/ThreadMessageStickyToolbar/Toolbar";
import useThreadMessageStickyToolbarObserver from "@/content-script/hooks/useThreadMessageStickyToolbarObserver";
import TtsButton from "@/content-script/components/ThreadMessageStickyToolbar/TtsButton";
import useThreadMessageStickyToolbarObserver from "@/content-script/components/ThreadMessageStickyToolbar/useThreadMessageStickyToolbarObserver";
import useHasActivePplxSub from "@/content-script/hooks/useHasActivePplxSub";
import Portal from "@/shared/components/Portal";
import { DomSelectors } from "@/utils/DomSelectors";
import UiUtils from "@/utils/UiUtils";
Expand All @@ -21,6 +25,7 @@ export type Container = {
query: Element;
container: Element;
answer: Element;
answerHeading: Element;
};

export type ContainerStates = {
Expand Down Expand Up @@ -48,7 +53,8 @@ const isChanged = (prev: Container[], next: Container[]): boolean => {
prev[i].messageBlock !== next[i].messageBlock ||
prev[i].query !== next[i].query ||
prev[i].container !== next[i].container ||
prev[i].answer !== next[i].answer
prev[i].answer !== next[i].answer ||
prev[i].answerHeading !== next[i].answerHeading
) {
return true;
}
Expand All @@ -57,6 +63,8 @@ const isChanged = (prev: Container[], next: Container[]): boolean => {
};

export default function ThreadMessageStickyToolbar() {
const { hasActivePplxSub } = useHasActivePplxSub();

const [containers, setContainers] = useState<Container[]>([]);
const deferredContainers = useDeferredValue(containers);
const [containersStates, setContainersStates] = useImmer<ContainerStates[]>(
Expand Down Expand Up @@ -115,17 +123,24 @@ export default function ThreadMessageStickyToolbar() {
if (containers[index] == null) return null;

return (
<Portal key={index} container={container.container as HTMLElement}>
<Toolbar
containers={containers}
containersStates={containersStates}
containerIndex={index}
setContainersStates={setContainersStates}
/>
</Portal>
<Fragment key={index}>
<Portal container={container.container as HTMLElement}>
<Toolbar
containers={containers}
containersStates={containersStates}
containerIndex={index}
setContainersStates={setContainersStates}
/>
</Portal>
{appConfig.BROWSER === "chrome" && hasActivePplxSub && (
<Portal container={container.answerHeading as HTMLElement}>
<TtsButton containers={containers} containerIndex={index} />
</Portal>
)}
</Fragment>
);
},
[containers, containersStates, setContainersStates],
[containers, containersStates, hasActivePplxSub, setContainersStates],
);

return deferredContainers.map(renderToolbar);
Expand Down
Loading

0 comments on commit 61399ce

Please sign in to comment.