Skip to content

Comments

Add speaker management frontend page#48

Draft
skywinder wants to merge 1 commit intodevfrom
claude/explore-diarization-0Jg1a
Draft

Add speaker management frontend page#48
skywinder wants to merge 1 commit intodevfrom
claude/explore-diarization-0Jg1a

Conversation

@skywinder
Copy link
Collaborator

  • Create SpeakersPage.tsx with enrollment dialog and timeline selection
  • Add backend proxy route for diarization service at /api/diarization/*
  • Add navigation link in Layout.tsx
  • Register /speakers route in router.tsx

Features:

  • List enrolled speakers with profile cards
  • Enroll new speakers by uploading audio files
  • Select diarization segments from timeline as samples
  • View speaker details with stats
  • Delete speakers

https://claude.ai/code/session_01QkQdYmZstodpuXPoZZoiKN

- Create SpeakersPage.tsx with enrollment dialog and timeline selection
- Add backend proxy route for diarization service at /api/diarization/*
- Add navigation link in Layout.tsx
- Register /speakers route in router.tsx

Features:
- List enrolled speakers with profile cards
- Enroll new speakers by uploading audio files
- Select diarization segments from timeline as samples
- View speaker details with stats
- Delete speakers

https://claude.ai/code/session_01QkQdYmZstodpuXPoZZoiKN
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds a comprehensive speaker management interface to enable voice identification through speaker enrollment. The feature allows users to create speaker profiles by uploading audio samples or selecting segments from existing diarization data.

Changes:

  • Added SpeakersPage with enrollment dialog supporting file uploads and timeline-based segment selection
  • Created backend proxy route /api/diarization/* to forward requests to the diarization microservice with authentication
  • Added navigation link for the Speakers page in the application layout

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 18 comments.

Show a summary per file
File Description
frontend/src/router.tsx Registered the /speakers route pointing to SpeakersPage component
frontend/src/pages/SpeakersPage.tsx New page component for managing speaker profiles with enrollment, deletion, and timeline selection features
frontend/src/components/Layout.tsx Added "Speakers" navigation link with Users icon in the sidebar
backend/routes.ts Registered wildcard route for diarization service proxy and imported enrollment handler (not yet used)
backend/app/routes/api.diarization.ts Implemented proxy handler for diarization service requests with authentication and error handling

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +59
// For multipart requests, we need to reconstruct the form data
// This is tricky with Express - we'll pass through the raw request
// Actually, since Express parses the body, we need to handle this differently

// For file uploads, we need to handle this specially
// The frontend should send directly to the diarization service for file uploads
// Or we need to use a middleware like multer

// For now, let's handle JSON requests and simple form data
res.status(400).json({
error: "File upload proxy not yet implemented. Please use direct upload to diarization service."
});
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The multipart/form-data handling returns an error stating the feature is not implemented. However, there's a separate apiDiarizationEnrollHandler function below (lines 117-172) that implements this functionality but is never registered. This creates a confusing situation where enrollment via file upload will always fail with an unhelpful error message. Either implement multipart handling here or register and use the dedicated enrollment handler.

Suggested change
// For multipart requests, we need to reconstruct the form data
// This is tricky with Express - we'll pass through the raw request
// Actually, since Express parses the body, we need to handle this differently
// For file uploads, we need to handle this specially
// The frontend should send directly to the diarization service for file uploads
// Or we need to use a middleware like multer
// For now, let's handle JSON requests and simple form data
res.status(400).json({
error: "File upload proxy not yet implemented. Please use direct upload to diarization service."
});
// Delegate multipart/form-data (file upload) handling to the dedicated
// enrollment handler, which is implemented below in this file.
// This avoids returning a misleading "not implemented" error while
// reusing the existing logic for enrollment via file upload.
await apiDiarizationEnrollHandler(req, res);

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +165
if (error instanceof Error && error.message === "Unauthorized") {
return;
}

Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Same issue as line 90-92: the "Unauthorized" error check will never match because authenticateOr401 throws a Response object, not an Error. This check can be removed as the error handler middleware will properly handle authentication failures.

Suggested change
if (error instanceof Error && error.message === "Unauthorized") {
return;
}

Copilot uses AI. Check for mistakes.
});
setDiarizationSegments(docs);
} catch (err) {
console.error("Failed to load segments:", err);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Error handling for loading diarization segments is incomplete. When the segment loading fails, the error is only logged to the console but not displayed to the user. Consider adding error state and showing an error message in the UI so users understand why segments aren't loading.

Suggested change
console.error("Failed to load segments:", err);
console.error("Failed to load segments:", err);
throw err;

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +229
await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The DELETE request doesn't check the response status. If the server returns a non-OK status (e.g., 404, 500), the code will still proceed to reload speakers and close the dialog without notifying the user of the failure. Consider checking response.ok similar to how it's done in the enrollment handler.

Suggested change
await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
const response = await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
if (!response.ok) {
let errorMessage = `Failed to delete speaker: ${response.status} ${response.statusText}`;
try {
const bodyText = await response.text();
if (bodyText) {
errorMessage += ` - ${bodyText}`;
}
} catch {
// Ignore errors while reading the response body
}
throw new Error(errorMessage);
}

Copilot uses AI. Check for mistakes.
const [detailSheetOpen, setDetailSheetOpen] = useState(false);

// Timeline selection state for enrollment
const [timelineSelectionMode, setTimelineSelectionMode] = useState(false);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The timelineSelectionMode state variable is declared but never used. The component has timeline selection functionality but doesn't appear to use this state variable. Consider removing it or implementing the intended functionality that uses it.

Suggested change
const [timelineSelectionMode, setTimelineSelectionMode] = useState(false);

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +168
const speakerId = `user_${userId}_${enrollName.toLowerCase().replace(/\s+/g, "_")}`;
const formData = new FormData();
formData.append("speaker_id", speakerId);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The speaker ID is generated client-side using a hardcoded user ID and transformed speaker name. This creates potential issues: (1) Multiple speakers with the same name would have ID collisions, (2) The naming convention is exposed and could be manipulated. Consider having the backend generate speaker IDs to ensure uniqueness and proper user isolation.

Suggested change
const speakerId = `user_${userId}_${enrollName.toLowerCase().replace(/\s+/g, "_")}`;
const formData = new FormData();
formData.append("speaker_id", speakerId);
const formData = new FormData();

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +233
await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
await loadSpeakers();
setDeleteConfirmSpeaker(null);
} catch (err) {
console.error("Delete failed:", err);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The error handling for the delete operation is incomplete. When the delete request fails, the error is logged but not shown to the user, and setDeleteConfirmSpeaker(null) is called regardless of success or failure. Users won't know if the deletion failed. Consider adding error state and displaying it to the user, and only close the confirmation dialog on successful deletion.

Suggested change
await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
await loadSpeakers();
setDeleteConfirmSpeaker(null);
} catch (err) {
console.error("Delete failed:", err);
const response = await fetch(`${apiClient.baseURL}${DIARIZATION_SERVICE_URL}/speakers/${speaker.id}`, {
method: "DELETE",
headers: await apiClient.getAuthHeaders(),
});
if (!response.ok) {
throw new Error(`Failed to delete speaker (status ${response.status})`);
}
await loadSpeakers();
setDeleteConfirmSpeaker(null);
} catch (err) {
console.error("Delete failed:", err);
alert("Failed to delete speaker. Please try again.");

Copilot uses AI. Check for mistakes.
import { wellKnownOauthAuthorizationServerHandler } from "@/routes/[.]well-known.oauth-authorization-server.ts";
import { wellKnownOauthProtectedResourceHandler } from "@/routes/[.]well-known.oauth-protected-resource.ts";
import { apiChatHandler } from "@/routes/api.chat.ts";
import { apiDiarizationProxyHandler, apiDiarizationEnrollHandler } from "@/routes/api.diarization.ts";
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

The apiDiarizationEnrollHandler is imported but never registered as a route in the Express app. This handler appears to be specifically designed to handle multipart file uploads for speaker enrollment, but it's not being used. Either this export should be removed, or a route should be registered for it (e.g., app.post("/api/diarization/enroll/batch", asyncHandler(apiDiarizationEnrollHandler))).

Suggested change
import { apiDiarizationProxyHandler, apiDiarizationEnrollHandler } from "@/routes/api.diarization.ts";
import { apiDiarizationProxyHandler } from "@/routes/api.diarization.ts";

Copilot uses AI. Check for mistakes.
@@ -1,5 +1,5 @@
import { Link, Navigate, Outlet, useLocation } from "react-router-dom";
import { Clock, Home, Package, Settings, MessageSquare, Activity, Mic } from "lucide-react";
import { Clock, Home, Package, Settings, MessageSquare, Activity, Mic, Users } from "lucide-react";
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

Unused import Home.

Suggested change
import { Clock, Home, Package, Settings, MessageSquare, Activity, Mic, Users } from "lucide-react";
import { Clock, Package, Settings, MessageSquare, Activity, Mic, Users } from "lucide-react";

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +301
<Button variant="outline" onClick={loadSpeakers} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

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

This use of variable 'loading' always evaluates to false.

Suggested change
<Button variant="outline" onClick={loadSpeakers} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />
<Button variant="outline" onClick={loadSpeakers}>
<RefreshCw className="h-4 w-4 mr-2" />

Copilot uses AI. Check for mistakes.
@skywinder skywinder marked this pull request as draft January 29, 2026 19:37
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.

2 participants