@lobehub/chat 1.114.5 → 1.115.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/project-introduce.mdc +1 -15
- package/.cursor/rules/project-structure.mdc +227 -0
- package/.cursor/rules/testing-guide/db-model-test.mdc +5 -3
- package/.cursor/rules/testing-guide/testing-guide.mdc +153 -168
- package/.github/workflows/claude.yml +1 -1
- package/.github/workflows/test.yml +9 -0
- package/.prettierignore +0 -1
- package/.vscode/settings.json +86 -80
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +11 -27
- package/changelog/v1.json +10 -0
- package/docs/development/basic/feature-development.mdx +1 -1
- package/docs/development/basic/feature-development.zh-CN.mdx +1 -1
- package/package.json +5 -5
- package/packages/const/src/image.ts +28 -0
- package/packages/const/src/index.ts +1 -0
- package/packages/database/package.json +4 -2
- package/packages/database/src/repositories/aiInfra/index.ts +1 -1
- package/packages/database/tests/setup-db.ts +3 -0
- package/packages/database/vitest.config.mts +33 -0
- package/packages/model-runtime/src/utils/modelParse.ts +1 -1
- package/packages/utils/src/client/imageDimensions.test.ts +95 -0
- package/packages/utils/src/client/imageDimensions.ts +54 -0
- package/packages/utils/src/number.test.ts +3 -1
- package/packages/utils/src/number.ts +1 -2
- package/src/app/[variants]/(main)/files/[id]/page.tsx +0 -2
- package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +206 -185
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +16 -4
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +52 -3
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +33 -19
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +40 -12
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useAutoDimensions.ts +56 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useUploadFilesValidation.ts +77 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +82 -5
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/dimensionConstraints.test.ts +235 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/imageValidation.test.ts +401 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/dimensionConstraints.ts +54 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/imageValidation.ts +117 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +3 -1
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +15 -2
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +5 -4
- package/src/libs/standard-parameters/index.ts +4 -1
- package/src/locales/default/components.ts +8 -0
- package/src/server/services/generation/index.ts +1 -1
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +29 -29
- package/src/store/aiInfra/slices/aiProvider/action.ts +80 -36
- package/src/store/chat/slices/builtinTool/actions/dalle.test.ts +20 -13
- package/src/store/file/slices/upload/action.ts +18 -7
- package/src/store/image/slices/generationConfig/hooks.ts +11 -1
- package/tsconfig.json +1 -10
- package/packages/const/src/imageGeneration.ts +0 -16
- package/src/app/(backend)/trpc/desktop/[trpc]/route.ts +0 -26
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +0 -24
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +0 -15
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +0 -91
- package/src/app/desktop/devtools/page.tsx +0 -89
- package/src/app/desktop/layout.tsx +0 -31
- /package/apps/desktop/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/database/{vitest.config.ts → vitest.config.server.mts} +0 -0
- /package/packages/electron-server-ipc/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/file-loaders/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/model-runtime/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/prompts/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/utils/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/packages/web-crawler/{vitest.config.ts → vitest.config.mts} +0 -0
- /package/{vitest.config.ts → vitest.config.mts} +0 -0
@@ -1,6 +1,8 @@
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
2
2
|
|
3
|
-
import { MAX_SEED
|
3
|
+
import { MAX_SEED } from '@/const/image';
|
4
|
+
|
5
|
+
import { calculateThumbnailDimensions, generateUniqueSeeds } from './number';
|
4
6
|
|
5
7
|
describe('number utilities', () => {
|
6
8
|
describe('MAX_SEED constant', () => {
|
@@ -1,8 +1,7 @@
|
|
1
1
|
import prand from 'pure-rand';
|
2
2
|
|
3
|
-
import { IMAGE_GENERATION_CONFIG } from '@/const/
|
3
|
+
import { IMAGE_GENERATION_CONFIG, MAX_SEED } from '@/const/image';
|
4
4
|
|
5
|
-
export const MAX_SEED = 2 ** 31 - 1;
|
6
5
|
export function generateUniqueSeeds(seedCount: number): number[] {
|
7
6
|
// Use current timestamp as the initial seed
|
8
7
|
const initialSeed = Date.now();
|
@@ -6,7 +6,7 @@ import { CSSProperties, memo, useCallback } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
8
8
|
|
9
|
-
import { MAX_SEED } from '@/
|
9
|
+
import { MAX_SEED } from '@/const/image';
|
10
10
|
import { generateUniqueSeeds } from '@/utils/number';
|
11
11
|
|
12
12
|
export interface SeedNumberInputProps {
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx
CHANGED
@@ -12,14 +12,20 @@ import { useFileStore } from '@/store/file';
|
|
12
12
|
import { FileUploadStatus } from '@/types/files/upload';
|
13
13
|
|
14
14
|
import { useDragAndDrop } from '../hooks/useDragAndDrop';
|
15
|
+
import { useUploadFilesValidation } from '../hooks/useUploadFilesValidation';
|
15
16
|
import { useConfigPanelStyles } from '../style';
|
16
17
|
|
17
18
|
// ======== Business Types ======== //
|
18
19
|
|
19
20
|
export interface ImageUploadProps {
|
20
|
-
// Callback when URL changes
|
21
|
+
// Callback when URL changes - supports both old API (string) and new API (object with dimensions)
|
21
22
|
className?: string; // Image URL
|
22
|
-
|
23
|
+
maxFileSize?: number;
|
24
|
+
onChange?: (
|
25
|
+
data?:
|
26
|
+
| string // Old API: just URL
|
27
|
+
| { dimensions?: { height: number, width: number; }, url: string; }, // New API: URL with dimensions
|
28
|
+
) => void;
|
23
29
|
style?: React.CSSProperties;
|
24
30
|
value?: string | null;
|
25
31
|
}
|
@@ -414,212 +420,227 @@ SuccessDisplay.displayName = 'SuccessDisplay';
|
|
414
420
|
|
415
421
|
// ======== Main Component ======== //
|
416
422
|
|
417
|
-
const ImageUpload: FC<ImageUploadProps> = memo(
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
423
|
+
const ImageUpload: FC<ImageUploadProps> = memo(
|
424
|
+
({ value, onChange, style, className, maxFileSize }) => {
|
425
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
426
|
+
const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
|
427
|
+
const [uploadState, setUploadState] = useState<UploadState | null>(null);
|
428
|
+
const { t } = useTranslation('components');
|
429
|
+
const { message } = App.useApp();
|
430
|
+
const { validateFiles } = useUploadFilesValidation(undefined, maxFileSize);
|
431
|
+
|
432
|
+
// Cleanup blob URLs to prevent memory leaks
|
433
|
+
useEffect(() => {
|
434
|
+
return () => {
|
435
|
+
if (uploadState?.previewUrl && isLocalBlobUrl(uploadState.previewUrl)) {
|
436
|
+
URL.revokeObjectURL(uploadState.previewUrl);
|
437
|
+
}
|
438
|
+
};
|
439
|
+
}, [uploadState?.previewUrl]);
|
440
|
+
|
441
|
+
const handleFileSelect = () => {
|
442
|
+
inputRef.current?.click();
|
430
443
|
};
|
431
|
-
}, [uploadState?.previewUrl]);
|
432
444
|
|
433
|
-
|
434
|
-
|
435
|
-
|
445
|
+
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
446
|
+
const file = event.target.files?.[0];
|
447
|
+
if (!file) return;
|
436
448
|
|
437
|
-
|
438
|
-
|
439
|
-
if (!file) return;
|
449
|
+
// Validate file using unified validation hook
|
450
|
+
if (!validateFiles([file])) return;
|
440
451
|
|
441
|
-
|
442
|
-
|
452
|
+
// Create preview URL
|
453
|
+
const previewUrl = URL.createObjectURL(file);
|
443
454
|
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
});
|
450
|
-
|
451
|
-
try {
|
452
|
-
// Start upload
|
453
|
-
const result = await uploadWithProgress({
|
454
|
-
file,
|
455
|
-
onStatusUpdate: (updateData) => {
|
456
|
-
if (updateData.type === 'updateFile') {
|
457
|
-
setUploadState((prev) => {
|
458
|
-
if (!prev) return null;
|
459
|
-
|
460
|
-
const fileStatus = updateData.value.status;
|
461
|
-
if (!fileStatus) return prev;
|
462
|
-
|
463
|
-
return {
|
464
|
-
...prev,
|
465
|
-
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
466
|
-
progress: updateData.value.uploadState?.progress || 0,
|
467
|
-
status: fileStatus,
|
468
|
-
};
|
469
|
-
});
|
470
|
-
} else if (updateData.type === 'removeFile') {
|
471
|
-
// Handle file removal
|
472
|
-
setUploadState(null);
|
473
|
-
}
|
474
|
-
},
|
475
|
-
skipCheckFileType: true,
|
455
|
+
// Set initial upload state
|
456
|
+
setUploadState({
|
457
|
+
previewUrl,
|
458
|
+
progress: 0,
|
459
|
+
status: 'pending',
|
476
460
|
});
|
477
461
|
|
478
|
-
|
479
|
-
//
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
462
|
+
try {
|
463
|
+
// Start upload
|
464
|
+
const result = await uploadWithProgress({
|
465
|
+
file,
|
466
|
+
onStatusUpdate: (updateData) => {
|
467
|
+
if (updateData.type === 'updateFile') {
|
468
|
+
setUploadState((prev) => {
|
469
|
+
if (!prev) return null;
|
470
|
+
|
471
|
+
const fileStatus = updateData.value.status;
|
472
|
+
if (!fileStatus) return prev;
|
473
|
+
|
474
|
+
return {
|
475
|
+
...prev,
|
476
|
+
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
477
|
+
progress: updateData.value.uploadState?.progress || 0,
|
478
|
+
status: fileStatus,
|
479
|
+
};
|
480
|
+
});
|
481
|
+
} else if (updateData.type === 'removeFile') {
|
482
|
+
// Handle file removal
|
483
|
+
setUploadState(null);
|
490
484
|
}
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
485
|
+
},
|
486
|
+
skipCheckFileType: true,
|
487
|
+
});
|
488
|
+
|
489
|
+
if (result?.url) {
|
490
|
+
// Upload successful - pass dimensions if available
|
491
|
+
const callbackData = result.dimensions
|
492
|
+
? { dimensions: result.dimensions, url: result.url }
|
493
|
+
: result.url;
|
494
|
+
onChange?.(callbackData);
|
495
|
+
}
|
496
|
+
} catch {
|
497
|
+
// Upload failed
|
498
|
+
setUploadState((prev) =>
|
499
|
+
prev
|
500
|
+
? {
|
501
|
+
...prev,
|
502
|
+
error: 'Upload failed',
|
503
|
+
status: 'error',
|
504
|
+
}
|
505
|
+
: null,
|
506
|
+
);
|
507
|
+
} finally {
|
508
|
+
// Cleanup
|
509
|
+
if (isLocalBlobUrl(previewUrl)) {
|
510
|
+
URL.revokeObjectURL(previewUrl);
|
511
|
+
}
|
512
|
+
|
513
|
+
// Clear upload state after a delay to show completion
|
514
|
+
setTimeout(() => {
|
515
|
+
setUploadState(null);
|
516
|
+
}, 1000);
|
497
517
|
}
|
518
|
+
};
|
498
519
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
}, 1000);
|
503
|
-
}
|
504
|
-
};
|
505
|
-
|
506
|
-
const handleDelete = () => {
|
507
|
-
onChange?.(undefined);
|
508
|
-
};
|
509
|
-
|
510
|
-
const handleDrop = async (files: File[]) => {
|
511
|
-
// Show warning if multiple files detected
|
512
|
-
if (files.length > 1) {
|
513
|
-
message.warning(t('ImageUpload.actions.dropMultipleFiles'));
|
514
|
-
}
|
515
|
-
|
516
|
-
// Take the first image file
|
517
|
-
const file = files[0];
|
520
|
+
const handleDelete = () => {
|
521
|
+
onChange?.(undefined);
|
522
|
+
};
|
518
523
|
|
519
|
-
|
520
|
-
|
524
|
+
const handleDrop = async (files: File[]) => {
|
525
|
+
// Show warning if multiple files detected
|
526
|
+
if (files.length > 1) {
|
527
|
+
message.warning(t('ImageUpload.actions.dropMultipleFiles'));
|
528
|
+
}
|
521
529
|
|
522
|
-
|
523
|
-
|
524
|
-
previewUrl,
|
525
|
-
progress: 0,
|
526
|
-
status: 'pending',
|
527
|
-
});
|
530
|
+
// Take the first image file
|
531
|
+
const file = files[0];
|
528
532
|
|
529
|
-
|
530
|
-
|
531
|
-
const result = await uploadWithProgress({
|
532
|
-
file,
|
533
|
-
onStatusUpdate: (updateData) => {
|
534
|
-
if (updateData.type === 'updateFile') {
|
535
|
-
setUploadState((prev) => {
|
536
|
-
if (!prev) return null;
|
533
|
+
// Validate file using unified validation hook
|
534
|
+
if (!validateFiles([file])) return;
|
537
535
|
|
538
|
-
|
539
|
-
|
536
|
+
// Create preview URL
|
537
|
+
const previewUrl = URL.createObjectURL(file);
|
540
538
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
};
|
547
|
-
});
|
548
|
-
} else if (updateData.type === 'removeFile') {
|
549
|
-
setUploadState(null);
|
550
|
-
}
|
551
|
-
},
|
552
|
-
skipCheckFileType: true,
|
539
|
+
// Set initial upload state
|
540
|
+
setUploadState({
|
541
|
+
previewUrl,
|
542
|
+
progress: 0,
|
543
|
+
status: 'pending',
|
553
544
|
});
|
554
545
|
|
555
|
-
|
556
|
-
//
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
546
|
+
try {
|
547
|
+
// Start upload using the same logic as handleFileChange
|
548
|
+
const result = await uploadWithProgress({
|
549
|
+
file,
|
550
|
+
onStatusUpdate: (updateData) => {
|
551
|
+
if (updateData.type === 'updateFile') {
|
552
|
+
setUploadState((prev) => {
|
553
|
+
if (!prev) return null;
|
554
|
+
|
555
|
+
const fileStatus = updateData.value.status;
|
556
|
+
if (!fileStatus) return prev;
|
557
|
+
|
558
|
+
return {
|
559
|
+
...prev,
|
560
|
+
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
561
|
+
progress: updateData.value.uploadState?.progress || 0,
|
562
|
+
status: fileStatus,
|
563
|
+
};
|
564
|
+
});
|
565
|
+
} else if (updateData.type === 'removeFile') {
|
566
|
+
setUploadState(null);
|
567
567
|
}
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
568
|
+
},
|
569
|
+
skipCheckFileType: true,
|
570
|
+
});
|
571
|
+
|
572
|
+
if (result?.url) {
|
573
|
+
// Upload successful - pass dimensions if available
|
574
|
+
const callbackData = result.dimensions
|
575
|
+
? { dimensions: result.dimensions, url: result.url }
|
576
|
+
: result.url;
|
577
|
+
onChange?.(callbackData);
|
578
|
+
}
|
579
|
+
} catch {
|
580
|
+
// Upload failed
|
581
|
+
setUploadState((prev) =>
|
582
|
+
prev
|
583
|
+
? {
|
584
|
+
...prev,
|
585
|
+
error: 'Upload failed',
|
586
|
+
status: 'error',
|
587
|
+
}
|
588
|
+
: null,
|
589
|
+
);
|
590
|
+
} finally {
|
591
|
+
// Cleanup
|
592
|
+
if (isLocalBlobUrl(previewUrl)) {
|
593
|
+
URL.revokeObjectURL(previewUrl);
|
594
|
+
}
|
595
|
+
|
596
|
+
// Clear upload state after a delay to show completion
|
597
|
+
setTimeout(() => {
|
598
|
+
setUploadState(null);
|
599
|
+
}, 1000);
|
574
600
|
}
|
601
|
+
};
|
575
602
|
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
}
|
581
|
-
};
|
582
|
-
|
583
|
-
const { isDragOver, dragHandlers } = useDragAndDrop({
|
584
|
-
accept: 'image/*',
|
585
|
-
onDrop: handleDrop,
|
586
|
-
});
|
587
|
-
|
588
|
-
// Determine which view to render
|
589
|
-
const hasImage = Boolean(value);
|
590
|
-
const isUploading = Boolean(uploadState);
|
603
|
+
const { isDragOver, dragHandlers } = useDragAndDrop({
|
604
|
+
accept: 'image/*',
|
605
|
+
onDrop: handleDrop,
|
606
|
+
});
|
591
607
|
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
<input
|
596
|
-
accept="image/*"
|
597
|
-
onChange={handleFileChange}
|
598
|
-
onClick={(e) => {
|
599
|
-
// Reset value to allow re-selecting the same file
|
600
|
-
e.currentTarget.value = '';
|
601
|
-
}}
|
602
|
-
ref={inputRef}
|
603
|
-
style={{ display: 'none' }}
|
604
|
-
type="file"
|
605
|
-
/>
|
608
|
+
// Determine which view to render
|
609
|
+
const hasImage = Boolean(value);
|
610
|
+
const isUploading = Boolean(uploadState);
|
606
611
|
|
607
|
-
|
608
|
-
{
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
612
|
+
return (
|
613
|
+
<div className={className} {...dragHandlers} style={style}>
|
614
|
+
{/* Hidden file input */}
|
615
|
+
<input
|
616
|
+
accept="image/*"
|
617
|
+
onChange={handleFileChange}
|
618
|
+
onClick={(e) => {
|
619
|
+
// Reset value to allow re-selecting the same file
|
620
|
+
e.currentTarget.value = '';
|
621
|
+
}}
|
622
|
+
ref={inputRef}
|
623
|
+
style={{ display: 'none' }}
|
624
|
+
type="file"
|
616
625
|
/>
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
626
|
+
|
627
|
+
{/* Conditional rendering based on state */}
|
628
|
+
{isUploading && uploadState ? (
|
629
|
+
<UploadingDisplay previewUrl={uploadState.previewUrl} progress={uploadState.progress} />
|
630
|
+
) : hasImage ? (
|
631
|
+
<SuccessDisplay
|
632
|
+
imageUrl={value!}
|
633
|
+
isDragOver={isDragOver}
|
634
|
+
onChangeImage={handleFileSelect}
|
635
|
+
onDelete={handleDelete}
|
636
|
+
/>
|
637
|
+
) : (
|
638
|
+
<Placeholder isDragOver={isDragOver} onClick={handleFileSelect} />
|
639
|
+
)}
|
640
|
+
</div>
|
641
|
+
);
|
642
|
+
},
|
643
|
+
);
|
623
644
|
|
624
645
|
ImageUpload.displayName = 'ImageUpload';
|
625
646
|
|
@@ -2,17 +2,29 @@ import { memo } from 'react';
|
|
2
2
|
|
3
3
|
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
4
4
|
|
5
|
+
import { useAutoDimensions } from '../hooks/useAutoDimensions';
|
5
6
|
import ImageUpload from './ImageUpload';
|
6
7
|
|
7
8
|
const ImageUrl = memo(() => {
|
8
|
-
const { value: imageUrl, setValue } = useGenerationConfigParam('imageUrl');
|
9
|
+
const { value: imageUrl, setValue, maxFileSize } = useGenerationConfigParam('imageUrl');
|
10
|
+
const { autoSetDimensions, extractUrlAndDimensions } = useAutoDimensions();
|
11
|
+
|
12
|
+
const handleChange = (
|
13
|
+
data?:
|
14
|
+
| string // Old API: just URL
|
15
|
+
| { dimensions?: { height: number; width: number }; url: string }, // New API: URL with dimensions
|
16
|
+
) => {
|
17
|
+
const { url, dimensions } = extractUrlAndDimensions(data);
|
9
18
|
|
10
|
-
// Extract the first URL from the array for single image display
|
11
|
-
const handleChange = (url?: string) => {
|
12
19
|
setValue(url ?? null);
|
20
|
+
|
21
|
+
// Auto-set dimensions if available
|
22
|
+
if (dimensions) {
|
23
|
+
autoSetDimensions(dimensions);
|
24
|
+
}
|
13
25
|
};
|
14
26
|
|
15
|
-
return <ImageUpload onChange={handleChange} value={imageUrl} />;
|
27
|
+
return <ImageUpload maxFileSize={maxFileSize} onChange={handleChange} value={imageUrl} />;
|
16
28
|
});
|
17
29
|
|
18
30
|
export default ImageUrl;
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx
CHANGED
@@ -2,18 +2,67 @@ import { memo } from 'react';
|
|
2
2
|
|
3
3
|
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
4
4
|
|
5
|
+
import { useAutoDimensions } from '../hooks/useAutoDimensions';
|
6
|
+
import ImageUpload from './ImageUpload';
|
5
7
|
import MultiImagesUpload from './MultiImagesUpload';
|
6
8
|
|
7
9
|
const ImageUrlsUpload = memo(() => {
|
8
|
-
const { value, setValue } = useGenerationConfigParam('imageUrls');
|
10
|
+
const { value, setValue, maxCount, maxFileSize } = useGenerationConfigParam('imageUrls');
|
11
|
+
const { autoSetDimensions, extractUrlAndDimensions } = useAutoDimensions();
|
12
|
+
|
13
|
+
// When maxCount is 1, use ImageUpload for single image upload
|
14
|
+
if (maxCount === 1) {
|
15
|
+
const handleSingleChange = (
|
16
|
+
data?:
|
17
|
+
| string // Old API: just URL
|
18
|
+
| { dimensions?: { height: number; width: number }; url: string }, // New API: URL with dimensions
|
19
|
+
) => {
|
20
|
+
const { url, dimensions } = extractUrlAndDimensions(data);
|
21
|
+
|
22
|
+
setValue(url ? [url] : []);
|
23
|
+
|
24
|
+
// Auto-set dimensions if available
|
25
|
+
if (dimensions) {
|
26
|
+
autoSetDimensions(dimensions);
|
27
|
+
}
|
28
|
+
};
|
29
|
+
|
30
|
+
return (
|
31
|
+
<ImageUpload
|
32
|
+
maxFileSize={maxFileSize}
|
33
|
+
onChange={handleSingleChange}
|
34
|
+
value={value?.[0] ?? null}
|
35
|
+
/>
|
36
|
+
);
|
37
|
+
}
|
38
|
+
|
39
|
+
// Otherwise use MultiImagesUpload for multiple images
|
40
|
+
const handleChange = (
|
41
|
+
data:
|
42
|
+
| string[] // Old API: just URLs
|
43
|
+
| { dimensions?: { height: number; width: number }; urls: string[] }, // New API: URLs with first image dimensions
|
44
|
+
) => {
|
45
|
+
const urls = Array.isArray(data) ? data : data.urls;
|
46
|
+
const dimensions = Array.isArray(data) ? undefined : data.dimensions;
|
9
47
|
|
10
|
-
const handleChange = (urls: string[]) => {
|
11
48
|
// Directly set the URLs to the store
|
12
49
|
// The store will handle URL to path conversion when needed
|
13
50
|
setValue(urls);
|
51
|
+
|
52
|
+
// Only auto-set dimensions if no existing images and only uploading one image
|
53
|
+
if (!value?.length && urls.length === 1 && dimensions) {
|
54
|
+
autoSetDimensions(dimensions);
|
55
|
+
}
|
14
56
|
};
|
15
57
|
|
16
|
-
return
|
58
|
+
return (
|
59
|
+
<MultiImagesUpload
|
60
|
+
maxCount={maxCount}
|
61
|
+
maxFileSize={maxFileSize}
|
62
|
+
onChange={handleChange}
|
63
|
+
value={value}
|
64
|
+
/>
|
65
|
+
);
|
17
66
|
});
|
18
67
|
|
19
68
|
export default ImageUrlsUpload;
|