Skip to content

feat(audio): add rename-title endpoint and inline edit UI#24

Merged
Krasimir-Hristov merged 2 commits into
mainfrom
feat/audio-rename-title
May 16, 2026
Merged

feat(audio): add rename-title endpoint and inline edit UI#24
Krasimir-Hristov merged 2 commits into
mainfrom
feat/audio-rename-title

Conversation

@Krasimir-Hristov

@Krasimir-Hristov Krasimir-Hristov commented May 16, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Audio entries can now be renamed inline with optimistic real-time updates
    • Added temporary audio file serving with secure token-based access and expiration support
    • Enhanced delete confirmation with dedicated dialog interface
    • Added back navigation to the audio dashboard

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Krasimir-Hristov has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 32 minutes and 10 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: de7c1171-bacd-4d8f-8f5f-f087468ecadf

📥 Commits

Reviewing files that changed from the base of the PR and between 33d2d6b and 583a0a0.

📒 Files selected for processing (7)
  • backend/app/features/audio/rename.py
  • backend/app/features/audio/router.py
  • backend/app/features/audio/service.py
  • backend/app/features/audio/tool.py
  • frontend/features/audio/components/AudioCard.tsx
  • frontend/features/audio/hooks/useAudio.ts
  • frontend/lib/api.ts

Walkthrough

This PR adds audio entry rename/edit functionality and temporary audio storage. The backend introduces a filesystem-based temp audio store with UUID tokens, a PATCH endpoint to rename audio entries with database updates and fresh signed URLs, and modifies TTS generation to forward raw text. The frontend exposes API helpers and React Query hooks for mutations and adds an interactive edit-in-place UI component with optimistic updates, plus a back-navigation link on the audio page.

Changes

Audio Rename and Temp Storage

Layer / File(s) Summary
Temporary audio storage and serving
backend/app/features/audio/service.py, backend/app/features/audio/router.py
Adds filesystem-backed temp audio store with UUID tokens and 1-hour TTL, including store_temp_audio, retrieve_temp_audio (with expiry check), and delete_temp_audio functions; GET /audio/temp/{token} endpoint serves stored MP3 bytes or returns HTTP 404; removes PII-masking from generate_tts text payload.
Audio entry rename with database update
backend/app/features/audio/schemas.py, backend/app/features/audio/service.py, backend/app/features/audio/router.py
AudioEntryPatch schema validates rename request body; rename_audio_entry service function updates entry title in database, re-fetches the row, generates a fresh signed Storage URL, and returns updated AudioEntryOut; PATCH /audio/{entry_id} endpoint wires the rename flow and returns HTTP 404 if entry is not found.
Frontend API and React Query mutations
frontend/lib/api.ts, frontend/features/audio/hooks/useAudio.ts
renameAudioEntry API helper calls PATCH /api/v1/audio/{id} with JSON title body; useRenameAudio hook performs mutation and invalidates AUDIO_ENTRIES_KEY cache on success to refresh the list.
Audio card edit UI and navigation
frontend/app/(dashboard)/audio/page.tsx, frontend/features/audio/components/AudioCard.tsx
AudioCard refactored to support edit-in-place title editing with useState, useOptimistic, and useTransition; adds editable input with save/cancel icon buttons, keyboard handling (Enter/Escape), and delete confirmation dialog with pending states; page header adds back-navigation link to /chat with ArrowLeft icon.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant Service
  participant Filesystem
  Client->>Router: GET /audio/temp/{token}
  Router->>Service: retrieve_temp_audio(token)
  Service->>Filesystem: check file mtime for expiry
  alt File exists and not expired
    Filesystem-->>Service: return audio bytes
    Service-->>Router: audio bytes
    Router-->>Client: audio/mpeg response
  else File missing or expired
    Filesystem-->>Service: None
    Service-->>Router: None
    Router-->>Client: HTTP 404 (Audio not found or expired)
  end
Loading
sequenceDiagram
  participant Client
  participant Router
  participant Service
  participant Database
  participant Storage
  Client->>Router: PATCH /audio/{entry_id} with AudioEntryPatch
  Router->>Service: rename_audio_entry(entry_id, user_id, title)
  Service->>Database: update entry title
  Service->>Database: fetch updated entry
  Service->>Storage: sign URL for entry filename
  Storage-->>Service: signed URL
  Service-->>Router: AudioEntryOut with new title and signed URL
  Router-->>Client: AudioEntryOut (HTTP 200)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Krasimir-Hristov/AXON#22: Adds TTS generation and audio upload; this PR adjusts TTS text handling and extends audio service features (temp storage, rename) that build on that foundation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main changes: adding a rename-title endpoint and inline edit UI for audio features.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/audio-rename-title

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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 `@backend/app/features/audio/router.py`:
- Around line 19-33: The public temp-audio endpoint serve_temp_audio currently
accepts arbitrary strings and has no per-route rate limiting; change the path
param type to UUID (use from uuid import UUID and signature
serve_temp_audio(token: UUID)) so FastAPI validates format before handler runs,
keep calling service.retrieve_temp_audio(token) without awaiting since it's
sync, and add a lightweight per-route rate limit (e.g., a dependency or
rate-limit decorator applied to the router.get("/temp/{token}") route) to
throttle requests and protect the unauthenticated surface.

In `@backend/app/features/audio/service.py`:
- Around line 394-450: The file exceeds the 300-line limit and mixes concerns;
move the rename flow into a smaller focused module (e.g.,
audio/rename_service.py) to split responsibilities: extract the
rename_audio_entry coroutine and any tightly-coupled helpers it needs
(references: rename_audio_entry, _row_to_entry, get_supabase_client,
_SELECT_COLS, _SIGNED_URL_TTL, settings.audio_bucket, logger) into the new
module and update imports in the original service to delegate to it; keep shared
constants or utilities (e.g., _row_to_entry, _SELECT_COLS) either in a new
audio/utils.py or export them from the original module so both pieces can import
them without duplicating logic, and run tests to ensure all references and
imports are updated.
- Around line 117-121: The code is sending raw user text to OpenRouter; call the
mask_pii() function from core/privacy.py on the user text before building the
payload. Import mask_pii, assign masked_text = mask_pii(text) and use
masked_text (not text) for the payload "input" value when constructing the dict
in the function that builds the payload (where settings.tts_model /
settings.tts_voice are used). Ensure you don't overwrite the original text
variable if it's needed elsewhere.
- Around line 67-90: The temp-audio helpers perform blocking filesystem I/O and
must be made async: change store_temp_audio, retrieve_temp_audio, and
delete_temp_audio to async def and move their blocking calls (path.write_bytes,
path.exists, path.stat, path.read_bytes, path.unlink) into asyncio.to_thread (or
an async file lib) and await those calls; preserve return types
(retrieve_temp_audio -> bytes | None returns as before). Also update any async
callers (e.g., the serve_temp_audio endpoint’s call to retrieve_temp_audio) to
await retrieve_temp_audio(token) so the event loop isn’t blocked.

In `@frontend/features/audio/components/AudioCard.tsx`:
- Around line 160-161: The delete confirmation text in AudioCard.tsx currently
interpolates entry.title which can be stale after a rename; update the
confirmation string to use optimisticTitle instead so the modal shows the title
the user currently sees. Locate the JSX that renders the confirmation message
(the string containing "{entry.title}") inside the AudioCard component and
replace entry.title with optimisticTitle, ensuring optimisticTitle is
defined/passed into the component where the confirmation is rendered.

In `@frontend/features/audio/hooks/useAudio.ts`:
- Around line 31-37: The rename mutation uses a global AUDIO_ENTRIES_KEY causing
cross-user cache leaks; update to scope keys by user: add a helper
audioEntriesKey(userId: string) (mirroring useConversations.ts), call
useUserId() inside useRenameAudio to get the current userId, and replace
AUDIO_ENTRIES_KEY with audioEntriesKey(userId) in the useMutation onSuccess
invalidation and any related queries; ensure mutationFn still calls
renameAudioEntry(id, title) but all cache operations use audioEntriesKey(userId)
so cache is isolated per user.

In `@frontend/lib/api.ts`:
- Around line 239-247: Add Zod validation at the API boundary inside
renameAudioEntry: define a Zod schema (e.g., renameAudioSchema) that
validates/normalizes the id (nonempty string, maybe uuid or pattern if required)
and title (string, min/max length, trim) and run zod.parse or zod.safeParse on
the inputs before using them; if validation fails, reject/throw with a clear
error instead of calling apiFetch. After successful parse use the
validated/normalized values (then encodeURIComponent(validated.id)) and pass
validated.title in the JSON body. Also add the necessary zod import and exported
types if needed.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa463d1b-f2e8-40bb-91e8-b8915f5e1396

📥 Commits

Reviewing files that changed from the base of the PR and between 373f547 and 33d2d6b.

📒 Files selected for processing (7)
  • backend/app/features/audio/router.py
  • backend/app/features/audio/schemas.py
  • backend/app/features/audio/service.py
  • frontend/app/(dashboard)/audio/page.tsx
  • frontend/features/audio/components/AudioCard.tsx
  • frontend/features/audio/hooks/useAudio.ts
  • frontend/lib/api.ts

