feat(frontend): Phase 1 – normalize speakers endpoints, fix API docs and JSON parsing, consolidate state in app.js, tweak CSS border color, align jest/babel-jest + add jest.config.cjs, add dev scripts, sanitize repo URL
This commit is contained in:
parent
948712bb3f
commit
b62eb0211f
|
@ -0,0 +1,138 @@
|
||||||
|
# Frontend Review and Recommendations
|
||||||
|
|
||||||
|
Date: 2025-08-12T11:32:16-05:00
|
||||||
|
Scope: `frontend/` of `chatterbox-test` monorepo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- Static vanilla JS frontend served by `frontend/start_dev_server.py` interacting with FastAPI backend under `/api`.
|
||||||
|
- Solid feature set (speaker management, dialog editor, per-line generation, full dialog generation, save/load) with robust error handling.
|
||||||
|
- Key issues: inconsistent API trailing slashes, Jest/babel-jest version/config mismatch, minor state duplication, alert/confirm UX, overly dark border color, token in `package.json` repo URL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
- **Framework/structure**
|
||||||
|
- `frontend/` is static vanilla JS. Main files:
|
||||||
|
- `index.html`, `js/app.js`, `js/api.js`, `js/config.js`, `css/style.css`.
|
||||||
|
- Dev server: `frontend/start_dev_server.py` (CORS, env-based port/host).
|
||||||
|
|
||||||
|
- **API client vs backend routes (trailing slashes)**
|
||||||
|
- Frontend `frontend/js/api.js` currently uses:
|
||||||
|
- `getSpeakers()`: `${API_BASE_URL}/speakers/` (trailing).
|
||||||
|
- `addSpeaker()`: `${API_BASE_URL}/speakers/` (trailing).
|
||||||
|
- `deleteSpeaker()`: `${API_BASE_URL}/speakers/${speakerId}/` (trailing).
|
||||||
|
- `generateLine()`: `${API_BASE_URL}/dialog/generate_line`.
|
||||||
|
- `generateDialog()`: `${API_BASE_URL}/dialog/generate`.
|
||||||
|
- Backend routes:
|
||||||
|
- `backend/app/routers/speakers.py`: `GET/POST /` and `DELETE /{speaker_id}` (no trailing slash on delete when prefixed under `/api/speakers`).
|
||||||
|
- `backend/app/routers/dialog.py`: `/generate_line` and `/generate` (match frontend).
|
||||||
|
- Tests in `frontend/tests/api.test.js` expect no trailing slashes for `/speakers` and `/speakers/{id}`.
|
||||||
|
- Implication: Inconsistent trailing slashes can cause test failures and possible 404s for delete.
|
||||||
|
|
||||||
|
- **Payload schema inconsistencies**
|
||||||
|
- `generateDialog()` JSDoc shows `silence` as `{ duration_ms: 500 }` but backend expects `duration` (seconds). UI also uses `duration` seconds.
|
||||||
|
|
||||||
|
- **Form fields alignment**
|
||||||
|
- Speaker add uses `name` and `audio_file` which match backend (`Form` and `File`).
|
||||||
|
|
||||||
|
- **State management duplication in `frontend/js/app.js`**
|
||||||
|
- `dialogItems` and `availableSpeakersCache` defined at module scope and again inside `initializeDialogEditor()`, creating shadowing risk. Consolidate to a single source of truth.
|
||||||
|
|
||||||
|
- **UX considerations**
|
||||||
|
- Heavy use of `alert()`/`confirm()`. Prefer inline notifications/banners and per-row error chips (you already render `item.error`).
|
||||||
|
- Add global loading/disabled states for long actions (e.g., full dialog generation, speaker add/delete).
|
||||||
|
|
||||||
|
- **CSS theme issue**
|
||||||
|
- `--border-light` is `#1b0404` (dark red); semantically a light gray fits better and improves contrast harmony.
|
||||||
|
|
||||||
|
- **Testing/Jest/Babel config**
|
||||||
|
- Root `package.json` uses `jest@^29.7.0` with `babel-jest@^30.0.0-beta.3` (major mismatch). Align versions.
|
||||||
|
- No `jest.config.cjs` to configure `transform` via `babel-jest` for ESM modules.
|
||||||
|
|
||||||
|
- **Security**
|
||||||
|
- `package.json` `repository.url` embeds a token. Remove secrets from VCS immediately.
|
||||||
|
|
||||||
|
- **Dev scripts**
|
||||||
|
- Only `"test": "jest"` present. Add scripts to run the frontend dev server and test config explicitly.
|
||||||
|
|
||||||
|
- **Response handling consistency**
|
||||||
|
- `generateLine()` parses via `response.text()` then `JSON.parse()`. Others use `response.json()`. Standardize for consistency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Actions (Phase 1: Quick wins)
|
||||||
|
|
||||||
|
- **Normalize API paths in `frontend/js/api.js`**
|
||||||
|
- Use no trailing slashes:
|
||||||
|
- `GET/POST`: `${API_BASE_URL}/speakers`
|
||||||
|
- `DELETE`: `${API_BASE_URL}/speakers/${speakerId}`
|
||||||
|
- Keep dialog endpoints unchanged.
|
||||||
|
|
||||||
|
- **Fix JSDoc for `generateDialog()`**
|
||||||
|
- Use `silence: { duration: number }` (seconds), not `duration_ms`.
|
||||||
|
|
||||||
|
- **Refactor `frontend/js/app.js` state**
|
||||||
|
- Remove duplicate `dialogItems`/`availableSpeakersCache` declarations. Choose module-scope or function-scope, and pass references.
|
||||||
|
|
||||||
|
- **Improve UX**
|
||||||
|
- Replace `alert/confirm` with inline banners near `#results-display` and per-row error chips (extend existing `.line-error-msg`).
|
||||||
|
- Add disabled/loading states for global generate and speaker actions.
|
||||||
|
|
||||||
|
- **CSS tweak**
|
||||||
|
- Set `--border-light: #e5e7eb;` (or similar) to reflect a light border.
|
||||||
|
|
||||||
|
- **Harden tests/Jest config**
|
||||||
|
- Align versions: either Jest 29 + `babel-jest` 29, or upgrade both to 30 stable together.
|
||||||
|
- Add `jest.config.cjs` with `transform` using `babel-jest` and suitable `testEnvironment`.
|
||||||
|
- Ensure tests expect normalized API paths (recommended to change code to match tests).
|
||||||
|
|
||||||
|
- **Dev scripts**
|
||||||
|
- Add to root `package.json`:
|
||||||
|
- `"frontend:dev": "python3 frontend/start_dev_server.py"`
|
||||||
|
- `"test:frontend": "jest --config ./jest.config.cjs"`
|
||||||
|
|
||||||
|
- **Sanitize repository URL**
|
||||||
|
- Remove embedded token from `package.json`.
|
||||||
|
|
||||||
|
- **Standardize response parsing**
|
||||||
|
- Switch `generateLine()` to `response.json()` unless backend returns `text/plain`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backend Endpoint Confirmation
|
||||||
|
|
||||||
|
- `speakers` router (`backend/app/routers/speakers.py`):
|
||||||
|
- List/Create: `GET /`, `POST /` (when mounted under `/api/speakers` → `/api/speakers/`).
|
||||||
|
- Delete: `DELETE /{speaker_id}` (→ `/api/speakers/{speaker_id}`), no trailing slash.
|
||||||
|
- `dialog` router (`backend/app/routers/dialog.py`):
|
||||||
|
- `POST /generate_line`, `POST /generate` (mounted under `/api/dialog`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Implementation Plan
|
||||||
|
|
||||||
|
- **Phase 1 (1–2 hours)**
|
||||||
|
- Normalize API paths in `api.js`.
|
||||||
|
- Fix JSDoc for `generateDialog`.
|
||||||
|
- Consolidate dialog state in `app.js`.
|
||||||
|
- Adjust `--border-light` to light gray.
|
||||||
|
- Add `jest.config.cjs`, align Jest/babel-jest versions.
|
||||||
|
- Add dev/test scripts.
|
||||||
|
- Remove token from `package.json`.
|
||||||
|
|
||||||
|
- **Phase 2 (2–4 hours)**
|
||||||
|
- Inline notifications and comprehensive loading/disabled states.
|
||||||
|
|
||||||
|
- **Phase 3 (optional)**
|
||||||
|
- ESLint + Prettier.
|
||||||
|
- Consider Vite migration (HMR, proxy to backend, improved DX).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Current local time captured for this review: 2025-08-12T11:32:16-05:00.
|
||||||
|
- Frontend config (`frontend/js/config.js`) supports env overrides for API base and dev server port.
|
||||||
|
- Tests (`frontend/tests/api.test.js`) currently assume endpoints without trailing slashes.
|
|
@ -24,7 +24,7 @@
|
||||||
--text-blue-darker: #205081;
|
--text-blue-darker: #205081;
|
||||||
|
|
||||||
/* Border Colors */
|
/* Border Colors */
|
||||||
--border-light: #1b0404;
|
--border-light: #e5e7eb;
|
||||||
--border-medium: #cfd8dc;
|
--border-medium: #cfd8dc;
|
||||||
--border-blue: #b5c6df;
|
--border-blue: #b5c6df;
|
||||||
--border-gray: #e3e3e3;
|
--border-gray: #e3e3e3;
|
||||||
|
|
|
@ -10,7 +10,7 @@ const API_BASE_URL = API_BASE_URL_WITH_PREFIX;
|
||||||
* @throws {Error} If the network response is not ok.
|
* @throws {Error} If the network response is not ok.
|
||||||
*/
|
*/
|
||||||
export async function getSpeakers() {
|
export async function getSpeakers() {
|
||||||
const response = await fetch(`${API_BASE_URL}/speakers/`);
|
const response = await fetch(`${API_BASE_URL}/speakers`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
throw new Error(`Failed to fetch speakers: ${errorData.detail || errorData.message || response.statusText}`);
|
throw new Error(`Failed to fetch speakers: ${errorData.detail || errorData.message || response.statusText}`);
|
||||||
|
@ -26,12 +26,12 @@ export async function getSpeakers() {
|
||||||
* Adds a new speaker.
|
* Adds a new speaker.
|
||||||
* @param {FormData} formData - The form data containing speaker name and audio file.
|
* @param {FormData} formData - The form data containing speaker name and audio file.
|
||||||
* Example: formData.append('name', 'New Speaker');
|
* Example: formData.append('name', 'New Speaker');
|
||||||
* formData.append('audio_sample_file', fileInput.files[0]);
|
* formData.append('audio_file', fileInput.files[0]);
|
||||||
* @returns {Promise<Object>} A promise that resolves to the new speaker object.
|
* @returns {Promise<Object>} A promise that resolves to the new speaker object.
|
||||||
* @throws {Error} If the network response is not ok.
|
* @throws {Error} If the network response is not ok.
|
||||||
*/
|
*/
|
||||||
export async function addSpeaker(formData) {
|
export async function addSpeaker(formData) {
|
||||||
const response = await fetch(`${API_BASE_URL}/speakers/`, {
|
const response = await fetch(`${API_BASE_URL}/speakers`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData, // FormData sets Content-Type to multipart/form-data automatically
|
body: formData, // FormData sets Content-Type to multipart/form-data automatically
|
||||||
});
|
});
|
||||||
|
@ -86,7 +86,7 @@ export async function addSpeaker(formData) {
|
||||||
* @throws {Error} If the network response is not ok.
|
* @throws {Error} If the network response is not ok.
|
||||||
*/
|
*/
|
||||||
export async function deleteSpeaker(speakerId) {
|
export async function deleteSpeaker(speakerId) {
|
||||||
const response = await fetch(`${API_BASE_URL}/speakers/${speakerId}/`, {
|
const response = await fetch(`${API_BASE_URL}/speakers/${speakerId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
@ -124,18 +124,8 @@ export async function generateLine(line) {
|
||||||
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
throw new Error(`Failed to generate line audio: ${errorData.detail || errorData.message || response.statusText}`);
|
throw new Error(`Failed to generate line audio: ${errorData.detail || errorData.message || response.statusText}`);
|
||||||
}
|
}
|
||||||
|
const data = await response.json();
|
||||||
const responseText = await response.text();
|
return data;
|
||||||
console.log('Raw response text:', responseText);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const jsonData = JSON.parse(responseText);
|
|
||||||
console.log('Parsed JSON:', jsonData);
|
|
||||||
return jsonData;
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('JSON parse error:', parseError);
|
|
||||||
throw new Error(`Invalid JSON response: ${responseText}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -146,7 +136,7 @@ export async function generateLine(line) {
|
||||||
* output_base_name: "my_dialog",
|
* output_base_name: "my_dialog",
|
||||||
* dialog_items: [
|
* dialog_items: [
|
||||||
* { type: "speech", speaker_id: "speaker1", text: "Hello world.", exaggeration: 1.0, cfg_weight: 2.0, temperature: 0.7 },
|
* { type: "speech", speaker_id: "speaker1", text: "Hello world.", exaggeration: 1.0, cfg_weight: 2.0, temperature: 0.7 },
|
||||||
* { type: "silence", duration_ms: 500 },
|
* { type: "silence", duration: 0.5 },
|
||||||
* { type: "speech", speaker_id: "speaker2", text: "How are you?" }
|
* { type: "speech", speaker_id: "speaker2", text: "How are you?" }
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
|
|
|
@ -140,9 +140,6 @@ async function initializeDialogEditor() {
|
||||||
const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder');
|
const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder');
|
||||||
const resultsDisplaySection = document.getElementById('results-display');
|
const resultsDisplaySection = document.getElementById('results-display');
|
||||||
|
|
||||||
let dialogItems = [];
|
|
||||||
let availableSpeakersCache = []; // Cache for speaker names and IDs
|
|
||||||
|
|
||||||
// Load speakers at startup
|
// Load speakers at startup
|
||||||
try {
|
try {
|
||||||
availableSpeakersCache = await getSpeakers();
|
availableSpeakersCache = await getSpeakers();
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
// jest.config.cjs
|
||||||
|
module.exports = {
|
||||||
|
testEnvironment: 'node',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.js$': 'babel-jest',
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['js', 'json'],
|
||||||
|
roots: ['<rootDir>/frontend/tests', '<rootDir>'],
|
||||||
|
};
|
|
@ -5,11 +5,13 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"test:frontend": "jest --config ./jest.config.cjs",
|
||||||
|
"frontend:dev": "python3 frontend/start_dev_server.py"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://oauth2:78f77aaebb8fa1cd3efbd5b738177c127f7d7d0b@gitea.r8z.us/stwhite/chatterbox-ui.git"
|
"url": "https://gitea.r8z.us/stwhite/chatterbox-ui.git"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -17,7 +19,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.27.4",
|
"@babel/core": "^7.27.4",
|
||||||
"@babel/preset-env": "^7.27.2",
|
"@babel/preset-env": "^7.27.2",
|
||||||
"babel-jest": "^30.0.0-beta.3",
|
"babel-jest": "^29.7.0",
|
||||||
"jest": "^29.7.0"
|
"jest": "^29.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue