Skip to content

Commit 384692d

Browse files
committed
fix(ui): multi-upload UI
1 parent 80a286c commit 384692d

File tree

4 files changed

+303
-73
lines changed

4 files changed

+303
-73
lines changed

ui/src/components/converter/AssetSelector.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,15 @@ interface AssetSelectorProps {
9494
selectedAssetId: string;
9595
onAssetSelect: (assetId: string) => void;
9696
isLoading?: boolean;
97+
groupId?: string;
9798
}
9899

99100
export const AssetSelector: React.FC<AssetSelectorProps> = ({
100101
assets,
101102
selectedAssetId,
102103
onAssetSelect,
103104
isLoading = false,
105+
groupId = 'asset',
104106
}) => {
105107
if (isLoading) {
106108
return (
@@ -134,11 +136,12 @@ export const AssetSelector: React.FC<AssetSelectorProps> = ({
134136
value={selectedAssetId}
135137
onValueChange={onAssetSelect}
136138
aria-label="Audio asset selection"
139+
name={`asset-selector-${groupId}`}
137140
>
138141
<AssetGrid>
139142
{assets.map((asset) => (
140-
<AssetCard key={asset.id} htmlFor={`asset-${asset.id}`}>
141-
<RadioItem value={asset.id} id={`asset-${asset.id}`}>
143+
<AssetCard key={`${groupId}-${asset.id}`} htmlFor={`${groupId}-asset-${asset.id}`}>
144+
<RadioItem value={asset.id} id={`${groupId}-asset-${asset.id}`}>
142145
<RadioIndicator />
143146
</RadioItem>
144147
<AssetContent>

ui/src/services/converter.test.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ vi.mock('./base', () => ({
3838
describe('converter service', () => {
3939
const MOCK_YAML = 'steps:\n - id: test\n kind: core::passthrough';
4040
const MOCK_FILE = new File(['test content'], 'test.ogg', { type: 'audio/ogg' });
41+
const MOCK_UPLOAD = [{ field: 'media', file: MOCK_FILE }];
4142

4243
let originalMediaSource: unknown;
4344

@@ -69,7 +70,7 @@ describe('converter service', () => {
6970
body: mockBody,
7071
} as Response);
7172

72-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
73+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
7374

7475
expect(result.success).toBe(true);
7576
expect(result.useStreaming).toBe(true);
@@ -97,7 +98,7 @@ describe('converter service', () => {
9798
} as unknown as Response);
9899

99100
const abortController = new AbortController();
100-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback', abortController.signal);
101+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback', abortController.signal);
101102

102103
expect(result.success).toBe(true);
103104
expect(result.responseStream).toBeDefined();
@@ -126,7 +127,7 @@ describe('converter service', () => {
126127
body: mockBody,
127128
} as Response);
128129

129-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
130+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
130131

131132
expect(result.success).toBe(true);
132133
expect(result.useStreaming).toBe(true);
@@ -155,7 +156,7 @@ describe('converter service', () => {
155156
} as unknown as Response);
156157

157158
const abortController = new AbortController();
158-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback', abortController.signal);
159+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback', abortController.signal);
159160

160161
expect(result.responseStream).toBeDefined();
161162

@@ -178,7 +179,7 @@ describe('converter service', () => {
178179
blob: vi.fn().mockResolvedValue(mockBlob),
179180
} as never);
180181

181-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
182+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
182183

183184
expect(result.success).toBe(true);
184185
expect(result.useStreaming).toBe(false);
@@ -212,7 +213,7 @@ describe('converter service', () => {
212213
blob: vi.fn().mockResolvedValue(mockBlob),
213214
} as never);
214215

215-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'download');
216+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'download');
216217

217218
expect(result.success).toBe(true);
218219
expect(mockLink.href).toBe('blob:download-url');
@@ -244,7 +245,7 @@ describe('converter service', () => {
244245
} as never);
245246

246247
const file = new File(['content'], 'my-audio.ogg', { type: 'audio/ogg' });
247-
await convertFile(MOCK_YAML, file, 'download');
248+
await convertFile(MOCK_YAML, [{ field: 'media', file }], 'download');
248249

249250
expect(mockLink.download).toBe('my-audio_converted.wav');
250251
});
@@ -262,7 +263,7 @@ describe('converter service', () => {
262263
});
263264
});
264265

265-
const resultPromise = convertFile(MOCK_YAML, MOCK_FILE, 'playback', abortController.signal);
266+
const resultPromise = convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback', abortController.signal);
266267
abortController.abort();
267268

268269
const result = await resultPromise;
@@ -280,7 +281,7 @@ describe('converter service', () => {
280281
blob: vi.fn().mockResolvedValue(new Blob()),
281282
} as never);
282283

283-
await convertFile(MOCK_YAML, MOCK_FILE, 'download', abortController.signal);
284+
await convertFile(MOCK_YAML, MOCK_UPLOAD, 'download', abortController.signal);
284285

285286
expect(fetch).toHaveBeenCalledWith(
286287
'http://localhost:4545/api/v1/process',
@@ -300,7 +301,7 @@ describe('converter service', () => {
300301
text: vi.fn().mockResolvedValue('Invalid pipeline configuration'),
301302
} as never);
302303

303-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
304+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
304305

305306
expect(result.success).toBe(false);
306307
expect(result.error).toContain('Bad Request');
@@ -310,7 +311,7 @@ describe('converter service', () => {
310311
it('should handle network errors', async () => {
311312
(fetch as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Network error'));
312313

313-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
314+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
314315

315316
expect(result.success).toBe(false);
316317
expect(result.error).toContain('Network error');
@@ -319,7 +320,7 @@ describe('converter service', () => {
319320
it('should handle unknown errors', async () => {
320321
(fetch as ReturnType<typeof vi.fn>).mockRejectedValue('Unknown error');
321322

322-
const result = await convertFile(MOCK_YAML, MOCK_FILE, 'playback');
323+
const result = await convertFile(MOCK_YAML, MOCK_UPLOAD, 'playback');
323324

324325
expect(result.success).toBe(false);
325326
expect(result.error).toBe('Unknown error occurred');
@@ -334,7 +335,7 @@ describe('converter service', () => {
334335
blob: vi.fn().mockResolvedValue(new Blob()),
335336
} as never);
336337

337-
await convertFile(MOCK_YAML, MOCK_FILE, 'download');
338+
await convertFile(MOCK_YAML, MOCK_UPLOAD, 'download');
338339

339340
expect(fetch).toHaveBeenCalledWith(
340341
'http://localhost:4545/api/v1/process',

ui/src/services/converter.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,11 @@ async function handleDownload(
183183
* @param signal - Optional AbortSignal to cancel the request
184184
* @returns A promise that resolves when the conversion is complete
185185
*/
186+
export type UploadField = { field: string; file: File };
187+
186188
export async function convertFile(
187189
pipelineYaml: string,
188-
mediaFile: File | null,
190+
uploads: UploadField[] | null,
189191
mode: OutputMode = 'download',
190192
signal?: AbortSignal,
191193
options?: ConvertFileOptions
@@ -195,15 +197,17 @@ export async function convertFile(
195197
const formData = new FormData();
196198
formData.append('config', new Blob([pipelineYaml], { type: 'text/yaml' }));
197199

198-
// Only append media file if provided (not needed for asset-based pipelines)
199-
if (mediaFile) {
200-
formData.append('media', mediaFile);
200+
// Append uploads (multi-field allowed)
201+
const files = uploads ?? [];
202+
for (const upload of files) {
203+
formData.append(upload.field, upload.file);
201204
}
202205

203206
// Determine the API URL
204207
logger.info('Starting conversion:', {
205-
fileName: mediaFile?.name || '(asset-based)',
206-
fileSize: mediaFile?.size || 0,
208+
uploads: files.length,
209+
fileNames: files.map((f) => f.file.name),
210+
fileSizes: files.map((f) => f.file.size),
207211
pipelineLength: pipelineYaml.length,
208212
});
209213

@@ -244,7 +248,9 @@ export async function convertFile(
244248
}
245249

246250
// Handle download mode
247-
return handleDownload(response, contentType, mediaFile);
251+
// Use first upload to infer output naming when possible
252+
const primaryFile = files[0]?.file ?? null;
253+
return handleDownload(response, contentType, primaryFile);
248254
} catch (error) {
249255
logger.error('Conversion error:', error);
250256
return {

0 commit comments

Comments
 (0)