Comment thread backend/app/features/audio/router.py
Comment thread backend/app/features/audio/service.py Outdated
Comment on lines 117 to 121
payload = {
"model": settings.tts_model,
"input": masked_text,
"input": text,
"voice": settings.tts_voice,
"response_format": "mp3",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reintroduce PII masking before sending text to OpenRouter.

Line 119 sends raw user text to an external provider. That creates a privacy/compliance gap and regresses existing protections.

Proposed fix
+from app.core.privacy import mask_pii
...
-    payload = {
+    masked_text = mask_pii(text)
+    if not masked_text.strip():
+        raise RuntimeError("Cannot generate TTS for empty text after masking.")
+
+    payload = {
         "model": settings.tts_model,
-        "input": text,
+        "input": masked_text,
         "voice": settings.tts_voice,
         "response_format": "mp3",
     }

As per coding guidelines: "NEVER send raw user input to OpenAI embeddings or OpenRouter without first calling mask_pii() from core/privacy.py."

🤖 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 `@backend/app/features/audio/service.py` around lines 117 - 121, The code is
sending raw user text to OpenRouter; call the mask_pii() function from
core/privacy.py on the user text before building the payload. Import mask_pii,
assign masked_text = mask_pii(text) and use masked_text (not text) for the
payload "input" value when constructing the dict in the function that builds the
payload (where settings.tts_model / settings.tts_voice are used). Ensure you
don't overwrite the original text variable if it's needed elsewhere.

Comment thread backend/app/features/audio/service.py Outdated
Comment on lines +394 to +450
async def rename_audio_entry(
entry_id: UUID, user_id: str, title: str
) -> AudioEntryOut | None:
"""Rename an audio entry's title.

Returns the updated AudioEntryOut, or None if the entry was not found.
Raises RuntimeError on DB or Storage failure.
"""
client = await get_supabase_client()

# UPDATE — postgrest-py's FilterRequestBuilder has no .select() after .eq(),
# so we do the update and re-fetch in a separate query.
try:
await (
client.table("audio_entries")
.update({"title": title})
.eq("id", str(entry_id))
.eq("user_id", user_id)
.execute()
)
except Exception:
logger.exception("[rename_audio_entry] UPDATE failed id=%s", entry_id)
raise RuntimeError("Failed to rename audio entry.") from None

# Re-fetch the updated row (also confirms the entry belongs to this user).
try:
fetch = (
await client.table("audio_entries")
.select(_SELECT_COLS)
.eq("id", str(entry_id))
.eq("user_id", user_id)
.execute()
)
except Exception:
logger.exception(
"[rename_audio_entry] SELECT after UPDATE failed id=%s", entry_id
)
raise RuntimeError("Failed to fetch renamed audio entry.") from None

if not fetch.data:
return None

row = fetch.data[0]
filename: str = row["filename"]
safe_log = filename.split("/")[-1] if "/" in filename else filename

try:
result = await client.storage.from_(settings.audio_bucket).create_signed_url(
filename, _SIGNED_URL_TTL
)
signed_url: str = result["signedURL"]
except Exception:
logger.exception("[rename_audio_entry] signed URL failed uuid=%s", safe_log)
raise RuntimeError("Title updated but failed to generate signed URL.") from None

logger.info("[rename_audio_entry] renamed uuid=%s", safe_log)
return _row_to_entry(row, signed_url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Split this service module; it now exceeds repo size limits.

Adding rename flow here pushes backend/app/features/audio/service.py well past the 300-line cap and further mixes responsibilities (TTS, temp storage, CRUD, signing).

As per coding guidelines: "No file longer than 300 lines — split into smaller modules."

🧰 Tools
🪛 Ruff (0.15.12)

[warning] 438-438: String is split more times than necessary

Use str.rsplit() and pass maxsplit=1

(PLC0207)

🤖 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 `@backend/app/features/audio/service.py` around lines 394 - 450, The file
exceeds the 300-line limit and mixes concerns; move the rename flow into a
smaller focused module (e.g., audio/rename_service.py) to split
responsibilities: extract the rename_audio_entry coroutine and any
tightly-coupled helpers it needs (references: rename_audio_entry, _row_to_entry,
get_supabase_client, _SELECT_COLS, _SIGNED_URL_TTL, settings.audio_bucket,
logger) into the new module and update imports in the original service to
delegate to it; keep shared constants or utilities (e.g., _row_to_entry,
_SELECT_COLS) either in a new audio/utils.py or export them from the original
module so both pieces can import them without duplicating logic, and run tests
to ensure all references and imports are updated.

Comment thread frontend/features/audio/components/AudioCard.tsx Outdated
Comment on lines +31 to +37
export function useRenameAudio() {
const qc = useQueryClient();
return useMutation<AudioEntryOut, Error, { id: string; title: string }>({
mutationFn: ({ id, title }) => renameAudioEntry(id, title),
onSuccess: () => {
void qc.invalidateQueries({ queryKey: AUDIO_ENTRIES_KEY });
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, find the audio hooks file and understand its structure
fd -t f "useAudio.ts" frontend/

Repository: Krasimir-Hristov/AXON

Length of output: 107


🏁 Script executed:

# Check the current content of useAudio.ts
cat -n frontend/features/audio/hooks/useAudio.ts

Repository: Krasimir-Hristov/AXON

Length of output: 1373


🏁 Script executed:

# Find how AUDIO_ENTRIES_KEY is defined
rg "AUDIO_ENTRIES_KEY" frontend/features/audio/

Repository: Krasimir-Hristov/AXON

Length of output: 459


🏁 Script executed:

# Check for cache key patterns across other hooks in the features directory
rg "const.*_KEY\s*=" frontend/features/ --type ts -A 2 | head -80

Repository: Krasimir-Hristov/AXON

Length of output: 1016


🏁 Script executed:

# Look at how other mutations handle cache invalidation in features
rg "invalidateQueries" frontend/features/ --type ts -B 2 -A 2 | head -100

Repository: Krasimir-Hristov/AXON

Length of output: 2643


🏁 Script executed:

# Check the conversationsKey function to see the proper user-scoping pattern
rg -A 5 "export function conversationsKey" frontend/features/chat/hooks/

Repository: Krasimir-Hristov/AXON

Length of output: 588


🏁 Script executed:

# Check how useAudioEntries uses the key and what listAudioEntries does
cat -n frontend/lib/api.ts | grep -A 10 "listAudioEntries\|deleteAudioEntry\|renameAudioEntry"

Repository: Krasimir-Hristov/AXON

Length of output: 714


🏁 Script executed:

# Check the full useConversations hook to see how it uses user-scoped keys
rg -B 5 -A 15 "useQuery.*CONVERSATIONS_KEY" frontend/features/chat/hooks/useConversations.ts

Repository: Krasimir-Hristov/AXON

Length of output: 47


🏁 Script executed:

# Check if there's a utility or pattern file for cache keys
find frontend -name "*cache*" -o -name "*key*" | grep -E "\.(ts|tsx)$"

Repository: Krasimir-Hristov/AXON

Length of output: 47


🏁 Script executed:

# Check how useAudioEntries is used and if userId is available at call site
rg "useAudioEntries" frontend/features/ -B 3 -A 3

Repository: Krasimir-Hristov/AXON

Length of output: 1539


🏁 Script executed:

# Check if there's a useCurrentUser or similar hook to get userId
rg "useCurrentUser\|useUser\|userId" frontend/features/audio/ --type ts -A 2

Repository: Krasimir-Hristov/AXON

Length of output: 47


🏁 Script executed:

# Verify the complete useConversations implementation to see how it properly uses userId
cat -n frontend/features/chat/hooks/useConversations.ts | head -60

Repository: Krasimir-Hristov/AXON

Length of output: 2434


Scope audio query keys by user ID to prevent cross-user cache leaks.

The rename mutation uses an unscoped cache key. If users switch accounts in the same client session, stale entries can be shown under the wrong identity. Adopt the pattern from useConversations.ts — create an audioEntriesKey(userId: string) function, call useUserId() from the Supabase session, and pass userId to all query and mutation cache keys.

🤖 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/features/audio/hooks/useAudio.ts` around lines 31 - 37, The rename
mutation uses a global AUDIO_ENTRIES_KEY causing cross-user cache leaks; update
to scope keys by user: add a helper audioEntriesKey(userId: string) (mirroring
useConversations.ts), call useUserId() inside useRenameAudio to get the current
userId, and replace AUDIO_ENTRIES_KEY with audioEntriesKey(userId) in the
useMutation onSuccess invalidation and any related queries; ensure mutationFn
still calls renameAudioEntry(id, title) but all cache operations use
audioEntriesKey(userId) so cache is isolated per user.

Comment thread frontend/lib/api.ts
- async temp I/O: wrap store/retrieve/delete_temp_audio in asyncio.to_thread
- extract rename_audio_entry to rename.py (service.py was ~460 lines)
- router: UUID token type + rate limit on /temp/{token} endpoint
- tool: await async temp audio service calls
- AudioCard: show optimisticTitle in delete dialog (was stale entry.title)
- useAudio: user-scoped cache keys to prevent cross-user cache leak
- api: Zod validation on renameAudioEntry inputs

skip: mask_pii intentionally removed (causes TTS to say 'person')
@Krasimir-Hristov
Krasimir-Hristov merged commit 35087c8 into main May 16, 2026
2 checks passed
@Krasimir-Hristov
Krasimir-Hristov deleted the feat/audio-rename-title branch May 16, 2026 17:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant