Skip to content

Feature/voice records#287

Merged
Abdalrahman-gits merged 3 commits into
devfrom
feature/voice-records
Jul 22, 2026
Merged

Feature/voice records#287
Abdalrahman-gits merged 3 commits into
devfrom
feature/voice-records

Conversation

@Abdalrahman-gits

@Abdalrahman-gits Abdalrahman-gits commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added voice report recording, playback, re-recording, file selection, and upload options.
    • Added clear recording status, duration, file size, and upload feedback.
  • Bug Fixes
    • Corrected doctor names in appointment reviews.
    • Improved specialty badges to display the correct specialty names.
  • Documentation
    • Replaced the default project README with comprehensive product, setup, workflow, architecture, and deployment documentation.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces the frontend README, corrects patient appointment doctor and specialty rendering, and expands voice reports with recording, playback preview, file uploads, mode-based UI, and media resource cleanup.

Changes

Voice report workflow

Layer / File(s) Summary
Recording, preview, and upload flow
frontend-next/src/components/voice-reports/VoiceReportUploader.js
Adds microphone recording, playback controls, file and recorded-audio uploads, mode transitions, toast handling, and cleanup for timers, media tracks, and preview URLs.

Patient appointment display corrections

Layer / File(s) Summary
Appointment doctor and specialty rendering
frontend-next/src/components/patient/AppointmentReivewForm.js, frontend-next/src/components/patient/PatientAppointmentCard.js
Uses the updated doctor name and specialty object fields for appointment headings and badges.

Frontend documentation

Layer / File(s) Summary
MedAI Web Client documentation
frontend-next/README.md
Documents the application architecture, workflows, project structure, setup, scripts, Docker deployment, and design system.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant VoiceReportUploader
  participant MediaRecorder
  participant createVoice
  User->>VoiceReportUploader: Start microphone recording
  VoiceReportUploader->>MediaRecorder: Capture audio chunks
  User->>VoiceReportUploader: Stop recording
  MediaRecorder-->>VoiceReportUploader: Return recorded blob
  User->>VoiceReportUploader: Upload recording or selected file
  VoiceReportUploader->>createVoice: Submit audio file
  createVoice-->>VoiceReportUploader: Return upload success or error
Loading

Possibly related PRs

Suggested reviewers: mena-rizkalla

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the main change, especially the new voice recording/upload workflow, though it is a bit broad.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/voice-records

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

🤖 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 `@frontend-next/README.md`:
- Around line 139-143: Correct the authentication screenshot table heading in
the README by changing “Appoval Pending” to “Approval Pending.”
- Around line 88-92: Update the authentication documentation near the automatic
token rotation description to match the implementation in apiFetchClient:
describe cookie-based authentication using credentials: "include" rather than
JWT Bearer header injection, while preserving the documented refresh, retry, and
middleware behavior.
- Around line 287-295: Update the Docker build command in the README to pass
values for the Dockerfile’s NEXT_PUBLIC_API_BASE_URL and MOBILE_APP_URL build
arguments. Keep the existing image tag and run command unchanged, and show both
required build-time configuration values so the generated client is correctly
configured.

In `@frontend-next/src/components/voice-reports/VoiceReportUploader.js`:
- Around line 141-152: Update handleRecordingComplete, the MediaRecorder onstop
completion path, to clear the interval stored in timerRef before transitioning
to recorded mode. Preserve handleStopRecording’s existing cleanup while ensuring
automatic stops from ended streams also stop recordingDuration updates.
- Around line 373-379: Add an accessible name to the icon-only remove-file
button in VoiceReportUploader, identified by its onClick={handleRemoveFile}
handler and X icon. Add a concise aria-label describing the remove-file action
while preserving the existing button behavior and styling.
- Around line 300-323: Add an accessible name to the icon-only button controlled
by handleTogglePlayback, using a state-dependent aria-label that identifies the
action, such as play when paused and pause when playing; keep the existing icon
and playback behavior unchanged.
- Around line 325-332: Update the status label in the VoiceReportUploader
recording summary to use “Recorded” when mode === "recorded" and retain
“Recording” while capture is active. Keep the existing duration and file-size
rendering unchanged.
- Around line 60-69: Update the unmount cleanup returned by the useEffect in
VoiceReportUploader so it reads timerRef.current and streamRef.current inside
the cleanup closure rather than capturing their mount-time values in locals.
Preserve the existing timer clearing, stream track stopping, and latestUrlRef
URL revocation behavior.
- Around line 113-130: Update handleStartRecording to wrap the getUserMedia and
recorder setup flow in try/catch, preserving the existing recording
initialization on success. In the catch path, stop or clear any partially
initialized recording resources and surface a user-visible error through the
component’s existing toast or error-feedback mechanism so permission, device,
and insecure-context failures do not leave the UI silent.
🪄 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

Run ID: cb737d7a-566e-48d9-a943-028db1b32cd1

📥 Commits

Reviewing files that changed from the base of the PR and between e69cd3f and 6e91a7b.

📒 Files selected for processing (4)
  • frontend-next/README.md
  • frontend-next/src/components/patient/AppointmentReivewForm.js
  • frontend-next/src/components/patient/PatientAppointmentCard.js
  • frontend-next/src/components/voice-reports/VoiceReportUploader.js

Comment thread frontend-next/README.md
Comment on lines +88 to +92
The client implements automatic token rotation via a custom fetch interceptor:

- **Automatic Header Injection**: All API requests include the JWT Bearer token from cookies.
- **Silent Refresh**: On 401 responses, the interceptor pauses the request, calls `/auth/refresh-token`, saves the new credentials, and retries seamlessly.
- **Middleware Protection**: The Next.js middleware (`proxy.js`) runs before every request, refreshing tokens and redirecting unauthenticated or unauthorized users.

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 | 🟡 Minor | ⚡ Quick win

Document the actual authentication transport.

frontend-next/src/lib/api/apiFetchClient.js sends cookies via credentials: "include" but does not inject a Bearer Authorization header. Either update this section to describe cookie-based authentication accurately, or change the client implementation if Bearer headers are the intended contract.

🤖 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 `@frontend-next/README.md` around lines 88 - 92, Update the authentication
documentation near the automatic token rotation description to match the
implementation in apiFetchClient: describe cookie-based authentication using
credentials: "include" rather than JWT Bearer header injection, while preserving
the documented refresh, retry, and middleware behavior.

Comment thread frontend-next/README.md
Comment on lines +139 to +143
### 1. Authentication & Onboarding

| Signup | Appoval Pending |
| :------------: | :------------: |
| <img width="1920" height="1080" alt="auth" src="https://github.com/user-attachments/assets/32b62c32-bc9a-43af-ad60-9ea95eb0c229" /> | <img width="1920" height="1080" alt="needs-approval" src="https://github.com/user-attachments/assets/53771a43-1ae2-4d52-969e-e257a71cca4a" />|

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the screenshot label typo.

Change Appoval Pending to Approval Pending.

🤖 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 `@frontend-next/README.md` around lines 139 - 143, Correct the authentication
screenshot table heading in the README by changing “Appoval Pending” to
“Approval Pending.”

Comment thread frontend-next/README.md
Comment on lines +287 to +295
```bash
# Build the Docker image
docker build -f docker/Dockerfile -t medai-frontend .

# Run the container
docker run -p 3000:3000 medai-frontend
```

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
The Dockerfile uses a multi-stage build with `node:24-alpine`, runs `npm ci`, produces a standalone output, and serves the app with the Next.js server.

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

Pass the required environment values to the Docker build.

frontend-next/docker/Dockerfile declares NEXT_PUBLIC_API_BASE_URL and MOBILE_APP_URL as build arguments, but the documented command supplies neither. Following these instructions builds a client with missing build-time configuration, causing API requests and mobile links to be incorrect.

