Skip to content

Commit 719ced5

Browse files
committed
Add TTS feature with autoplay
1 parent b117c69 commit 719ced5

9 files changed

Lines changed: 612 additions & 18 deletions

File tree

src/components/Bot.tsx

Lines changed: 447 additions & 1 deletion
Large diffs are not rendered by default.

src/components/bubbles/BotBubble.tsx

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Marked } from '@ts-stack/markdown';
44
import { FeedbackRatingType, sendFeedbackQuery, sendFileDownloadQuery, updateFeedbackQuery } from '@/queries/sendMessageQuery';
55
import { FileUpload, IAction, MessageType } from '../Bot';
66
import { CopyToClipboardButton, ThumbsDownButton, ThumbsUpButton } from '../buttons/FeedbackButtons';
7+
import { TTSButton } from '../buttons/TTSButton';
78
import FeedbackContentDialog from '../FeedbackContentDialog';
89
import { AgentReasoningBubble } from './AgentReasoningBubble';
910
import { TickIcon, XIcon } from '../icons';
@@ -32,6 +33,12 @@ type Props = {
3233
renderHTML?: boolean;
3334
handleActionClick: (elem: any, action: IAction | undefined | null) => void;
3435
handleSourceDocumentsClick: (src: any) => void;
36+
// TTS props
37+
isTTSEnabled?: boolean;
38+
isTTSLoading?: Record<string, boolean>;
39+
isTTSPlaying?: Record<string, boolean>;
40+
handleTTSClick?: (messageId: string, messageText: string) => void;
41+
handleTTSStop?: (messageId: string) => void;
3542
};
3643

3744
const defaultBackgroundColor = '#f7f8ff';
@@ -481,7 +488,7 @@ export const BotBubble = (props: Props) => {
481488
{action.label}
482489
</button>
483490
) : (
484-
<button>{action.label}</button>
491+
<button type="button">{action.label}</button>
485492
)}
486493
</>
487494
);
@@ -521,9 +528,25 @@ export const BotBubble = (props: Props) => {
521528
)}
522529
</div>
523530
<div>
524-
{props.chatFeedbackStatus && props.message.messageId && (
525-
<>
526-
<div class={`flex items-center px-2 pb-2 ${props.showAvatar ? 'ml-10' : ''}`}>
531+
<div class={`flex items-center px-2 pb-2 ${props.showAvatar ? 'ml-10' : ''}`}>
532+
<Show when={props.isTTSEnabled}>
533+
<TTSButton
534+
feedbackColor={props.feedbackColor}
535+
isLoading={props.isTTSLoading?.[props.message.id || ''] || false}
536+
isPlaying={props.isTTSPlaying?.[props.message.id || ''] || false}
537+
onClick={() => {
538+
const messageId = props.message.id || '';
539+
const messageText = props.message.message || '';
540+
if (props.isTTSPlaying?.[messageId]) {
541+
props.handleTTSStop?.(messageId);
542+
} else {
543+
props.handleTTSClick?.(messageId, messageText);
544+
}
545+
}}
546+
/>
547+
</Show>
548+
{props.chatFeedbackStatus && props.message.messageId && (
549+
<>
527550
<CopyToClipboardButton feedbackColor={props.feedbackColor} onClick={() => copyMessageToClipboard()} />
528551
<Show when={copiedMessage()}>
529552
<div class="copied-message" style={{ color: props.feedbackColor ?? defaultFeedbackColor }}>
@@ -546,18 +569,18 @@ export const BotBubble = (props: Props) => {
546569
{formatDateTime(props.message.dateTime, props?.dateTimeToggle?.date, props?.dateTimeToggle?.time)}
547570
</div>
548571
</Show>
549-
</div>
550-
<Show when={showFeedbackContentDialog()}>
551-
<FeedbackContentDialog
552-
isOpen={showFeedbackContentDialog()}
553-
onClose={() => setShowFeedbackContentModal(false)}
554-
onSubmit={submitFeedbackContent}
555-
backgroundColor={props.backgroundColor}
556-
textColor={props.textColor}
557-
/>
558-
</Show>
559-
</>
560-
)}
572+
</>
573+
)}
574+
</div>
575+
<Show when={showFeedbackContentDialog()}>
576+
<FeedbackContentDialog
577+
isOpen={showFeedbackContentDialog()}
578+
onClose={() => setShowFeedbackContentModal(false)}
579+
onSubmit={submitFeedbackContent}
580+
backgroundColor={props.backgroundColor}
581+
textColor={props.textColor}
582+
/>
583+
</Show>
561584
</div>
562585
</div>
563586
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Show } from 'solid-js';
2+
import { VolumeIcon, SquareStopIcon } from '../icons';
3+
4+
type Props = {
5+
isLoading?: boolean;
6+
isPlaying?: boolean;
7+
feedbackColor?: string;
8+
onClick: () => void;
9+
class?: string;
10+
};
11+
12+
const defaultButtonColor = '#3B81F6';
13+
14+
export const TTSButton = (props: Props) => {
15+
const handleClick = (event: MouseEvent) => {
16+
event.preventDefault();
17+
props.onClick();
18+
};
19+
20+
return (
21+
<button
22+
class={`py-2 px-2 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed transition-all filter hover:brightness-90 active:brightness-75 ${props.class ?? ''}`}
23+
style={{
24+
background: 'transparent',
25+
border: 'none',
26+
color: props.feedbackColor ?? defaultButtonColor,
27+
}}
28+
disabled={props.isLoading}
29+
onClick={handleClick}
30+
type="button"
31+
title={props.isPlaying ? 'Stop audio' : 'Play audio'}
32+
>
33+
<Show
34+
when={!props.isLoading}
35+
fallback={
36+
<div
37+
class="animate-spin rounded-full border-2 border-current border-t-transparent"
38+
style={{
39+
width: '16px',
40+
height: '16px',
41+
}}
42+
/>
43+
}
44+
>
45+
<Show when={!props.isPlaying} fallback={<SquareStopIcon color={props.feedbackColor ?? defaultButtonColor} />}>
46+
<VolumeIcon color={props.feedbackColor ?? defaultButtonColor} />
47+
</Show>
48+
</Show>
49+
</button>
50+
);
51+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { JSX } from 'solid-js/jsx-runtime';
2+
const defaultButtonColor = '#3B81F6';
3+
export const SquareStopIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
4+
<svg
5+
xmlns="http://www.w3.org/2000/svg"
6+
class="icon icon-tabler icon-tabler-square w-4 h-4"
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
stroke={props.color ?? defaultButtonColor}
12+
stroke-width="2"
13+
stroke-linecap="round"
14+
stroke-linejoin="round"
15+
{...props}
16+
>
17+
<rect width="18" height="18" x="3" y="3" rx="2" />
18+
</svg>
19+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { JSX } from 'solid-js/jsx-runtime';
2+
const defaultButtonColor = '#3B81F6';
3+
export const VolumeIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
4+
<svg
5+
xmlns="http://www.w3.org/2000/svg"
6+
class="icon icon-tabler icon-tabler-volume w-4 h-4"
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
stroke={props.color ?? defaultButtonColor}
12+
stroke-width="2"
13+
stroke-linecap="round"
14+
stroke-linejoin="round"
15+
{...props}
16+
>
17+
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
18+
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
19+
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
20+
</svg>
21+
);

src/components/icons/XIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const XIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement> & { isCurrentCo
77
height="24"
88
viewBox="0 0 24 24"
99
fill="none"
10-
stroke={props.isCurrentColor ? 'currentColor' : props.color ?? defaultButtonColor}
10+
stroke={props.isCurrentColor ? 'currentColor' : (props.color ?? defaultButtonColor)}
1111
stroke-width="2"
1212
stroke-linecap="round"
1313
stroke-linejoin="round"

src/components/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export * from './XIcon';
1111
export * from './TickIcon';
1212
export * from './AttachmentIcon';
1313
export * from './SparklesIcon';
14+
export * from './VolumeIcon';
15+
export * from './SquareStopIcon';

src/queries/sendMessageQuery.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ export type LeadCaptureRequest = BaseRequest & {
6161
body: Partial<LeadCaptureInput>;
6262
};
6363

64+
export type GenerateTTSRequest = BaseRequest & {
65+
body: {
66+
chatId: string;
67+
chatflowId: string;
68+
chatMessageId: string;
69+
text: string;
70+
};
71+
signal?: AbortSignal;
72+
};
73+
6474
export const sendFeedbackQuery = ({ chatflowid, apiHost = 'http://localhost:3000', body, onRequest }: CreateFeedbackRequest) =>
6575
sendRequest({
6676
method: 'POST',
@@ -137,3 +147,23 @@ export const addLeadQuery = ({ apiHost = 'http://localhost:3000', body, onReques
137147
body,
138148
onRequest: onRequest,
139149
});
150+
151+
export const generateTTSQuery = async ({ apiHost = 'http://localhost:3000', body, onRequest, signal }: GenerateTTSRequest): Promise<Response> => {
152+
const headers = {
153+
'Content-Type': 'application/json',
154+
};
155+
156+
const requestInfo: RequestInit = {
157+
method: 'POST',
158+
mode: 'cors',
159+
headers,
160+
body: JSON.stringify(body),
161+
signal,
162+
};
163+
164+
if (onRequest) {
165+
await onRequest(requestInfo);
166+
}
167+
168+
return fetch(`${apiHost}/api/v1/text-to-speech/generate`, requestInfo);
169+
};

src/utils/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const sendRequest = async <ResponseData>(
1616
headers?: Record<string, any>;
1717
formData?: FormData;
1818
onRequest?: (request: RequestInit) => Promise<void>;
19+
signal?: AbortSignal;
1920
}
2021
| string,
2122
): Promise<{ data?: ResponseData; error?: Error }> => {
@@ -36,6 +37,7 @@ export const sendRequest = async <ResponseData>(
3637
mode: 'cors',
3738
headers,
3839
body,
40+
signal: typeof params !== 'string' ? params.signal : undefined,
3941
};
4042

4143
if (typeof params !== 'string' && params.onRequest) {

0 commit comments

Comments
 (0)