@lobehub/chat 1.100.2 → 1.101.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/CHANGELOG.md +33 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/changelog/v1.json +12 -0
- package/docs/development/basic/add-new-ai-image-model.mdx +36 -0
- package/docs/development/basic/add-new-ai-image-model.zh-CN.mdx +0 -0
- package/locales/ar/components.json +5 -4
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/components.json +5 -4
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/components.json +5 -4
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/components.json +5 -4
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/components.json +5 -4
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/components.json +5 -4
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/components.json +5 -4
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/components.json +5 -4
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/components.json +5 -4
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/components.json +5 -4
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/components.json +5 -4
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/components.json +5 -4
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/components.json +5 -4
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/components.json +5 -4
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/components.json +5 -4
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/components.json +5 -4
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/components.json +5 -4
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/components.json +5 -4
- package/locales/zh-TW/models.json +3 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +24 -9
- 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 +161 -39
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +10 -10
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +153 -83
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +2 -11
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useDragAndDrop.ts +71 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/style.ts +17 -0
- package/src/config/aiModels/zhipu.ts +29 -6
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +20 -3
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +20 -11
- package/src/locales/default/components.ts +4 -3
@@ -12,6 +12,8 @@ import { useFileStore } from '@/store/file';
|
|
12
12
|
import { FileUploadStatus } from '@/types/files/upload';
|
13
13
|
|
14
14
|
import { CONFIG_PANEL_WIDTH } from '../../constants';
|
15
|
+
import { useDragAndDrop } from '../../hooks/useDragAndDrop';
|
16
|
+
import { useConfigPanelStyles } from '../../style';
|
15
17
|
import ImageManageModal, { type ImageItem } from './ImageManageModal';
|
16
18
|
|
17
19
|
// ======== Business Types ======== //
|
@@ -105,6 +107,14 @@ const useStyles = createStyles(({ css, token }) => {
|
|
105
107
|
width: 100%;
|
106
108
|
height: ${thumbnailSize}px;
|
107
109
|
padding: 0;
|
110
|
+
border-radius: ${token.borderRadiusLG}px;
|
111
|
+
|
112
|
+
transition: all 0.2s ease;
|
113
|
+
|
114
|
+
&.drag-over {
|
115
|
+
transform: scale(1.02);
|
116
|
+
background: ${token.colorPrimaryBg};
|
117
|
+
}
|
108
118
|
`,
|
109
119
|
|
110
120
|
moreOverlay: css`
|
@@ -139,6 +149,12 @@ const useStyles = createStyles(({ css, token }) => {
|
|
139
149
|
border-color: ${token.colorPrimary};
|
140
150
|
background: ${token.colorFillSecondary};
|
141
151
|
}
|
152
|
+
|
153
|
+
&.drag-over {
|
154
|
+
transform: scale(1.02);
|
155
|
+
border-color: ${token.colorPrimary};
|
156
|
+
background: ${token.colorPrimaryBg};
|
157
|
+
}
|
142
158
|
`,
|
143
159
|
|
144
160
|
placeholderIcon: css`
|
@@ -167,6 +183,11 @@ const useStyles = createStyles(({ css, token }) => {
|
|
167
183
|
background: ${token.colorFillSecondary};
|
168
184
|
|
169
185
|
transition: all 0.2s ease;
|
186
|
+
|
187
|
+
&.drag-over {
|
188
|
+
transform: scale(1.02);
|
189
|
+
background: ${token.colorPrimaryBg};
|
190
|
+
}
|
170
191
|
`,
|
171
192
|
|
172
193
|
progressPrimary: css`
|
@@ -197,6 +218,8 @@ const useStyles = createStyles(({ css, token }) => {
|
|
197
218
|
|
198
219
|
background: ${token.colorBgContainer};
|
199
220
|
|
221
|
+
transition: all 0.2s ease;
|
222
|
+
|
200
223
|
&:hover .upload-more-overlay {
|
201
224
|
opacity: 1;
|
202
225
|
}
|
@@ -204,6 +227,11 @@ const useStyles = createStyles(({ css, token }) => {
|
|
204
227
|
&:hover .delete-icon {
|
205
228
|
opacity: 1;
|
206
229
|
}
|
230
|
+
|
231
|
+
&.drag-over {
|
232
|
+
transform: scale(1.02);
|
233
|
+
background: ${token.colorPrimaryBg};
|
234
|
+
}
|
207
235
|
`,
|
208
236
|
uploadMoreButton: css`
|
209
237
|
cursor: pointer;
|
@@ -256,15 +284,21 @@ const isLocalBlobUrl = (url: string): boolean => url.startsWith('blob:');
|
|
256
284
|
// ======== Sub-Components ======== //
|
257
285
|
|
258
286
|
interface ImageUploadPlaceholderProps {
|
287
|
+
isDragOver?: boolean;
|
259
288
|
onClick?: () => void;
|
260
289
|
}
|
261
290
|
|
262
|
-
const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ onClick }) => {
|
291
|
+
const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ isDragOver, onClick }) => {
|
263
292
|
const { styles } = useStyles();
|
264
293
|
const { t } = useTranslation('components');
|
265
294
|
|
266
295
|
return (
|
267
|
-
<Center
|
296
|
+
<Center
|
297
|
+
className={`${styles.placeholder} ${isDragOver ? 'drag-over' : ''}`}
|
298
|
+
gap={16}
|
299
|
+
horizontal={false}
|
300
|
+
onClick={onClick}
|
301
|
+
>
|
268
302
|
<ImageIcon className={styles.placeholderIcon} size={48} strokeWidth={1.5} />
|
269
303
|
<div className={styles.placeholderText}>
|
270
304
|
{t('MultiImagesUpload.placeholder.primary')}
|
@@ -408,104 +442,117 @@ ImageUploadProgress.displayName = 'ImageUploadProgress';
|
|
408
442
|
|
409
443
|
interface ImageThumbnailsProps {
|
410
444
|
images: string[];
|
445
|
+
isDragOver?: boolean;
|
411
446
|
onClick?: () => void;
|
412
447
|
onDelete?: (index: number) => void;
|
413
448
|
}
|
414
449
|
|
415
|
-
const ImageThumbnails: FC<ImageThumbnailsProps> = memo(
|
416
|
-
|
450
|
+
const ImageThumbnails: FC<ImageThumbnailsProps> = memo(
|
451
|
+
({ images, isDragOver, onClick, onDelete }) => {
|
452
|
+
const { styles } = useStyles();
|
453
|
+
const { styles: configStyles } = useConfigPanelStyles();
|
417
454
|
|
418
|
-
|
419
|
-
|
420
|
-
|
455
|
+
// Display max 4 images, with overflow indication
|
456
|
+
const displayImages = images.slice(0, 4);
|
457
|
+
const remainingCount = Math.max(0, images.length - 3); // Show +x for images beyond first 3
|
421
458
|
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
459
|
+
const handleDelete = (index: number, event: React.MouseEvent) => {
|
460
|
+
event.stopPropagation(); // Prevent triggering container click
|
461
|
+
onDelete?.(index);
|
462
|
+
};
|
426
463
|
|
427
|
-
|
428
|
-
|
429
|
-
|
464
|
+
const renderImageItem = (imageUrl: string, index: number) => {
|
465
|
+
const isLastItem = index === 3;
|
466
|
+
const showOverlay = isLastItem && remainingCount > 1;
|
467
|
+
|
468
|
+
return (
|
469
|
+
<div className={styles.imageItem} key={imageUrl}>
|
470
|
+
<Image
|
471
|
+
alt={`Uploaded image ${index + 1}`}
|
472
|
+
fill
|
473
|
+
src={imageUrl}
|
474
|
+
style={{ objectFit: 'cover' }}
|
475
|
+
unoptimized
|
476
|
+
/>
|
477
|
+
{!showOverlay && (
|
478
|
+
<div
|
479
|
+
className={`${styles.deleteIcon} delete-icon`}
|
480
|
+
onClick={(e) => handleDelete(index, e)}
|
481
|
+
>
|
482
|
+
<X size={12} />
|
483
|
+
</div>
|
484
|
+
)}
|
485
|
+
{showOverlay && <div className={styles.moreOverlay}>+{remainingCount}</div>}
|
486
|
+
</div>
|
487
|
+
);
|
488
|
+
};
|
430
489
|
|
431
490
|
return (
|
432
|
-
<div
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
style={{ objectFit: 'cover' }}
|
438
|
-
unoptimized
|
439
|
-
/>
|
440
|
-
{!showOverlay && (
|
441
|
-
<div
|
442
|
-
className={`${styles.deleteIcon} delete-icon`}
|
443
|
-
onClick={(e) => handleDelete(index, e)}
|
444
|
-
>
|
445
|
-
<X size={12} />
|
446
|
-
</div>
|
447
|
-
)}
|
448
|
-
{showOverlay && <div className={styles.moreOverlay}>+{remainingCount}</div>}
|
491
|
+
<div
|
492
|
+
className={`${styles.imageThumbnails} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
|
493
|
+
onClick={onClick}
|
494
|
+
>
|
495
|
+
{displayImages.map(renderImageItem)}
|
449
496
|
</div>
|
450
497
|
);
|
451
|
-
}
|
452
|
-
|
453
|
-
return (
|
454
|
-
<div className={styles.imageThumbnails} onClick={onClick}>
|
455
|
-
{displayImages.map(renderImageItem)}
|
456
|
-
</div>
|
457
|
-
);
|
458
|
-
});
|
498
|
+
},
|
499
|
+
);
|
459
500
|
|
460
501
|
ImageThumbnails.displayName = 'ImageThumbnails';
|
461
502
|
|
462
503
|
interface SingleImageDisplayProps {
|
463
504
|
imageUrl: string;
|
505
|
+
isDragOver?: boolean;
|
464
506
|
onClick?: () => void;
|
465
507
|
onDelete?: () => void;
|
466
508
|
}
|
467
509
|
|
468
|
-
const SingleImageDisplay: FC<SingleImageDisplayProps> = memo(
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
event.stopPropagation(); // Prevent triggering container click
|
474
|
-
onDelete?.();
|
475
|
-
};
|
510
|
+
const SingleImageDisplay: FC<SingleImageDisplayProps> = memo(
|
511
|
+
({ imageUrl, isDragOver, onClick, onDelete }) => {
|
512
|
+
const { styles } = useStyles();
|
513
|
+
const { styles: configStyles } = useConfigPanelStyles();
|
514
|
+
const { t } = useTranslation('components');
|
476
515
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
516
|
+
const handleDelete = (event: React.MouseEvent) => {
|
517
|
+
event.stopPropagation(); // Prevent triggering container click
|
518
|
+
onDelete?.();
|
519
|
+
};
|
481
520
|
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
fill
|
487
|
-
src={imageUrl}
|
488
|
-
style={{ objectFit: 'contain' }}
|
489
|
-
unoptimized
|
490
|
-
/>
|
491
|
-
|
492
|
-
{/* Delete button */}
|
493
|
-
<div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
|
494
|
-
<X size={12} />
|
495
|
-
</div>
|
521
|
+
const handleOverlayClick = (event: React.MouseEvent) => {
|
522
|
+
event.stopPropagation(); // Prevent triggering container click
|
523
|
+
onClick?.();
|
524
|
+
};
|
496
525
|
|
497
|
-
|
526
|
+
return (
|
498
527
|
<div
|
499
|
-
className={`${styles.
|
500
|
-
onClick={handleOverlayClick}
|
528
|
+
className={`${styles.singleImageDisplay} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
|
501
529
|
>
|
502
|
-
<
|
503
|
-
|
504
|
-
|
530
|
+
<Image
|
531
|
+
alt="Uploaded image"
|
532
|
+
fill
|
533
|
+
src={imageUrl}
|
534
|
+
style={{ objectFit: 'contain' }}
|
535
|
+
unoptimized
|
536
|
+
/>
|
537
|
+
|
538
|
+
{/* Delete button */}
|
539
|
+
<div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
|
540
|
+
<X size={12} />
|
541
|
+
</div>
|
542
|
+
|
543
|
+
{/* Upload more overlay */}
|
544
|
+
<div
|
545
|
+
className={`${styles.uploadMoreOverlay} upload-more-overlay`}
|
546
|
+
onClick={handleOverlayClick}
|
547
|
+
>
|
548
|
+
<button className={styles.uploadMoreButton} type="button">
|
549
|
+
{t('MultiImagesUpload.actions.uploadMore')}
|
550
|
+
</button>
|
551
|
+
</div>
|
505
552
|
</div>
|
506
|
-
|
507
|
-
|
508
|
-
|
553
|
+
);
|
554
|
+
},
|
555
|
+
);
|
509
556
|
|
510
557
|
SingleImageDisplay.displayName = 'SingleImageDisplay';
|
511
558
|
|
@@ -517,6 +564,7 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
|
|
517
564
|
const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
|
518
565
|
const [displayItems, setDisplayItems] = useState<DisplayItem[]>([]);
|
519
566
|
const [modalOpen, setModalOpen] = useState(false);
|
567
|
+
const { styles: configStyles } = useConfigPanelStyles();
|
520
568
|
|
521
569
|
// Cleanup blob URLs to prevent memory leaks
|
522
570
|
useEffect(() => {
|
@@ -656,6 +704,18 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
|
|
656
704
|
await handleFilesSelected(Array.from(files));
|
657
705
|
};
|
658
706
|
|
707
|
+
// ======== Drag and Drop Handlers ======== //
|
708
|
+
|
709
|
+
const handleDrop = async (files: File[]) => {
|
710
|
+
// Add all image files to existing images
|
711
|
+
await handleFilesSelected(files);
|
712
|
+
};
|
713
|
+
|
714
|
+
const { isDragOver, dragHandlers } = useDragAndDrop({
|
715
|
+
accept: 'image/*',
|
716
|
+
onDrop: handleDrop,
|
717
|
+
});
|
718
|
+
|
659
719
|
// 处理 Modal 完成回调
|
660
720
|
const handleModalComplete = async (imageItems: ImageItem[]) => {
|
661
721
|
// 分离现有URL和新文件
|
@@ -685,7 +745,7 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
|
|
685
745
|
const isSingleImage = value && value.length === 1;
|
686
746
|
|
687
747
|
return (
|
688
|
-
<div className={className} style={style}>
|
748
|
+
<div className={className} {...dragHandlers} style={style}>
|
689
749
|
{/* Hidden file input */}
|
690
750
|
<input
|
691
751
|
accept="image/*"
|
@@ -702,22 +762,32 @@ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
|
|
702
762
|
|
703
763
|
{/* Conditional rendering based on state */}
|
704
764
|
{isUploading ? (
|
705
|
-
<
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
765
|
+
<div
|
766
|
+
className={`${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
|
767
|
+
>
|
768
|
+
<ImageUploadProgress
|
769
|
+
completedCount={completedFiles}
|
770
|
+
currentProgress={overallProgress}
|
771
|
+
showCount={totalFiles > 1}
|
772
|
+
totalCount={totalFiles}
|
773
|
+
/>
|
774
|
+
</div>
|
711
775
|
) : isSingleImage ? (
|
712
776
|
<SingleImageDisplay
|
713
777
|
imageUrl={value[0]}
|
778
|
+
isDragOver={isDragOver}
|
714
779
|
onClick={handleOpenModal}
|
715
780
|
onDelete={() => handleDelete(0)}
|
716
781
|
/>
|
717
782
|
) : hasImages ? (
|
718
|
-
<ImageThumbnails
|
783
|
+
<ImageThumbnails
|
784
|
+
images={value || []}
|
785
|
+
isDragOver={isDragOver}
|
786
|
+
onClick={handleOpenModal}
|
787
|
+
onDelete={handleDelete}
|
788
|
+
/>
|
719
789
|
) : (
|
720
|
-
<ImageUploadPlaceholder onClick={handlePlaceholderClick} />
|
790
|
+
<ImageUploadPlaceholder isDragOver={isDragOver} onClick={handlePlaceholderClick} />
|
721
791
|
)}
|
722
792
|
|
723
793
|
{/* Image Management Modal */}
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx
CHANGED
@@ -7,18 +7,9 @@ import InputNumber from '../../../components/SeedNumberInput';
|
|
7
7
|
|
8
8
|
const SeedNumberInput = memo(() => {
|
9
9
|
const { t } = useTranslation('image');
|
10
|
-
const { value, setValue
|
10
|
+
const { value, setValue } = useGenerationConfigParam('seed');
|
11
11
|
|
12
|
-
return (
|
13
|
-
<InputNumber
|
14
|
-
max={max}
|
15
|
-
min={min}
|
16
|
-
onChange={setValue as any}
|
17
|
-
placeholder={t('config.seed.random')}
|
18
|
-
step={step}
|
19
|
-
value={value}
|
20
|
-
/>
|
21
|
-
);
|
12
|
+
return <InputNumber onChange={setValue} placeholder={t('config.seed.random')} value={value} />;
|
22
13
|
});
|
23
14
|
|
24
15
|
export default SeedNumberInput;
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
2
|
+
|
3
|
+
interface UseDragAndDropOptions {
|
4
|
+
accept?: string; // MIME type filter, e.g., 'image/*'
|
5
|
+
onDrop: (files: File[]) => void;
|
6
|
+
}
|
7
|
+
|
8
|
+
export const useDragAndDrop = ({ onDrop, accept = 'image/*' }: UseDragAndDropOptions) => {
|
9
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
10
|
+
const dragCounter = useRef(0);
|
11
|
+
|
12
|
+
const handleDragEnter = (e: React.DragEvent) => {
|
13
|
+
e.preventDefault();
|
14
|
+
e.stopPropagation();
|
15
|
+
|
16
|
+
// Check if dragging files
|
17
|
+
if (e.dataTransfer.types.includes('Files')) {
|
18
|
+
dragCounter.current++;
|
19
|
+
setIsDragOver(true);
|
20
|
+
}
|
21
|
+
};
|
22
|
+
|
23
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
24
|
+
e.preventDefault();
|
25
|
+
e.stopPropagation();
|
26
|
+
|
27
|
+
dragCounter.current--;
|
28
|
+
if (dragCounter.current === 0) {
|
29
|
+
setIsDragOver(false);
|
30
|
+
}
|
31
|
+
};
|
32
|
+
|
33
|
+
const handleDrop = async (e: React.DragEvent) => {
|
34
|
+
e.preventDefault();
|
35
|
+
e.stopPropagation();
|
36
|
+
|
37
|
+
dragCounter.current = 0;
|
38
|
+
setIsDragOver(false);
|
39
|
+
|
40
|
+
const files = Array.from(e.dataTransfer.files);
|
41
|
+
|
42
|
+
// Filter files based on accept type
|
43
|
+
const filteredFiles = files.filter((file) => {
|
44
|
+
if (accept === '*/*') return true;
|
45
|
+
if (accept.endsWith('/*')) {
|
46
|
+
const acceptType = accept.slice(0, -2);
|
47
|
+
return file.type.startsWith(acceptType);
|
48
|
+
}
|
49
|
+
return file.type === accept;
|
50
|
+
});
|
51
|
+
|
52
|
+
if (filteredFiles.length > 0) {
|
53
|
+
onDrop(filteredFiles);
|
54
|
+
}
|
55
|
+
};
|
56
|
+
|
57
|
+
const dragHandlers = {
|
58
|
+
onDragEnter: handleDragEnter,
|
59
|
+
onDragLeave: handleDragLeave,
|
60
|
+
onDragOver: (e: React.DragEvent) => {
|
61
|
+
e.preventDefault();
|
62
|
+
e.stopPropagation();
|
63
|
+
},
|
64
|
+
onDrop: handleDrop,
|
65
|
+
};
|
66
|
+
|
67
|
+
return {
|
68
|
+
dragHandlers,
|
69
|
+
isDragOver,
|
70
|
+
};
|
71
|
+
};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { createStyles } from 'antd-style';
|
2
|
+
|
3
|
+
export const useConfigPanelStyles = createStyles(({ css, token }) => ({
|
4
|
+
dragOver: css`
|
5
|
+
transform: scale(1.02);
|
6
|
+
border-color: ${token.colorPrimary} !important;
|
7
|
+
box-shadow: 0 0 0 2px ${token.colorPrimary}20;
|
8
|
+
transition: transform 0.2s ease;
|
9
|
+
`,
|
10
|
+
|
11
|
+
dragTransition: css`
|
12
|
+
transition:
|
13
|
+
transform 0.2s ease,
|
14
|
+
border-color 0.2s ease,
|
15
|
+
box-shadow 0.2s ease;
|
16
|
+
`,
|
17
|
+
}));
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { AIChatModelCard } from '@/types/aiModel';
|
1
|
+
import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
|
2
2
|
|
3
3
|
const zhipuChatModels: AIChatModelCard[] = [
|
4
4
|
{
|
@@ -8,7 +8,8 @@ const zhipuChatModels: AIChatModelCard[] = [
|
|
8
8
|
vision: true,
|
9
9
|
},
|
10
10
|
contextWindowTokens: 64_000,
|
11
|
-
description:
|
11
|
+
description:
|
12
|
+
'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
|
12
13
|
displayName: 'GLM-4.1V-Thinking-FlashX',
|
13
14
|
id: 'glm-4.1v-thinking-flashx',
|
14
15
|
pricing: {
|
@@ -28,7 +29,8 @@ const zhipuChatModels: AIChatModelCard[] = [
|
|
28
29
|
vision: true,
|
29
30
|
},
|
30
31
|
contextWindowTokens: 64_000,
|
31
|
-
description:
|
32
|
+
description:
|
33
|
+
'GLM-4.1V-Thinking 系列模型是目前已知10B级别的VLM模型中性能最强的视觉模型,融合了同级别SOTA的各项视觉语言任务,包括视频理解、图片问答、学科解题、OCR文字识别、文档和图表解读、GUI Agent、前端网页Coding、Grounding等,多项任务能力甚至超过8倍参数量的Qwen2.5-VL-72B。通过领先的强化学习技术,模型掌握了通过思维链推理的方式提升回答的准确性和丰富度,从最终效果和可解释性等维度都显著超过传统的非thinking模型。',
|
32
34
|
displayName: 'GLM-4.1V-Thinking-Flash',
|
33
35
|
enabled: true,
|
34
36
|
id: 'glm-4.1v-thinking-flash',
|
@@ -103,8 +105,7 @@ const zhipuChatModels: AIChatModelCard[] = [
|
|
103
105
|
search: true,
|
104
106
|
},
|
105
107
|
contextWindowTokens: 128_000,
|
106
|
-
description:
|
107
|
-
'高速低价:Flash增强版本,超快推理速度,更快并发保障。',
|
108
|
+
description: '高速低价:Flash增强版本,超快推理速度,更快并发保障。',
|
108
109
|
displayName: 'GLM-Z1-FlashX',
|
109
110
|
id: 'glm-z1-flashx',
|
110
111
|
maxOutput: 32_000,
|
@@ -409,6 +410,28 @@ const zhipuChatModels: AIChatModelCard[] = [
|
|
409
410
|
},
|
410
411
|
];
|
411
412
|
|
412
|
-
|
413
|
+
const zhipuImageModels: AIImageModelCard[] = [
|
414
|
+
// https://bigmodel.cn/dev/api/image-model/cogview
|
415
|
+
{
|
416
|
+
description:
|
417
|
+
'CogView-4 是智谱首个支持生成汉字的开源文生图模型,在语义理解、图像生成质量、中英文字生成能力等方面全面提升,支持任意长度的中英双语输入,能够生成在给定范围内的任意分辨率图像。',
|
418
|
+
displayName: 'CogView-4',
|
419
|
+
enabled: true,
|
420
|
+
id: 'cogview-4',
|
421
|
+
parameters: {
|
422
|
+
prompt: {
|
423
|
+
default: '',
|
424
|
+
},
|
425
|
+
size: {
|
426
|
+
default: '1024x1024',
|
427
|
+
enum: ['1024x1024', '768x1344', '864x1152', '1344x768', '1152x864', '1440x720', '720x1440'],
|
428
|
+
},
|
429
|
+
},
|
430
|
+
releasedAt: '2025-03-04',
|
431
|
+
type: 'image',
|
432
|
+
},
|
433
|
+
];
|
434
|
+
|
435
|
+
export const allModels = [...zhipuChatModels, ...zhipuImageModels];
|
413
436
|
|
414
437
|
export default allModels;
|
@@ -1227,9 +1227,26 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
1227
1227
|
);
|
1228
1228
|
});
|
1229
1229
|
|
1230
|
-
it('should
|
1230
|
+
it('should handle url format response successfully', async () => {
|
1231
1231
|
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
|
1232
|
-
data: [{ url: 'https://example.com/image.jpg' }],
|
1232
|
+
data: [{ url: 'https://example.com/generated-image.jpg' }],
|
1233
|
+
} as any);
|
1234
|
+
|
1235
|
+
const payload = {
|
1236
|
+
model: 'dall-e-3',
|
1237
|
+
params: { prompt: 'Test prompt' },
|
1238
|
+
};
|
1239
|
+
|
1240
|
+
const result = await (instance as any).createImage(payload);
|
1241
|
+
|
1242
|
+
expect(result).toEqual({
|
1243
|
+
imageUrl: 'https://example.com/generated-image.jpg',
|
1244
|
+
});
|
1245
|
+
});
|
1246
|
+
|
1247
|
+
it('should throw error when both b64_json and url are missing', async () => {
|
1248
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
|
1249
|
+
data: [{ some_other_field: 'value' }],
|
1233
1250
|
} as any);
|
1234
1251
|
|
1235
1252
|
const payload = {
|
@@ -1238,7 +1255,7 @@ describe('LobeOpenAICompatibleFactory', () => {
|
|
1238
1255
|
};
|
1239
1256
|
|
1240
1257
|
await expect((instance as any).createImage(payload)).rejects.toThrow(
|
1241
|
-
'Invalid image response: missing b64_json
|
1258
|
+
'Invalid image response: missing both b64_json and url fields',
|
1242
1259
|
);
|
1243
1260
|
});
|
1244
1261
|
});
|
@@ -390,21 +390,30 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
390
390
|
throw new Error('Invalid image response: first data item is null or undefined');
|
391
391
|
}
|
392
392
|
|
393
|
-
|
394
|
-
log('Invalid image response: missing b64_json field');
|
395
|
-
throw new Error('Invalid image response: missing b64_json field');
|
396
|
-
}
|
397
|
-
|
398
|
-
// 确定图片的 MIME 类型,默认为 PNG
|
399
|
-
const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
|
393
|
+
let imageUrl: string;
|
400
394
|
|
401
|
-
//
|
402
|
-
|
395
|
+
// 处理 base64 格式的响应
|
396
|
+
if (imageData.b64_json) {
|
397
|
+
// 确定图片的 MIME 类型,默认为 PNG
|
398
|
+
const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
|
403
399
|
|
404
|
-
|
400
|
+
// 将 base64 字符串转换为完整的 data URL
|
401
|
+
imageUrl = `data:${mimeType};base64,${imageData.b64_json}`;
|
402
|
+
log('Successfully converted base64 to data URL, length: %d', imageUrl.length);
|
403
|
+
}
|
404
|
+
// 处理 URL 格式的响应
|
405
|
+
else if (imageData.url) {
|
406
|
+
imageUrl = imageData.url;
|
407
|
+
log('Using direct image URL: %s', imageUrl);
|
408
|
+
}
|
409
|
+
// 如果两种格式都不存在,抛出错误
|
410
|
+
else {
|
411
|
+
log('Invalid image response: missing both b64_json and url fields');
|
412
|
+
throw new Error('Invalid image response: missing both b64_json and url fields');
|
413
|
+
}
|
405
414
|
|
406
415
|
return {
|
407
|
-
imageUrl
|
416
|
+
imageUrl,
|
408
417
|
};
|
409
418
|
}
|
410
419
|
|
@@ -73,10 +73,11 @@ export default {
|
|
73
73
|
ImageUpload: {
|
74
74
|
actions: {
|
75
75
|
changeImage: '点击更换图片',
|
76
|
+
dropMultipleFiles: '不支持多上传多个文件,只会使用第一个文件',
|
76
77
|
},
|
77
78
|
placeholder: {
|
78
79
|
primary: '添加图片',
|
79
|
-
secondary: '
|
80
|
+
secondary: '点击或拖拽上传',
|
80
81
|
},
|
81
82
|
},
|
82
83
|
KeyValueEditor: {
|
@@ -111,7 +112,7 @@ export default {
|
|
111
112
|
},
|
112
113
|
MultiImagesUpload: {
|
113
114
|
actions: {
|
114
|
-
uploadMore: '
|
115
|
+
uploadMore: '点击或拖拽上传更多',
|
115
116
|
},
|
116
117
|
modal: {
|
117
118
|
complete: '完成',
|
@@ -121,7 +122,7 @@ export default {
|
|
121
122
|
upload: '上传图片',
|
122
123
|
},
|
123
124
|
placeholder: {
|
124
|
-
primary: '
|
125
|
+
primary: '点击或拖拽上传图片',
|
125
126
|
secondary: '支持多张图片选择',
|
126
127
|
},
|
127
128
|
progress: {
|