Skip to content

Feat/unsaved changes safeguard 1172 - #1222

Merged
Itzzavdheshh merged 14 commits into
itzzavdhesh:mainfrom
Myparadox-creator:feat/unsaved-changes-safeguard-1172
Aug 12, 2026
Merged

Feat/unsaved changes safeguard 1172#1222
Itzzavdheshh merged 14 commits into
itzzavdhesh:mainfrom
Myparadox-creator:feat/unsaved-changes-safeguard-1172

Conversation

@Myparadox-creator

@Myparadox-creator Myparadox-creator commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Program

ELUSOC

📝 Description

This PR introduces safeguards to prevent accidental data loss when users refresh or close the browser tab (F5, Ctrl+W) while composing speech or setting up custom voice clone profiles.

Key Changes:

  • useUnsavedChanges Custom Hook: Created a reusable hook (client/src/hooks/useUnsavedChanges.js) that attaches a beforeunload window listener whenever unsaved changes exist (hasUnsavedChanges === true).
  • Session Storage Draft Persistence: Updated TextToSpeech.jsx to sync active composer text into sessionStorage (voiceforge_draft_text), automatically restoring draft text upon page reload.
  • Submit Draft Cleanup: Clears draft from sessionStorage and removes beforeunload warning upon speech submission or manual text clear.
  • Voice Cloning Safeguard: Attached useUnsavedChanges to Onboarding.jsx while reference voice audio is staged or being processed.
  • Unit Testing: Added unit tests in useUnsavedChanges.test.js verifying module exports and event prevention handlers.

🔗 Related Issue

Closes #1172

🔄 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. Navigate to Speech Composer (TextToSpeech) and type a multi-sentence paragraph.
  3. Attempt to refresh (F5) or close the tab (Ctrl+W) -> observe browser beforeunload warning popup ("Changes you made may not be saved").
  4. Reload the page -> observe draft text is automatically restored from sessionStorage.
  5. Press Enter to speak -> observe draft is cleared from sessionStorage and subsequent refreshes do not prompt.
  6. Navigate to Voice Cloning setup (Onboarding.jsx), record/upload reference audio -> attempt refresh -> observe beforeunload warning modal.

📸 Screenshots (if applicable)

Screenshot 2026-08-03 201627 Screenshot 2026-08-03 201640

✅ Checklist

  • I am contributing under ELUSOC
  • 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 cubic

Protects in‑progress composer text and voice‑cloning setup from tab close/refresh and restores drafts on reload. Previously, refreshing or closing the tab discarded work; now we show a before‑unload prompt when unsaved input exists and persist drafts in session storage until submission. Drafts previously saved in local storage will not auto‑load. Closes #1172.

  • Introduces useUnsavedChanges; applied in TextToSpeech, VoiceForge, and Onboarding.
  • Drafts now use sessionStorage (voiceforge_draft_text, voiceforge_composer_draft_text) and are cleared on submit.
  • Composer: caps input at 300 chars, calls onSpeak once, removes the Clear button.
  • Calls: persists subtitle preferences (subtitlesEnabled, subtitleFontSize, subtitleBgOpacity) and wires them to preview.
  • Recorder/Onboarding: capture duration/isValid, preserve state during extraction, and remove front‑end size/duration hard limits.
  • Adds unit tests for the useUnsavedChanges module.

Written for commit c4fb229. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added subtitle overlays during speech, with saved preferences for visibility, font size, and background opacity.
    • Added message tagging, tag-based filtering, and improved speech-history analytics.
    • Draft text is now restored automatically and saved between sessions.
    • Added warnings before leaving with unsaved text, recordings, or voice-cloning progress.
    • Added production deployment support with health monitoring.
  • Bug Fixes

    • Improved recording and voice extraction reliability.
    • Added a friendly recovery screen for unexpected application errors.

…ified asset serving

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…dingReady callback

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
… and client

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…d verify tag callbacks

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…ate symbols

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…tate during extraction

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
… files at 13

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…and add draft persistence (itzzavdhesh#1172)

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
…main VoiceForge composer (itzzavdhesh#1172)

Signed-off-by: Aditya R. Satapathy <adityaranjanwxd@gmail.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

@Myparadox-creator 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 Aug 3, 2026

Copy link
Copy Markdown

✍️ DCO Sign-off Needed

Hey @Myparadox-creator! 👋 One or more commits in this PR are missing a Signed-off-by: line.

Warning

  • ea9e8db Merge branch 'main' into feat/unsaved-changes-safeguard-1172
  • c4fb229 Merge branch 'main' into feat/unsaved-changes-safeguard-1172

How to fix:

For the latest commit:

git commit --amend --signoff
git push --force-with-lease

For multiple commits, replace N with the number to update:

git rebase --signoff HEAD~N
git push --force-with-lease

This comment will update automatically after you push.


🤖 VoiceForge Automation · Updates automatically on edits

@github-actions github-actions Bot added bug Something isn't working client documentation Improvements or additions to documentation enhancement New feature or request large PR needs-assignee-match needs-title server size/l labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🛠️ PR Needs Updates

Hey @Myparadox-creator! 👋 A few things need fixing before a mentor can review this PR.

Warning

How to fix:

  • Issue assignment: Ask a maintainer to assign the linked issue to @Myparadox-creator, or open a PR only for an issue already assigned to you.
  • PR title: Use a clear title like fix: update onboarding progress bar or [bug refactor]: replace pending stream cache.

Once fixed, the workflow re-runs automatically and pings the right mentor.


🤖 VoiceForge Automation · Updates automatically on edits

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Myparadox-creator, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c477d667-f28b-4dcf-a3d9-29cc506660bf

📥 Commits

Reviewing files that changed from the base of the PR and between d61443e and ea9e8db.

📒 Files selected for processing (10)
  • client/src/App.jsx
  • client/src/components/TextToSpeech.jsx
  • client/src/components/VideoPreview.jsx
  • client/src/components/VoiceForge.jsx
  • client/src/components/VoiceRecorder.jsx
  • client/src/hooks/useSpeechHistory.js
  • client/src/main.jsx
  • client/src/pages/Call.jsx
  • client/src/pages/Onboarding.jsx
  • server/index.js
📝 Walkthrough

Walkthrough

The PR adds production Docker and Compose support, SPA serving, draft persistence, unload protection, speech-history tags and analytics, subtitle overlays, recording validation, an application error boundary, and explicit frontend import extensions. It also removes the README About section.

Changes

VoiceForge delivery and frontend behavior

Layer / File(s) Summary
Containerized production delivery
.dockerignore, Dockerfile, docker-compose.yml, server/index.js, README.md
Adds a multi-stage Node Alpine image, Compose health checks, production SPA serving, and removes the README About section.
Draft persistence and unload protection
client/src/hooks/useUnsavedChanges.*, client/src/components/TextToSpeech.jsx, client/src/components/VoiceForge.jsx, client/src/pages/Onboarding.jsx
Persists composer drafts, restores saved text, clears drafts after speaking, and warns before unload when work remains.
Speech history tags and analytics
client/src/hooks/useSpeechHistory.js, client/src/components/SpeechHistory.jsx, client/src/components/VoiceForge.jsx
Adds tag management, filtering, normalized backup tags, analytics, and UUID fallback handling.
Persisted subtitle overlay
client/src/pages/Call.jsx, client/src/components/VideoPreview.jsx
Persists subtitle settings and passes active speech text to the preview rendering loop.
Recording extraction and normalization
client/src/components/VoiceRecorder.jsx, client/src/pages/Onboarding.jsx
Supports multiple extraction response shapes, rejects invalid blobs, prevents post-unmount updates, and normalizes recording metadata.
Application resilience and module resolution
client/src/main.jsx, client/src/App.jsx
Adds an application error boundary and explicit local import extensions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • #1187 — Covers the draft persistence and recovery behavior added in VoiceForge.jsx and TextToSpeech.jsx.

Possibly related PRs

Suggested labels: type:accessibility, type:testing

Suggested reviewers: vivek0028, itzzavdhesh, hrshjswniii

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes unrelated Docker, deployment, subtitle, tagging, analytics, error-boundary, and static-serving changes beyond issue #1172. Remove unrelated changes or split them into separate pull requests so this PR only addresses unsaved-data protection.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the unsaved-change safeguard and references issue #1172.
Linked Issues check ✅ Passed The PR implements composer draft persistence and beforeunload protection during onboarding, satisfying the data-loss safeguards in issue #1172.
✨ Finishing Touches 💡 2
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch feat/unsaved-changes-safeguard-1172
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/src/pages/Call.jsx (1)

208-225: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep activeText until the current playback ends.

useTTS().speak() does not await playback completion, and VideoPreview shows subtitles only while audio.onPlay sets isSpeaking to true. Clear activeText from a matching playback-ended path instead of the TTS generation finally; clear it immediately only for a matching generation request that fails before playback starts.

🤖 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/pages/Call.jsx` around lines 208 - 225, Update the speak flow
around setActiveText and useTTS().speak() so the finally block no longer clears
activeText after generation returns. Clear activeText from the matching
playback-ended handler, and only clear it immediately when the matching
generation request fails before playback begins; ensure stale or unrelated
playback events cannot clear newer active text.
🧹 Nitpick comments (3)
client/src/hooks/useUnsavedChanges.test.js (1)

10-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test does not exercise the real hook implementation.

This test defines its own copy of handleBeforeUnload instead of invoking the handler that useUnsavedChanges registers. If the real handler in useUnsavedChanges.js changes or breaks, this test still passes, because it only checks the local duplicate.

Render a component that calls the real hook, and capture the listener passed to window.addEventListener("beforeunload", ...) for testing.

♻️ Proposed fix to test the real hook
-import { describe, it, expect, vi } from "vitest";
+import { describe, it, expect, vi } from "vitest";
+import { render } from "`@testing-library/react`";
+import { useUnsavedChanges } from "./useUnsavedChanges.js";
+
+function TestComponent({ condition }) {
+  useUnsavedChanges(condition);
+  return null;
+}
 
 describe("useUnsavedChanges module", () => {
   it("exports useUnsavedChanges function", async () => {
     const mod = await import("./useUnsavedChanges.js");
     expect(typeof mod.useUnsavedChanges).toBe("function");
     expect(typeof mod.default).toBe("function");
   });
 
   it("handles beforeunload event and prevents default", () => {
-    const event = {
-      preventDefault: vi.fn(),
-      returnValue: undefined,
-    };
-
-    const handleBeforeUnload = (evt) => {
-      evt.preventDefault();
-      evt.returnValue = "";
-      return "";
-    };
-
-    handleBeforeUnload(event);
-
-    expect(event.preventDefault).toHaveBeenCalled();
-    expect(event.returnValue).toBe("");
+    const addSpy = vi.spyOn(window, "addEventListener");
+    render(<TestComponent condition={true} />);
+    const call = addSpy.mock.calls.find(([type]) => type === "beforeunload");
+    const handler = call?.[1];
+    const event = { preventDefault: vi.fn(), returnValue: undefined };
+    handler(event);
+    expect(event.preventDefault).toHaveBeenCalled();
+    expect(event.returnValue).toBe("");
   });
 });

Verify whether @testing-library/react (or an equivalent) is already used in this project's test suite before applying this pattern.

🤖 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/hooks/useUnsavedChanges.test.js` around lines 10 - 27, Replace the
locally defined handleBeforeUnload test logic in useUnsavedChanges.test.js with
a component that invokes the real useUnsavedChanges hook. Capture the
beforeunload listener registered through window.addEventListener, using the
existing `@testing-library/react` or equivalent test utilities if available, then
invoke that captured handler and retain the preventDefault and returnValue
assertions.
client/src/pages/Onboarding.jsx (1)

222-236: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded 10 duplicates MIN_DURATION from VoiceRecorder.jsx.

isValid here is computed against a literal 10 in two places, and the Clone button's disabled condition at Line 408 does the same. VoiceRecorder.jsx uses a MIN_DURATION constant for this threshold. If that constant changes, this file will silently disagree with it.

Export MIN_DURATION from a shared module and import it in both files.

🤖 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/pages/Onboarding.jsx` around lines 222 - 236, Replace the
hardcoded duration threshold in the onboarding validation logic and Clone button
disabled condition with a shared MIN_DURATION constant. Export MIN_DURATION from
a shared module, then import and use it in both Onboarding.jsx and
VoiceRecorder.jsx so all duration checks remain consistent when the threshold
changes.
client/src/components/VoiceForge.jsx (1)

8-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared draft-persistence hook. Both files independently implement the same three-part pattern: restore text from sessionStorage in a useState initializer, sync it back with an effect, and feed the trimmed result into useUnsavedChanges. Both also manually call sessionStorage.removeItem after clearing state, which is redundant with their own sync effect. A shared hook (e.g., useDraftText(key) returning [value, setValue, clearDraft] that internally wraps useUnsavedChanges) removes the duplication and the redundant manual cleanup calls.

  • client/src/components/VoiceForge.jsx#L8-L47: replace the inputText init/effect/useUnsavedChanges block with the shared hook, and drop the manual sessionStorage.removeItem at Lines 165-167 in favor of the hook's clearDraft.
  • client/src/components/TextToSpeech.jsx#L5-L92: replace the text init/effect/useUnsavedChanges block with the shared hook, and drop the manual sessionStorage.removeItem at Lines 140-142 in favor of the hook's clearDraft.
🤖 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/VoiceForge.jsx` around lines 8 - 47, Extract a shared
useDraftText hook that restores and synchronizes sessionStorage, invokes
useUnsavedChanges with the trimmed value, and returns the draft value, setter,
and clearDraft function. In client/src/components/VoiceForge.jsx lines 8-47,
replace the local inputText persistence block with this hook and use clearDraft
instead of the manual sessionStorage.removeItem at lines 165-167; apply the same
replacement in client/src/components/TextToSpeech.jsx lines 5-92 and use
clearDraft instead of its manual removal at lines 140-142.
🤖 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 @.dockerignore:
- Around line 16-18: Update the .dockerignore environment-file patterns to
exclude all .env.* files, including production and development variants, while
explicitly unignoring only the committed example templates. Ensure these rules
prevent environment files from reaching either the Dockerfile builder copy or
the copied server directory.

In `@client/src/components/VoiceForge.jsx`:
- Around line 8-47: In VoiceForge’s inputText initializer, cap the
sessionStorage draft to MAX_CHARS when restoring it, and update handleSpeak to
reject input whose length exceeds MAX_CHARS before submission. Preserve the
existing textarea limit and ensure the Speak & Save action cannot submit
overlong restored drafts.

In `@client/src/pages/Onboarding.jsx`:
- Around line 207-241: Update the clone-success flow in handleClone to clear
recording after the submitted clip is successfully sent, then remove the
successProfile condition from useUnsavedChanges so protection depends directly
on an existing recording blob or active clone operation. Keep successProfile for
its other UI/state purposes if still needed.

---

Outside diff comments:
In `@client/src/pages/Call.jsx`:
- Around line 208-225: Update the speak flow around setActiveText and
useTTS().speak() so the finally block no longer clears activeText after
generation returns. Clear activeText from the matching playback-ended handler,
and only clear it immediately when the matching generation request fails before
playback begins; ensure stale or unrelated playback events cannot clear newer
active text.

---

Nitpick comments:
In `@client/src/components/VoiceForge.jsx`:
- Around line 8-47: Extract a shared useDraftText hook that restores and
synchronizes sessionStorage, invokes useUnsavedChanges with the trimmed value,
and returns the draft value, setter, and clearDraft function. In
client/src/components/VoiceForge.jsx lines 8-47, replace the local inputText
persistence block with this hook and use clearDraft instead of the manual
sessionStorage.removeItem at lines 165-167; apply the same replacement in
client/src/components/TextToSpeech.jsx lines 5-92 and use clearDraft instead of
its manual removal at lines 140-142.

In `@client/src/hooks/useUnsavedChanges.test.js`:
- Around line 10-27: Replace the locally defined handleBeforeUnload test logic
in useUnsavedChanges.test.js with a component that invokes the real
useUnsavedChanges hook. Capture the beforeunload listener registered through
window.addEventListener, using the existing `@testing-library/react` or equivalent
test utilities if available, then invoke that captured handler and retain the
preventDefault and returnValue assertions.

In `@client/src/pages/Onboarding.jsx`:
- Around line 222-236: Replace the hardcoded duration threshold in the
onboarding validation logic and Clone button disabled condition with a shared
MIN_DURATION constant. Export MIN_DURATION from a shared module, then import and
use it in both Onboarding.jsx and VoiceRecorder.jsx so all duration checks
remain consistent when the threshold changes.
🪄 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: f900dd54-bf96-471d-a2a4-b23cd339d5dd

📥 Commits

Reviewing files that changed from the base of the PR and between 80429aa and d61443e.

📒 Files selected for processing (17)
  • .dockerignore
  • Dockerfile
  • README.md
  • client/src/App.jsx
  • client/src/components/SpeechHistory.jsx
  • client/src/components/TextToSpeech.jsx
  • client/src/components/VideoPreview.jsx
  • client/src/components/VoiceForge.jsx
  • client/src/components/VoiceRecorder.jsx
  • client/src/hooks/useSpeechHistory.js
  • client/src/hooks/useUnsavedChanges.js
  • client/src/hooks/useUnsavedChanges.test.js
  • client/src/main.jsx
  • client/src/pages/Call.jsx
  • client/src/pages/Onboarding.jsx
  • docker-compose.yml
  • server/index.js
💤 Files with no reviewable changes (1)
  • README.md

Comment thread .dockerignore
Comment on lines +16 to +18
.env
.env.local
.env.*.local

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Exclude all environment files from the Docker build context.

These patterns do not exclude .env.production or .env.development. Dockerfile Line 17 copies those files into the builder stage. A server-scoped file can also reach the runtime image through the copied server directory.

Ignore .env.* and explicitly unignore only committed example templates.

Proposed fix
 .env
-.env.local
-.env.*.local
+.env.*
+!.env.example
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.env
.env.local
.env.*.local
.env
.env.*
!.env.example
🤖 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 @.dockerignore around lines 16 - 18, Update the .dockerignore
environment-file patterns to exclude all .env.* files, including production and
development variants, while explicitly unignoring only the committed example
templates. Ensure these rules prevent environment files from reaching either the
Dockerfile builder copy or the copied server directory.

Comment on lines +8 to 47
import { VoiceQuickSettings } from "./VoiceQuickSettings.jsx";
import { FavoriteMessages } from "./FavoriteMessages.jsx";
import { QuickReplies } from "./QuickReplies.jsx";
import { SpeechHistory } from "./SpeechHistory.jsx";
import { ToastContainer, useToast } from "./useToast.jsx";
import { useSpeechHistory } from "../hooks/useSpeechHistory.js";
import { LanguageSelector } from "./LanguageSelector.jsx";
import { loadLanguage, persistLanguage } from "../utils/languages.js";
import useTTS from "../hooks/useTTS.js";
import { getActiveVoiceProfile } from "../hooks/useVoiceClone.js";
import { saveAudioBlob, getAudioBlob } from "../utils/db.js";
import { useUnsavedChanges } from "../hooks/useUnsavedChanges.js";

const MAX_CHARS = 300;
const DRAFT_KEY = "voiceforge_composer_draft_text";

export default function VoiceForge() {
const [inputText, setInputText] = useState("");
const [inputText, setInputText] = useState(() => {
try {
return sessionStorage.getItem(DRAFT_KEY) || "";
} catch {
return "";
}
});
const [isSpeaking, setIsSpeaking] = useState(false);
const [language, setLanguage] = useState(loadLanguage);
const [historyOpen, setHistoryOpen] = useState(false);
const drawerRef = useRef(null);
const historyToggleRef = useRef(null);

useEffect(() => {
try {
if (inputText.length > 0) {
sessionStorage.setItem(DRAFT_KEY, inputText);
} else {
sessionStorage.removeItem(DRAFT_KEY);
}
} catch {}
}, [inputText]);

useUnsavedChanges(inputText.trim().length > 0);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restored draft can exceed MAX_CHARS with no submission guard.

The initializer restores inputText from sessionStorage without slicing to MAX_CHARS. Unlike the textarea's onChange handler at Line 364, this restoration path bypasses the length cap. handleSpeak (Lines 154-168) and the Speak & Save button's disabled condition also do not reject text longer than MAX_CHARS. TextToSpeech.jsx enforces this same limit at both the initializer level and inside submit(); this file does not.

Cap the restored value at read time, and add a length guard to handleSpeak to match TextToSpeech.jsx's behavior.

🐛 Proposed fix
   const [inputText, setInputText] = useState(() => {
     try {
-      return sessionStorage.getItem(DRAFT_KEY) || "";
+      return (sessionStorage.getItem(DRAFT_KEY) || "").slice(0, MAX_CHARS);
     } catch {
       return "";
     }
   });
   const handleSpeak = useCallback(() => {
     const text = inputText.trim();
-    if (!text) {
+    if (!text || text.length > MAX_CHARS) {
       showToast("Please type a message first", "error");
       textareaRef.current?.focus();
       return;
     }
🤖 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/VoiceForge.jsx` around lines 8 - 47, In VoiceForge’s
inputText initializer, cap the sessionStorage draft to MAX_CHARS when restoring
it, and update handleSpeak to reject input whose length exceeds MAX_CHARS before
submission. Preserve the existing textarea limit and ensure the Speak & Save
action cannot submit overlong restored drafts.

Comment on lines +207 to +241
useUnsavedChanges((Boolean(recording?.blob) || isCloning) && !successProfile);

const handleRecordingReady = React.useCallback((blobArg, metaArg) => {
if (!blobArg) {
setRecording(null);
return;
}
let blob = blobArg instanceof Blob ? blobArg : blobArg?.blob;
if (!blob && !(blobArg instanceof Blob)) {
setRecording(null);
return;
}
let duration = 0;
let isValid = false;

if (typeof metaArg === "number") {
duration = metaArg;
isValid = duration >= 10;
} else if (metaArg && typeof metaArg === "object") {
duration = metaArg.duration || 0;
isValid = metaArg.isValid !== undefined ? metaArg.isValid : duration >= 10;
} else if (blobArg && typeof blobArg === "object" && !(blobArg instanceof Blob)) {
blob = blobArg.blob;
duration = blobArg.duration || 0;
isValid = blobArg.isValid !== undefined ? blobArg.isValid : duration >= 10;
}

if (!isValid && duration >= 10) {
isValid = true;
}

setRecording({ blob: blob || blobArg, duration, isValid });
}, []);

const recordingDuration = recording?.duration || 0;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Stale successProfile can suppress unload protection for a new recording.

successProfile is set once by handleClone and never reset. If a user returns to Step 1 after a successful clone and records a new reference clip, recording.blob becomes truthy again, but useUnsavedChanges still evaluates to false because !successProfile is permanently false. The new, genuinely unsaved recording loses beforeunload protection.

Clear recording after a successful clone (it has already been sent to the server) instead of gating on successProfile.

🐛 Proposed fix
   useUnsavedChanges((Boolean(recording?.blob) || isCloning) && !successProfile);
   async function handleClone() {
     if (!recording || !recording.isValid) return;
     const profile = await cloneVoice(recording.blob, voiceName);
     setSuccessProfile(profile);
+    setRecording(null);
   }

With this change, useUnsavedChanges can drop the && !successProfile term entirely, since recording is only truthy while there's an actual unsaved clip:

-  useUnsavedChanges((Boolean(recording?.blob) || isCloning) && !successProfile);
+  useUnsavedChanges(Boolean(recording?.blob) || isCloning);
🤖 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/pages/Onboarding.jsx` around lines 207 - 241, Update the
clone-success flow in handleClone to clear recording after the submitted clip is
successfully sent, then remove the successProfile condition from
useUnsavedChanges so protection depends directly on an existing recording blob
or active clone operation. Keep successProfile for its other UI/state purposes
if still needed.

@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.

8 issues found and verified against the latest diff

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/SpeechHistory.jsx">

<violation number="1" location="client/src/components/SpeechHistory.jsx:26">
P3: A tag literally named `All Tags` cannot be filtered: selecting it is indistinguishable from the all-tags sentinel. Represent the unfiltered state separately (for example, `null`) so this valid user tag remains selectable.</violation>

<violation number="2" location="client/src/components/SpeechHistory.jsx:55">
P3: Top Phrases miscounts phrases named `constructor` or `__proto__`, because arbitrary text is stored in a prototype-bearing object. Use a null-prototype dictionary so every phrase is counted as an own key.</violation>
</file>

<file name="client/src/pages/Onboarding.jsx">

<violation number="1" location="client/src/pages/Onboarding.jsx:207">
P2: Reloading or closing while microphone capture is in progress shows no warning, so the in-progress reference recording can be lost. Include recorder activity in this condition by exposing it from `VoiceRecorder` (or emitting a recording-start state).</violation>
</file>

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

<violation number="1" location="client/src/components/TextToSpeech.jsx:139">
P1: A failed speech request now clears the persisted draft and disables the unload safeguard. `handleSpeak` catches generation errors rather than rejecting, so retain the draft unless generation succeeds.</violation>
</file>

<file name="client/src/hooks/useUnsavedChanges.test.js">

<violation number="1" location="client/src/hooks/useUnsavedChanges.test.js:16">
P2: The second test doesn't actually test the hook: it re-implements a copy of handleBeforeUnload inline and asserts on that copy, so a regression in useUnsavedChanges (e.g. a broken effect or wrong listener registration) would still pass. Recommend mounting the hook (e.g. with @testing-library/react) or extracting/importing the real handler so the test verifies the production listener, including that the cleanup removes it when condition toggles false.</violation>
</file>

<file name="docker-compose.yml">

<violation number="1" location="docker-compose.yml:13">
P3: In this production compose service, MOCK_CHATTERBOX=true is a no-op: getIsMock() (server/utils/mock.js) only returns true when NODE_ENV !== 'production', and the README explicitly documents that MOCK_CHATTERBOX has no effect under NODE_ENV=production. As written, the file implies the service will run the offline test stub, but it will actually invoke the live Hugging Face engine. Drop the variable (or set it false) so the production config isn't misleading.</violation>
</file>

<file name="Dockerfile">

<violation number="1" location="Dockerfile:23">
P3: Runtime image retains all client production packages even though it copies only `client/dist`; `npm prune --omit=dev` prunes devDependencies, not dependencies of the client workspace. Reinstall/filter production dependencies for `@voiceforge/server` after the client build so the image does not ship unused browser libraries.</violation>
</file>

<file name="server/index.js">

<violation number="1" location="server/index.js:61">
P2: sendFile error path silently drops the error — request may hang without a response. When `client/dist/index.html` fails to serve (e.g., dist not built, file missing, permissions), the callback calls `next()` without arguments, so Express never reaches the 4-param error handler. The browser receives no response and times out. Pass the error with `next(err)` to let the error handler log it and return a proper 500.</violation>
</file>

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

Re-trigger cubic

}

await onSpeak(finalText, voice_settings_override);
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.

P1: A failed speech request now clears the persisted draft and disables the unload safeguard. handleSpeak catches generation errors rather than rejecting, so retain the draft unless generation succeeds.

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 139:

<comment>A failed speech request now clears the persisted draft and disables the unload safeguard. `handleSpeak` catches generation errors rather than rejecting, so retain the draft unless generation succeeds.</comment>

<file context>
@@ -101,25 +121,26 @@ if (estimatedDuration > 30) {
-  setText("");
-}
+    await onSpeak(finalText, voice_settings_override);
+    setText("");
+    try {
+      sessionStorage.removeItem(DRAFT_KEY);
</file context>

Comment thread client/src/components/VoiceRecorder.jsx
hasServerKey: false,
});

useUnsavedChanges((Boolean(recording?.blob) || isCloning) && !successProfile);

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: Reloading or closing while microphone capture is in progress shows no warning, so the in-progress reference recording can be lost. Include recorder activity in this condition by exposing it from VoiceRecorder (or emitting a recording-start state).

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

<comment>Reloading or closing while microphone capture is in progress shows no warning, so the in-progress reference recording can be lost. Include recorder activity in this condition by exposing it from `VoiceRecorder` (or emitting a recording-start state).</comment>

<file context>
@@ -203,6 +204,42 @@ export default function Onboarding({ onReady }) {
   const isCloning = status === "cloning";
   const [serverStatus, setServerStatus] = React.useState({ isMock: false, space: "" });
 
+  useUnsavedChanges((Boolean(recording?.blob) || isCloning) && !successProfile);
+
+  const handleRecordingReady = React.useCallback((blobArg, metaArg) => {
</file context>

returnValue: undefined,
};

const handleBeforeUnload = (evt) => {

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: The second test doesn't actually test the hook: it re-implements a copy of handleBeforeUnload inline and asserts on that copy, so a regression in useUnsavedChanges (e.g. a broken effect or wrong listener registration) would still pass. Recommend mounting the hook (e.g. with @testing-library/react) or extracting/importing the real handler so the test verifies the production listener, including that the cleanup removes it when condition toggles false.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At client/src/hooks/useUnsavedChanges.test.js, line 16:

<comment>The second test doesn't actually test the hook: it re-implements a copy of handleBeforeUnload inline and asserts on that copy, so a regression in useUnsavedChanges (e.g. a broken effect or wrong listener registration) would still pass. Recommend mounting the hook (e.g. with @testing-library/react) or extracting/importing the real handler so the test verifies the production listener, including that the cleanup removes it when condition toggles false.</comment>

<file context>
@@ -0,0 +1,27 @@
+      returnValue: undefined,
+    };
+
+    const handleBeforeUnload = (evt) => {
+      evt.preventDefault();
+      evt.returnValue = "";
</file context>

Comment thread server/index.js
if (req.method !== "GET" || req.path.startsWith("/api") || !req.headers.accept?.includes("text/html")) {
return next();
}
res.sendFile(path.join(staticDistPath, "index.html"), (err) => {

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: sendFile error path silently drops the error — request may hang without a response. When client/dist/index.html fails to serve (e.g., dist not built, file missing, permissions), the callback calls next() without arguments, so Express never reaches the 4-param error handler. The browser receives no response and times out. Pass the error with next(err) to let the error handler log it and return a proper 500.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At server/index.js, line 61:

<comment>sendFile error path silently drops the error — request may hang without a response. When `client/dist/index.html` fails to serve (e.g., dist not built, file missing, permissions), the callback calls `next()` without arguments, so Express never reaches the 4-param error handler. The browser receives no response and times out. Pass the error with `next(err)` to let the error handler log it and return a proper 500.</comment>

<file context>
@@ -50,6 +50,21 @@ app.get("/api/health", (_request, response) => {
+  if (req.method !== "GET" || req.path.startsWith("/api") || !req.headers.accept?.includes("text/html")) {
+    return next();
+  }
+  res.sendFile(path.join(staticDistPath, "index.html"), (err) => {
+    if (err) {
+      next();
</file context>

const source = sessionTranscript && sessionTranscript.length > 0 ? sessionTranscript : history;
const totalSentences = source.length;
const totalWords = source.reduce((acc, msg) => acc + (msg?.text ? msg.text.split(/\s+/).length : 0), 0);
const counts = {};

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.

P3: Top Phrases miscounts phrases named constructor or __proto__, because arbitrary text is stored in a prototype-bearing object. Use a null-prototype dictionary so every phrase is counted as an own key.

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

<comment>Top Phrases miscounts phrases named `constructor` or `__proto__`, because arbitrary text is stored in a prototype-bearing object. Use a null-prototype dictionary so every phrase is counted as an own key.</comment>

<file context>
@@ -13,15 +14,58 @@ export function SpeechHistory({history,
+    const source = sessionTranscript && sessionTranscript.length > 0 ? sessionTranscript : history;
+    const totalSentences = source.length;
+    const totalWords = source.reduce((acc, msg) => acc + (msg?.text ? msg.text.split(/\s+/).length : 0), 0);
+    const counts = {};
+    source.forEach((msg) => {
+      if (msg?.text) {
</file context>
Suggested change
const counts = {};
const counts = Object.create(null);

const [tab, setTab] = useState("all");
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search, 300);
const [selectedTag, setSelectedTag] = useState("All Tags");

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.

P3: A tag literally named All Tags cannot be filtered: selecting it is indistinguishable from the all-tags sentinel. Represent the unfiltered state separately (for example, null) so this valid user tag remains selectable.

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

<comment>A tag literally named `All Tags` cannot be filtered: selecting it is indistinguishable from the all-tags sentinel. Represent the unfiltered state separately (for example, `null`) so this valid user tag remains selectable.</comment>

<file context>
@@ -13,15 +14,58 @@ export function SpeechHistory({history,
   const [tab, setTab] = useState("all");
   const [search, setSearch] = useState("");
   const debouncedSearch = useDebounce(search, 300);
+  const [selectedTag, setSelectedTag] = useState("All Tags");
+  const [analyticsOpen, setAnalyticsOpen] = useState(false);
 
</file context>

Comment thread client/src/components/TextToSpeech.jsx
Comment thread docker-compose.yml
environment:
- NODE_ENV=production
- PORT=3001
- MOCK_CHATTERBOX=true

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.

P3: In this production compose service, MOCK_CHATTERBOX=true is a no-op: getIsMock() (server/utils/mock.js) only returns true when NODE_ENV !== 'production', and the README explicitly documents that MOCK_CHATTERBOX has no effect under NODE_ENV=production. As written, the file implies the service will run the offline test stub, but it will actually invoke the live Hugging Face engine. Drop the variable (or set it false) so the production config isn't misleading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 13:

<comment>In this production compose service, MOCK_CHATTERBOX=true is a no-op: getIsMock() (server/utils/mock.js) only returns true when NODE_ENV !== 'production', and the README explicitly documents that MOCK_CHATTERBOX has no effect under NODE_ENV=production. As written, the file implies the service will run the offline test stub, but it will actually invoke the live Hugging Face engine. Drop the variable (or set it false) so the production config isn't misleading.</comment>

<file context>
@@ -0,0 +1,21 @@
+    environment:
+      - NODE_ENV=production
+      - PORT=3001
+      - MOCK_CHATTERBOX=true
+      - CLIENT_URL=http://localhost:3001
+    restart: unless-stopped
</file context>

Comment thread Dockerfile
RUN npm run build --workspace client

# Prune devDependencies to keep runtime node_modules lightweight
RUN npm prune --omit=dev

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.

P3: Runtime image retains all client production packages even though it copies only client/dist; npm prune --omit=dev prunes devDependencies, not dependencies of the client workspace. Reinstall/filter production dependencies for @voiceforge/server after the client build so the image does not ship unused browser libraries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Dockerfile, line 23:

<comment>Runtime image retains all client production packages even though it copies only `client/dist`; `npm prune --omit=dev` prunes devDependencies, not dependencies of the client workspace. Reinstall/filter production dependencies for `@voiceforge/server` after the client build so the image does not ship unused browser libraries.</comment>

<file context>
@@ -0,0 +1,60 @@
+RUN npm run build --workspace client
+
+# Prune devDependencies to keep runtime node_modules lightweight
+RUN npm prune --omit=dev
+
+
</file context>

@github-actions github-actions Bot removed the large PR label Aug 12, 2026
@Itzzavdheshh
Itzzavdheshh merged commit 6fa1de8 into itzzavdhesh:main Aug 12, 2026
3 of 8 checks passed
@github-actions

Copy link
Copy Markdown

❌ Merge Policy Violation

Caution

Unauthorized Merge — Pull request #1222 was merged by @Itzzavdheshh (mentor) without any review on record.

VoiceForge guidelines require contributors/mentors to submit at least one review (approval, comment, or changes requested) before merging a pull request to ensure code quality and point-tracking integrity.

📊 Violation Summary

Parameter Details
Merged By @Itzzavdheshh (mentor)
Review Count 0 reviews on record
PR Author @Myparadox-creator
PR Number #1222

⚠️ Action Required (@itzzavdhesh):

  • Please review the merged code for quality and scope.
  • Consider reverting the merge if it was done inappropriately.
    • Ensure your repository ruleset or branch protection rules are active.

🤖 VoiceForge Automation

@github-actions

Copy link
Copy Markdown

🎊 PR Merged Successfully

Hey @Myparadox-creator! 👋 Congratulations and thank you for your contribution to VoiceForge!

Note

🔗 Linked issue(s): #1172 · ✅ Marked as merged and complete

Maintainers may still handle final cleanup, release notes, or follow-up tracking after the merge.


🤖 VoiceForge Automation · Updates automatically on edits

@itzzavdhesh itzzavdhesh added gssoc:approved gssoc:approved type:accessibility type SSoC26 SSOC Hard hard ELUSOC ELUSOC VETERAN VETERAN type:bug type:bug type:feature type:feature type:performance type:performance type:refactor type:refactor quality:clean quality:clean labels Aug 13, 2026
@Itzzavdheshh Itzzavdheshh added the level:advanced level:advanced label Aug 13, 2026
@itzzavdhesh itzzavdhesh added mentor:Anushreebasics GSSoC: Mentor-@Anushreebasics mentor:sabeenaviklar GSSoC: Mentor-@sabeenaviklar mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain mentor:Nitya-003 Applied by mentor automation gssoc26 gssoc26 labels Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working client completed dco-missing documentation Improvements or additions to documentation ELUSOC ELUSOC enhancement New feature or request gssoc:approved gssoc:approved gssoc26 gssoc26 Hard hard level:advanced level:advanced mentor:Anushreebasics GSSoC: Mentor-@Anushreebasics mentor:itsdakshjain GSSoC: Mentor-@itsdakshjain mentor:Nitya-003 Applied by mentor automation mentor:sabeenaviklar GSSoC: Mentor-@sabeenaviklar merged needs-title pr-merged quality:clean quality:clean server size/l SSoC26 SSOC type:accessibility type type:bug type:bug type:feature type:feature type:performance type:performance type:refactor type:refactor VETERAN VETERAN

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Data Loss on Accidental Tab Closure During Voice Cloning

3 participants