Proposed documentation fix
- docker build -f docker/Dockerfile -t medai-frontend .
+ docker build \
+   --build-arg NEXT_PUBLIC_API_BASE_URL="$NEXT_PUBLIC_API_BASE_URL" \
+   --build-arg MOBILE_APP_URL="$MOBILE_APP_URL" \
+   -f docker/Dockerfile \
+   -t medai-frontend .
📝 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
```bash
# Build the Docker image
docker build -f docker/Dockerfile -t medai-frontend .
# Run the container
docker run -p 3000:3000 medai-frontend
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
The Dockerfile uses a multi-stage build with `node:24-alpine`, runs `npm ci`, produces a standalone output, and serves the app with the Next.js server.
🤖 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 `@frontend-next/README.md` around lines 287 - 295, Update the Docker build
command in the README to pass values for the Dockerfile’s
NEXT_PUBLIC_API_BASE_URL and MOBILE_APP_URL build arguments. Keep the existing
image tag and run command unchanged, and show both required build-time
configuration values so the generated client is correctly configured.

Comment on lines +60 to +69
// Cleanup on unmount — capture ref values in locals
useEffect(() => {
const timer = timerRef.current;
const stream = streamRef.current;
return () => {
if (timer) clearInterval(timer);
if (stream) stream.getTracks().forEach((t) => t.stop());
if (latestUrlRef.current) URL.revokeObjectURL(latestUrlRef.current);
};
}, []);

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unmount cleanup never actually clears the timer/stream — stale refs captured at mount.

timerRef.current and streamRef.current are read into timer/stream locals when this effect runs on mount (deps []), at which point both are still null (recording hasn't started yet). The returned cleanup closure only ever sees those mount-time null values, so clearInterval(timer) and stream.getTracks().forEach(...) never run for real. Only the latestUrlRef.current check works, because it dereferences the ref at call time instead of capturing it into a local. Net effect: if a user starts recording and navigates away/unmounts mid-recording, the mic stream stays open and the duration timer keeps ticking forever — the opposite of what this effect is meant to do.

🐛 Proposed fix — read refs inside the cleanup closure, not before it
-  // Cleanup on unmount — capture ref values in locals
-  useEffect(() => {
-    const timer = timerRef.current;
-    const stream = streamRef.current;
-    return () => {
-      if (timer) clearInterval(timer);
-      if (stream) stream.getTracks().forEach((t) => t.stop());
-      if (latestUrlRef.current) URL.revokeObjectURL(latestUrlRef.current);
-    };
-  }, []);
+  // Cleanup on unmount — read refs at teardown time so an in-progress
+  // recording is actually released.
+  useEffect(() => {
+    return () => {
+      if (timerRef.current) clearInterval(timerRef.current);
+      if (streamRef.current)
+        streamRef.current.getTracks().forEach((t) => t.stop());
+      if (latestUrlRef.current) URL.revokeObjectURL(latestUrlRef.current);
+    };
+  }, []);
📝 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
// Cleanup on unmount — capture ref values in locals
useEffect(() => {
const timer = timerRef.current;
const stream = streamRef.current;
return () => {
if (timer) clearInterval(timer);
if (stream) stream.getTracks().forEach((t) => t.stop());
if (latestUrlRef.current) URL.revokeObjectURL(latestUrlRef.current);
};
}, []);
// Cleanup on unmount — read refs at teardown time so an in-progress
// recording is actually released.
useEffect(() => {
return () => {
if (timerRef.current) clearInterval(timerRef.current);
if (streamRef.current)
streamRef.current.getTracks().forEach((t) => t.stop());
if (latestUrlRef.current) URL.revokeObjectURL(latestUrlRef.current);
};
}, []);
🤖 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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 60 - 69, Update the unmount cleanup returned by the useEffect in
VoiceReportUploader so it reads timerRef.current and streamRef.current inside
the cleanup closure rather than capturing their mount-time values in locals.
Preserve the existing timer clearing, stream track stopping, and latestUrlRef
URL revocation behavior.

Comment on lines +113 to +130
async function handleStartRecording() {
streamRef.current = await navigator.mediaDevices.getUserMedia({
audio: true,
});
mediaRecorderRef.current = new MediaRecorder(streamRef.current);

mediaRecorderRef.current.ondataavailable = (e) => {
audioChunksRef.current.push(e.data);
};
mediaRecorderRef.current.onstop = handleRecordingComplete;

mediaRecorderRef.current.start();
setMode("recording");
timerRef.current = setInterval(
() => setRecordingDuration((d) => d + 1),
1000,
);
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No error handling around getUserMedia — permission denial/no-device leaves the UI silently stuck.

await navigator.mediaDevices.getUserMedia(...) isn't wrapped in try/catch. Permission denial, no microphone, or an insecure context all cause this promise to reject; since handleStartRecording is an async function invoked from an onClick with no .catch, the rejection becomes an unhandled promise rejection and the function simply exits before setMode("recording"). The user clicks "Record", nothing happens, and there is no toast or visual feedback at all.

🐛 Proposed fix — catch getUserMedia failures and surface them
 async function handleStartRecording() {
-    streamRef.current = await navigator.mediaDevices.getUserMedia({
-      audio: true,
-    });
-    mediaRecorderRef.current = new MediaRecorder(streamRef.current);
+    try {
+      streamRef.current = await navigator.mediaDevices.getUserMedia({
+        audio: true,
+      });
+    } catch {
+      toast.error("Microphone access was denied or unavailable.");
+      return;
+    }
+
+    mediaRecorderRef.current = new MediaRecorder(streamRef.current);
+    mediaRecorderRef.current.onerror = () => {
+      toast.error("Recording failed unexpectedly.");
+    };
📝 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
async function handleStartRecording() {
streamRef.current = await navigator.mediaDevices.getUserMedia({
audio: true,
});
mediaRecorderRef.current = new MediaRecorder(streamRef.current);
mediaRecorderRef.current.ondataavailable = (e) => {
audioChunksRef.current.push(e.data);
};
mediaRecorderRef.current.onstop = handleRecordingComplete;
mediaRecorderRef.current.start();
setMode("recording");
timerRef.current = setInterval(
() => setRecordingDuration((d) => d + 1),
1000,
);
}
async function handleStartRecording() {
try {
streamRef.current = await navigator.mediaDevices.getUserMedia({
audio: true,
});
} catch {
toast.error("Microphone access was denied or unavailable.");
return;
}
mediaRecorderRef.current = new MediaRecorder(streamRef.current);
mediaRecorderRef.current.onerror = () => {
toast.error("Recording failed unexpectedly.");
};
mediaRecorderRef.current.ondataavailable = (e) => {
audioChunksRef.current.push(e.data);
};
mediaRecorderRef.current.onstop = handleRecordingComplete;
mediaRecorderRef.current.start();
setMode("recording");
timerRef.current = setInterval(
() => setRecordingDuration((d) => d + 1),
1000,
);
}
🧰 Tools
🪛 ast-grep (0.44.1)

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

(usestate-direct-usage)


[error] 125-128: React's useState should not be directly called
Context: setInterval(
() => setRecordingDuration((d) => d + 1),
1000,
)
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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 113 - 130, Update handleStartRecording to wrap the getUserMedia and
recorder setup flow in try/catch, preserving the existing recording
initialization on success. In the catch path, stop or clear any partially
initialized recording resources and surface a user-visible error through the
component’s existing toast or error-feedback mechanism so permission, device,
and insecure-context failures do not leave the UI silent.

Comment on lines +141 to +152
function handleRecordingComplete() {
const mimeType = audioChunksRef.current[0]?.type || "audio/webm";
const blob = new Blob(audioChunksRef.current, { type: mimeType });
const url = URL.createObjectURL(blob);
setRecordedBlob(blob);
setPreviewUrl(url);
setMode("recorded");
audioChunksRef.current = [];

streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Recording timer isn't cleared when onstop fires without a manual stop.

timerRef is only cleared inside handleStopRecording (bound to the "Stop" button). Per the MediaRecorder spec, the stop event also fires when the underlying MediaStream ends on its own (permission revoked mid-recording, device disconnected, track ended), bypassing handleStopRecording entirely. In that case handleRecordingComplete runs, transitions to mode === "recorded", but the 1s interval keeps incrementing recordingDuration indefinitely (compounded by the unmount-cleanup bug above, this interval may never be cleared for the rest of the session).

🐛 Proposed fix — clear the timer in the onstop handler too
 function handleRecordingComplete() {
+    if (timerRef.current) {
+      clearInterval(timerRef.current);
+      timerRef.current = null;
+    }
     const mimeType = audioChunksRef.current[0]?.type || "audio/webm";
     const blob = new Blob(audioChunksRef.current, { type: mimeType });
📝 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
function handleRecordingComplete() {
const mimeType = audioChunksRef.current[0]?.type || "audio/webm";
const blob = new Blob(audioChunksRef.current, { type: mimeType });
const url = URL.createObjectURL(blob);
setRecordedBlob(blob);
setPreviewUrl(url);
setMode("recorded");
audioChunksRef.current = [];
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
function handleRecordingComplete() {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
const mimeType = audioChunksRef.current[0]?.type || "audio/webm";
const blob = new Blob(audioChunksRef.current, { type: mimeType });
const url = URL.createObjectURL(blob);
setRecordedBlob(blob);
setPreviewUrl(url);
setMode("recorded");
audioChunksRef.current = [];
streamRef.current?.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
🧰 Tools
🪛 ast-grep (0.44.1)

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

(usestate-direct-usage)


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

(usestate-direct-usage)


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

(usestate-direct-usage)


[warning] 144-144: Avoid using the initial state variable in setState
Context: setRecordedBlob(blob)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[warning] 145-145: Avoid using the initial state variable in setState
Context: setPreviewUrl(url)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 141 - 152, Update handleRecordingComplete, the MediaRecorder onstop
completion path, to clear the interval stored in timerRef before transitioning
to recorded mode. Preserve handleStopRecording’s existing cleanup while ensuring
automatic stops from ended streams also stop recordingDuration updates.

Comment on lines +300 to +323
{previewUrl && (
<>
<button
type="button"
onClick={handleTogglePlayback}
className="bg-primary flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition-colors hover:opacity-90"
>
{isPlaying ? <Pause size={16} /> : <Play size={16} />}
</button>

<audio
ref={audioRef}
src={previewUrl}
onEnded={() => setIsPlaying(false)}
style={{
position: "absolute",
width: 0,
height: 0,
opacity: 0,
pointerEvents: "none",
}}
/>
</>
)}

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Icon-only play/pause button has no accessible name.

The toggle button at Lines 302-308 renders only <Play>/<Pause> with no text or aria-label. Screen-reader users get no indication of what the control does or its current state.

♿ Proposed fix
 <button
   type="button"
   onClick={handleTogglePlayback}
+  aria-label={isPlaying ? "Pause recording" : "Play recording"}
   className="bg-primary flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition-colors hover:opacity-90"
 >
📝 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
{previewUrl && (
<>
<button
type="button"
onClick={handleTogglePlayback}
className="bg-primary flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition-colors hover:opacity-90"
>
{isPlaying ? <Pause size={16} /> : <Play size={16} />}
</button>
<audio
ref={audioRef}
src={previewUrl}
onEnded={() => setIsPlaying(false)}
style={{
position: "absolute",
width: 0,
height: 0,
opacity: 0,
pointerEvents: "none",
}}
/>
</>
)}
{previewUrl && (
<>
<button
type="button"
onClick={handleTogglePlayback}
aria-label={isPlaying ? "Pause recording" : "Play recording"}
className="bg-primary flex size-9 cursor-pointer items-center justify-center rounded-full text-white transition-colors hover:opacity-90"
>
{isPlaying ? <Pause size={16} /> : <Play size={16} />}
</button>
<audio
ref={audioRef}
src={previewUrl}
onEnded={() => setIsPlaying(false)}
style={{
position: "absolute",
width: 0,
height: 0,
opacity: 0,
pointerEvents: "none",
}}
/>
</>
)}
🤖 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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 300 - 323, Add an accessible name to the icon-only button controlled by
handleTogglePlayback, using a state-dependent aria-label that identifies the
action, such as play when paused and pause when playing; keep the existing icon
and playback behavior unchanged.

Comment on lines +325 to +332
<div>
<p className="text-text-base text-sm font-medium">
Recording — {formatDuration(recordingDuration)}
</p>
<p className="text-text-subtle text-xs">
{recordedBlob ? formatSize(recordedBlob.size) : ""}
</p>
</div>

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Label says "Recording" even after the recording has finished.

Once mode === "recorded", the audio is already captured and stopped, so Recording — {formatDuration(recordingDuration)} is misleading; "Recorded" reads clearer for a completed take.

-              Recording — {formatDuration(recordingDuration)}
+              Recorded — {formatDuration(recordingDuration)}
📝 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
<div>
<p className="text-text-base text-sm font-medium">
Recording {formatDuration(recordingDuration)}
</p>
<p className="text-text-subtle text-xs">
{recordedBlob ? formatSize(recordedBlob.size) : ""}
</p>
</div>
<div>
<p className="text-text-base text-sm font-medium">
Recorded {formatDuration(recordingDuration)}
</p>
<p className="text-text-subtle text-xs">
{recordedBlob ? formatSize(recordedBlob.size) : ""}
</p>
</div>
🤖 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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 325 - 332, Update the status label in the VoiceReportUploader recording
summary to use “Recorded” when mode === "recorded" and retain “Recording” while
capture is active. Keep the existing duration and file-size rendering unchanged.

Comment on lines +373 to +379
<button
type="button"
onClick={handleRemoveFile}
className="text-text-muted flex size-9 cursor-pointer items-center justify-center rounded-lg transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
>
<X size={16} />
</button>

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Icon-only remove-file button has no accessible name.

Same accessibility gap as the playback toggle — only an X icon with no text or aria-label.

♿ Proposed fix
 <button
   type="button"
   onClick={handleRemoveFile}
+  aria-label="Remove selected file"
   className="text-text-muted flex size-9 cursor-pointer items-center justify-center rounded-lg transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
 >
📝 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
<button
type="button"
onClick={handleRemoveFile}
className="text-text-muted flex size-9 cursor-pointer items-center justify-center rounded-lg transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
>
<X size={16} />
</button>
<button
type="button"
onClick={handleRemoveFile}
aria-label="Remove selected file"
className="text-text-muted flex size-9 cursor-pointer items-center justify-center rounded-lg transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20"
>
<X size={16} />
</button>
🤖 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 `@frontend-next/src/components/voice-reports/VoiceReportUploader.js` around
lines 373 - 379, Add an accessible name to the icon-only remove-file button in
VoiceReportUploader, identified by its onClick={handleRemoveFile} handler and X
icon. Add a concise aria-label describing the remove-file action while
preserving the existing button behavior and styling.

@Abdalrahman-gits
Abdalrahman-gits merged commit 0c65e23 into dev Jul 22, 2026
3 checks passed
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.

2 participants