Skip to content

Commit a65eaa4

Browse files
committed
fix(ui): TTS page UX improvements and CSP fix for audio playback
- Add media-src 'self' blob: to CSP for TTS audio blob URLs - Show advanced config section expanded by default - Add Save button in credentials section - Show Voice/Playground sections only after credentials saved - Improve voice picker message when API key not saved yet - Add Vite proxy timeout for large audio responses
1 parent c85730e commit a65eaa4

9 files changed

Lines changed: 50 additions & 20 deletions

File tree

ui/web/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
- style-src 'self' 'unsafe-inline' — Tailwind/inline loader keyframes +
1414
Google Fonts stylesheet require inline styles.
1515
- font-src self + gstatic — Google Fonts woff2 files.
16-
- img-src self + data: — base64 avatars/thumbnails.
16+
- img-src self + data: + blob: — base64 avatars/thumbnails, blob previews.
17+
- media-src self + blob: — TTS audio playback via blob URLs.
1718
- connect-src self + ws:/wss: — websocket to gateway. Same-origin only;
1819
if the gateway is hosted on a different domain in production, add the
1920
explicit origin here rather than wildcard-allowing all http(s):.
2021
-->
21-
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' data: https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob:; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self';" />
22+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net; font-src 'self' data: https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' ws: wss:; frame-ancestors 'none'; base-uri 'self';" />
2223
<link rel="preconnect" href="https://fonts.googleapis.com" />
2324
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
2425
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />

ui/web/src/components/voice-picker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ function DynamicVoicePicker({
236236
</div>
237237
) : filtered.length === 0 ? (
238238
<p className="py-4 text-center text-sm text-muted-foreground">
239-
{voices.length === 0 ? t("voice_no_voices") : search ? t("voice_no_voices") : t("voice_loading")}
239+
{voices.length === 0 ? t("voice_save_config_first") : search ? t("voice_no_voices") : t("voice_loading")}
240240
</p>
241241
) : (
242242
filtered.map((voice) => (

ui/web/src/i18n/locales/en/tts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"voice_preview": "Preview",
7070
"voice_stop_preview": "Stop",
7171
"voice_no_voices": "No voices available",
72+
"voice_save_config_first": "Save API key first to load voices",
7273
"voice_loading": "Loading voices…",
7374
"voice_preview_error": "Preview failed — refreshing list",
7475
"model_label": "Model",

ui/web/src/i18n/locales/vi/tts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"voice_preview": "Nghe thử",
3939
"voice_stop_preview": "Dừng",
4040
"voice_no_voices": "Chưa có giọng đọc",
41+
"voice_save_config_first": "Lưu API key trước để tải giọng đọc",
4142
"voice_loading": "Đang tải giọng đọc…",
4243
"voice_preview_error": "Không nghe thử được — đang làm mới",
4344
"model_label": "Mô hình",

ui/web/src/i18n/locales/zh/tts.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"voice_preview": "试听",
3939
"voice_stop_preview": "停止",
4040
"voice_no_voices": "暂无声音",
41+
"voice_save_config_first": "请先保存 API Key 以加载声音",
4142
"voice_loading": "加载声音中…",
4243
"voice_preview_error": "试听失败 — 正在刷新",
4344
"model_label": "模型",

ui/web/src/pages/tts/sections/behavior-section.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Behavior section — collapsible "Advanced settings" panel.
33
* Contains: auto mode, reply mode, max_length, timeout_ms.
4-
* Collapsed by default to reduce visual noise for the common 4-step flow.
4+
* Expanded by default for visibility; users can collapse if needed.
55
* Uses local useState + chevron (no Radix Collapsible dependency needed).
66
*/
77
import { useState } from "react";
@@ -24,7 +24,7 @@ interface Props {
2424

2525
export function BehaviorSection({ draft, onUpdate }: Props) {
2626
const { t } = useTranslation("tts");
27-
const [open, setOpen] = useState(false);
27+
const [open, setOpen] = useState(true);
2828

2929
return (
3030
<Card>

ui/web/src/pages/tts/sections/credentials-section.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { useState } from "react";
88
import { useTranslation } from "react-i18next";
9-
import { FlaskConical } from "lucide-react";
9+
import { FlaskConical, Save } from "lucide-react";
1010
import { Button } from "@/components/ui/button";
1111
import { Input } from "@/components/ui/input";
1212
import { Label } from "@/components/ui/label";
@@ -23,9 +23,12 @@ interface Props {
2323
patch: Partial<TtsProviderConfig>,
2424
) => void;
2525
testConnection: (params: TestConnectionParams) => Promise<TestConnectionResult>;
26+
onSave: () => Promise<void>;
27+
saving?: boolean;
28+
dirty?: boolean;
2629
}
2730

28-
export function CredentialsSection({ provider, draft, onUpdate, testConnection }: Props) {
31+
export function CredentialsSection({ provider, draft, onUpdate, testConnection, onSave, saving, dirty }: Props) {
2932
const { t } = useTranslation("tts");
3033
const [testing, setTesting] = useState(false);
3134

@@ -128,17 +131,29 @@ export function CredentialsSection({ provider, draft, onUpdate, testConnection }
128131
</>
129132
)}
130133

131-
<Button
132-
type="button"
133-
variant="outline"
134-
size="sm"
135-
className="h-9 gap-1.5"
136-
disabled={testing}
137-
onClick={handleTestConnection}
138-
>
139-
<FlaskConical className="h-3.5 w-3.5" />
140-
{testing ? t("testConnection.testing", "Testing…") : t("testConnection.label", "Test connection")}
141-
</Button>
134+
<div className="flex gap-2">
135+
<Button
136+
type="button"
137+
variant="outline"
138+
size="sm"
139+
className="h-9 gap-1.5"
140+
disabled={testing}
141+
onClick={handleTestConnection}
142+
>
143+
<FlaskConical className="h-3.5 w-3.5" />
144+
{testing ? t("testConnection.testing", "Testing…") : t("testConnection.label", "Test connection")}
145+
</Button>
146+
<Button
147+
type="button"
148+
size="sm"
149+
className="h-9 gap-1.5"
150+
disabled={saving || !dirty}
151+
onClick={onSave}
152+
>
153+
<Save className="h-3.5 w-3.5" />
154+
{saving ? t("saving") : t("save")}
155+
</Button>
156+
</div>
142157
</CardContent>
143158
</Card>
144159
);

ui/web/src/pages/tts/tts-page.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@ function modelPatch(provider: string, value: string): [ProviderKey, Partial<TtsP
6565
}
6666
}
6767

68+
// Check if provider credentials are saved (to show lower sections)
69+
function isCredentialsSaved(provider: string, tts: TtsConfig): boolean {
70+
if (provider === "edge") return true; // Edge doesn't require API key
71+
const cfg = tts[provider as ProviderKey];
72+
return !!cfg?.api_key;
73+
}
74+
6875
export function TtsPage() {
6976
const { t } = useTranslation("tts");
7077
const { t: tc } = useTranslation("common");
@@ -139,10 +146,13 @@ export function TtsPage() {
139146
draft={draft}
140147
onUpdate={updateProvider}
141148
testConnection={testConnection}
149+
onSave={handleSave}
150+
saving={saving}
151+
dirty={dirty}
142152
/>
143153
)}
144154

145-
{draft.provider && (
155+
{draft.provider && isCredentialsSaved(draft.provider, tts) && (
146156
<VoiceModelSection
147157
provider={draft.provider}
148158
voiceId={getVoiceId(draft)}
@@ -152,7 +162,7 @@ export function TtsPage() {
152162
/>
153163
)}
154164

155-
{draft.provider && (
165+
{draft.provider && isCredentialsSaved(draft.provider, tts) && (
156166
<TestPlayground
157167
provider={draft.provider}
158168
voiceId={getVoiceId(draft)}

ui/web/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default defineConfig(({ mode }) => {
2626
"/v1": {
2727
target: `http://${backendHost}:${backendPort}`,
2828
changeOrigin: true,
29+
timeout: 30000, // 30s for large audio responses
2930
},
3031
"/health": {
3132
target: `http://${backendHost}:${backendPort}`,

0 commit comments

Comments
 (0)