Feature/voice records#287
Conversation
Updated the README to include images for various workflows and removed placeholder screenshots.
📝 WalkthroughWalkthroughThe 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. ChangesVoice report workflow
Patient appointment display corrections
Frontend documentation
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
frontend-next/README.mdfrontend-next/src/components/patient/AppointmentReivewForm.jsfrontend-next/src/components/patient/PatientAppointmentCard.jsfrontend-next/src/components/voice-reports/VoiceReportUploader.js
| 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. |
There was a problem hiding this comment.
🔒 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.
| ### 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" />| |
There was a problem hiding this comment.
📐 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.”
| ```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. |
There was a problem hiding this comment.
🎯 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.
| ```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.
| // 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); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
🩺 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.
| // 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.
| 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, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
🩺 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.
| 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.
| {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", | ||
| }} | ||
| /> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
📐 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.
| {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.
| <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> |
There was a problem hiding this comment.
📐 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.
| <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.
| <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> |
There was a problem hiding this comment.
📐 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.
| <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.
Summary by CodeRabbit