Skip to content

feat: Add loading indicator during TTS audio generation#369

Open
pixeltannu wants to merge 2 commits into
itzzavdhesh:mainfrom
pixeltannu:feat/loading-indicator-tts
Open

feat: Add loading indicator during TTS audio generation#369
pixeltannu wants to merge 2 commits into
itzzavdhesh:mainfrom
pixeltannu:feat/loading-indicator-tts

Conversation

@pixeltannu

@pixeltannu pixeltannu commented Jun 18, 2026

Copy link
Copy Markdown

🚀 Program

GSSoC

📝 Description

Adds a loading indicator while text-to-speech audio is being generated, so users get clear visual feedback instead of an unresponsive-looking Speak button.

  • Added a spinner and "Generating speech..." status text in TextToSpeech.jsx, shown while the TTS request is in progress.
  • Disabled the Speak button and textarea during the loading state to prevent duplicate requests.
  • Loading state automatically resets once the audio is ready or an error occurs.

🔗 Related Issue

Closes #363

🔄 Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 🔍 SEO improvement
  • 🎨 Style / UI improvement
  • ♿ Accessibility improvement
  • 📝 Documentation
  • ⚙️ CI / configuration
  • 🧹 Refactor / cleanup

🧪 How to Test

  1. Open http://localhost:5173 in a browser
  2. Select or create a voice profile so the Speak button is enabled
  3. Type any text in the "Type to speak" box and click Speak
  4. Confirm a spinner and "Generating speech..." text appear, and the button/textarea are disabled
  5. Confirm the loading state clears once audio is ready (or on error)

📸 Screenshots (if applicable)

N/A

✅ Checklist

  • I am contributing under GSSoC
  • My code follows the project's existing style
  • I have tested my changes in a browser
  • I have linked the related issue above
  • My PR title follows Conventional Commits format (e.g. feat: add voice preview)

Summary by CodeRabbit

  • New Features

    • Added loading indicator and "Generating speech…" message during speech generation
    • Speak button displays animated loader and "Generating..." status while processing
  • Bug Fixes

    • Prevents duplicate speech generation requests
    • Re-spoken messages now correctly update position in history after reload

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

@pixeltannu is attempting to deploy a commit to the itzzavdhesh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

✅ DCO Verified

Hey @pixeltannu! 👋 All commits include a valid Signed-off-by: line.

Note

Your PR can continue through the normal review process.


🤖 VoiceForge Automation · Updates automatically on edits

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

🎉 PR Ready for Mentor Review

Hey @pixeltannu! 👋 Your PR passed all checks and is now in the GSSoC review queue.

Note

🔗 Closing: #363 · 📐 94 lines across 2 file(s) · 📬 Already requested or no eligible reviewer found

@sabeenaviklar @Anushreebasics @itsdakshjain @snehkris @1754riya @Mrigakshi-Rathore @Itzzavdheshh @vedhapprakashni, this PR is ready for your review — please confirm scope, check behavior and tests, then approve or request changes.

Important

This is not an approval. Please wait for mentor feedback before expecting a merge. If changes are requested, push them to this same branch and keep the PR focused on the linked issue.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds an isLoading boolean derived from status === "speaking" to the TextToSpeech component, imports Loader2, and uses it to guard the submit function against duplicate calls, disable the textarea, show a "Generating speech…" status block, and swap the Speak button's icon and label during speech generation. In parallel, useSpeechHistory's addMessage now preserves and re-timestamps duplicate history entries so re-spoken messages reorder correctly after reload.

Changes

TTS Loading Indicator and History Deduplication

Layer / File(s) Summary
isLoading state and submit guard
client/src/components/TextToSpeech.jsx
Adds Loader2 import, derives isLoading from status === "speaking", and extends the submit early-return guard to include isLoading, preventing duplicate API calls while speech is generating.
Loading UI: textarea, status message, and Speak button
client/src/components/TextToSpeech.jsx
Updates textarea disabled to disabled || isLoading, conditionally renders a "Generating speech…" block with spinning Loader2 when loading, and toggles the Speak button between Loader2/"Generating..." and SendHorizontal/"Speak" based on isLoading.
History deduplication and timestamp update
client/src/hooks/useSpeechHistory.js
Updates addMessage to preserve existing history entries while refreshing their timestamp via Date.now() when text matches, ensuring re-spoken messages reorder correctly without creating duplicates. New entries continue to use crypto.randomUUID() with the current timestamp.

Sequence Diagram

sequenceDiagram
  participant User
  participant TextToSpeech
  participant useSpeechHistory
  participant History List

  User->>TextToSpeech: Click Speak button
  TextToSpeech->>TextToSpeech: isLoading check passes (not already speaking)
  TextToSpeech->>TextToSpeech: Show Loader2 + "Generating..."<br/>Disable textarea
  TextToSpeech->>useSpeechHistory: onSpeak(text)
  useSpeechHistory->>History List: Check if text exists
  alt Duplicate found
    useSpeechHistory->>History List: Update timestamp to Date.now()
    useSpeechHistory->>History List: Move to top
  else New text
    useSpeechHistory->>History List: Create entry with UUID
  end
  History List->>useSpeechHistory: Return updated history (capped to MAX)
  Note over TextToSpeech: Audio streams from API
  TextToSpeech->>TextToSpeech: status changes, isLoading clears
  TextToSpeech->>TextToSpeech: Show SendHorizontal + "Speak"<br/>Enable textarea
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • itzzavdhesh/VoiceForge#286: Both PRs update useSpeechHistory's addMessage to preserve and re-timestamp duplicate entries for correct message reordering.
  • itzzavdhesh/VoiceForge#294: Overlaps with useSpeechHistory timestamp handling in the same hook's duplicate-detection logic.
  • itzzavdhesh/VoiceForge#11: Related to speech-history persistence and deduplication system changes in useSpeechHistory.

Suggested labels

type:feature, level:intermediate, quality:clean

Suggested reviewers

  • snehkris
  • Anushreebasics
  • sabeenaviklar
  • itsdakshjain

Poem

🐇 Hop hop, the spinner spins so bright!
No more clicking twice — loading state's in sight.
A Loader2 whirls, the button turns to grey,
"Generating speech…" keeps doubt at bay.
Re-spoken words? Timestamps refresh today! 🎙️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add loading indicator during TTS audio generation' directly and accurately describes the main feature being implemented in the changeset.
Linked Issues check ✅ Passed The PR implementation fulfills all key coding requirements from #363: adds loading indicator with spinner, disables button/textarea during generation, prevents duplicate requests, and persists until audio streaming begins.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the loading indicator feature; modifications to TextToSpeech.jsx UI/logic and useSpeechHistory.js duplicate-handling are directly related to the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/components/TextToSpeech.jsx`:
- Around line 29-35: The submit() function has a race condition where rapid
clicks can both pass the guard clause before parent state updates, causing
duplicate onSpeak calls. Implement a local in-flight latch by creating a local
state variable (using useState) to track whether an API request is currently in
progress. In the submit() function, check this local state in the guard clause
and set it to true immediately before calling onSpeak, then set it back to false
after the call completes. Apply the same pattern to the other affected locations
mentioned at lines 68-69 and 86-87.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 572501f6-00cf-494e-b6ec-f098b3620615

📥 Commits

Reviewing files that changed from the base of the PR and between c926322 and e18bcf9.

📒 Files selected for processing (1)
  • client/src/components/TextToSpeech.jsx

Comment on lines +29 to +35
const isLoading = status === "speaking";

async function submit() {
if (!trimmedText || disabled) return;
await onSpeak(trimmedText);
setText("");
}
if (!trimmedText || disabled || isLoading) return;
await onSpeak(trimmedText);
setText("");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the pre-rerender race that still allows duplicate onSpeak calls.

At Line 31/Line 32, duplicate prevention relies on parent-driven status (isLoading). Because that state arrives on a later render, two rapid clicks can still enter submit() before isLoading flips, triggering duplicate API requests.

Suggested patch (local in-flight latch)
 export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
   const [text, setText] = React.useState("");
+  const [isSubmitting, setIsSubmitting] = React.useState(false);
   const trimmedText = text.trim();
@@
   const isLoading = status === "speaking";
+  const isBusy = isLoading || isSubmitting;
@@
   async function submit() {
-    if (!trimmedText || disabled || isLoading) return;
-    await onSpeak(trimmedText);
-    setText("");
+    if (!trimmedText || disabled || isBusy) return;
+    setIsSubmitting(true);
+    try {
+      await onSpeak(trimmedText);
+      setText("");
+    } finally {
+      setIsSubmitting(false);
+    }
   }
@@
-        disabled={disabled || isLoading}
+        disabled={disabled || isBusy}
@@
-      {isLoading && (
+      {isBusy && (
@@
-        disabled={disabled || !trimmedText || isLoading}
-        aria-busy={isLoading}
+        disabled={disabled || !trimmedText || isBusy}
+        aria-busy={isBusy}
@@
-        {isLoading ? (
+        {isBusy ? (
@@
-        {isLoading ? "Generating..." : "Speak"}
+        {isBusy ? "Generating..." : "Speak"}
       </button>

Also applies to: 68-69, 86-87

🧰 Tools
🪛 ast-grep (0.43.0)

[error] 33-33: React's useState should not be directly called
Context: setText("")
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/TextToSpeech.jsx` around lines 29 - 35, The submit()
function has a race condition where rapid clicks can both pass the guard clause
before parent state updates, causing duplicate onSpeak calls. Implement a local
in-flight latch by creating a local state variable (using useState) to track
whether an API request is currently in progress. In the submit() function, check
this local state in the guard clause and set it to true immediately before
calling onSpeak, then set it back to false after the call completes. Apply the
same pattern to the other affected locations mentioned at lines 68-69 and 86-87.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="client/src/components/TextToSpeech.jsx">

<violation number="1" location="client/src/components/TextToSpeech.jsx:32">
P2: Race condition: the `isLoading` guard is derived from the parent's `status` prop, which only updates on a subsequent render. Two rapid clicks can both enter `submit()` before `isLoading` flips to `true`, resulting in duplicate TTS API requests. Add a local `isSubmitting` state that is set synchronously before `await onSpeak(trimmedText)` and cleared in a `finally` block to close this window.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

await onSpeak(trimmedText);
setText("");
}
if (!trimmedText || disabled || isLoading) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Race condition: the isLoading guard is derived from the parent's status prop, which only updates on a subsequent render. Two rapid clicks can both enter submit() before isLoading flips to true, resulting in duplicate TTS API requests. Add a local isSubmitting state that is set synchronously before await onSpeak(trimmedText) and cleared in a finally block to close this window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/components/TextToSpeech.jsx, line 32:

<comment>Race condition: the `isLoading` guard is derived from the parent's `status` prop, which only updates on a subsequent render. Two rapid clicks can both enter `submit()` before `isLoading` flips to `true`, resulting in duplicate TTS API requests. Add a local `isSubmitting` state that is set synchronously before `await onSpeak(trimmedText)` and cleared in a `finally` block to close this window.</comment>

<file context>
@@ -1,37 +1,38 @@
-  await onSpeak(trimmedText);
-  setText("");
-}
+    if (!trimmedText || disabled || isLoading) return;
+    await onSpeak(trimmedText);
+    setText("");
</file context>

@pixeltannu

Copy link
Copy Markdown
Author

PR is ready for review whenever a mentor has time. Thanks!

…vdhesh#363)

Signed-off-by: Tannu Kumari <tannu.kumarieng1@gmail.com>
Signed-off-by: Tannu Kumari <tannu.kumarieng1@gmail.com>
@pixeltannu pixeltannu force-pushed the feat/loading-indicator-tts branch from e18bcf9 to 3878999 Compare June 18, 2026 14:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
client/src/components/TextToSpeech.jsx (1)

31-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the remaining pre-rerender duplicate-submit race.

isLoading depends on parent status, so two rapid submits can still pass the guard before the next render and trigger duplicate onSpeak calls. Add a local in-flight latch and use it for guard + disabled states.

Suggested patch
 export default function TextToSpeech({ onSpeak, disabled = false, status = "idle" }) {
   const [text, setText] = React.useState("");
+  const [isSubmitting, setIsSubmitting] = React.useState(false);
   const trimmedText = text.trim();
@@
   const isLoading = status === "speaking";
+  const isBusy = isLoading || isSubmitting;
 
   async function submit() {
-    if (!trimmedText || disabled || isLoading) return;
-    await onSpeak(trimmedText);
-    setText("");
+    if (!trimmedText || disabled || isBusy) return;
+    setIsSubmitting(true);
+    try {
+      await onSpeak(trimmedText);
+      setText("");
+    } finally {
+      setIsSubmitting(false);
+    }
   }
@@
-        disabled={disabled || isLoading}
+        disabled={disabled || isBusy}
@@
-      {isLoading && (
+      {isBusy && (
@@
-        disabled={disabled || !trimmedText || isLoading}
-        aria-busy={isLoading}
+        disabled={disabled || !trimmedText || isBusy}
+        aria-busy={isBusy}
@@
-        {isLoading ? (
+        {isBusy ? (
@@
-        {isLoading ? "Generating..." : "Speak"}
+        {isBusy ? "Generating..." : "Speak"}

Also applies to: 68-69, 86-87

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/TextToSpeech.jsx` around lines 31 - 35, The submit
function in TextToSpeech has a race condition where two rapid submits can bypass
the isLoading guard before the parent re-renders, causing duplicate onSpeak
calls. Add a local state variable (an in-flight latch) to track whether a
request is currently in progress. Update the submit function to check this local
state in the guard condition before calling onSpeak, set the latch to true when
the async onSpeak call starts, and reset it to false when complete. Also update
the disabled state logic to use this local latch alongside the existing checks.
Apply the same pattern to the other submit locations referenced at lines 68-69
and 86-87.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@client/src/components/TextToSpeech.jsx`:
- Around line 31-35: The submit function in TextToSpeech has a race condition
where two rapid submits can bypass the isLoading guard before the parent
re-renders, causing duplicate onSpeak calls. Add a local state variable (an
in-flight latch) to track whether a request is currently in progress. Update the
submit function to check this local state in the guard condition before calling
onSpeak, set the latch to true when the async onSpeak call starts, and reset it
to false when complete. Also update the disabled state logic to use this local
latch alongside the existing checks. Apply the same pattern to the other submit
locations referenced at lines 68-69 and 86-87.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f1abdb60-3a56-46a9-a7bb-804717ecf68d

📥 Commits

Reviewing files that changed from the base of the PR and between e18bcf9 and 3878999.

📒 Files selected for processing (2)
  • client/src/components/TextToSpeech.jsx
  • client/src/hooks/useSpeechHistory.js

@pixeltannu

Copy link
Copy Markdown
Author

Please review this PR when you get a chance. This PR closes issue #363. CodeRabbit also flagged a minor race condition issue in TextToSpeech.jsx — should I fix it before merging, or is it okay to proceed?

@Anushreebasics Anushreebasics left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls apply bot's code suggestions
add empty line at the end of the file

@github-actions github-actions Bot added mentor:Anushreebasics GSSoC: Mentor-@Anushreebasics needs revision and removed ready for review labels Jun 18, 2026
@github-actions

Copy link
Copy Markdown

🔄 Changes Requested

Hey @pixeltannu! 👋 A mentor has reviewed your PR and requested some changes.

Warning

Please review the feedback above, update this same branch, and keep the PR focused on the linked issue.

Once you push your updates, the review flow will continue automatically on this same PR.


🤖 VoiceForge Automation · Updates automatically on edits

@itsdakshjain itsdakshjain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve bot comments and empty line as stated by Anushree

@itsdakshjain itsdakshjain added the mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain label Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add Loading Indicator During TTS Audio Generation

3 participants