@lobehub/chat 1.100.2 → 1.101.1
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 +58 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/changelog/v1.json +21 -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 +2 -2
- package/src/app/[variants]/(auth)/next-auth/signin/AuthSignInBox.tsx +2 -2
- 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
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
+
import { App } from 'antd';
|
3
4
|
import { createStyles, useTheme } from 'antd-style';
|
4
5
|
import { Image as ImageIcon, X } from 'lucide-react';
|
6
|
+
import Image from 'next/image';
|
5
7
|
import React, { type FC, memo, useEffect, useRef, useState } from 'react';
|
6
8
|
import { useTranslation } from 'react-i18next';
|
7
9
|
import { Center } from 'react-layout-kit';
|
@@ -9,6 +11,9 @@ import { Center } from 'react-layout-kit';
|
|
9
11
|
import { useFileStore } from '@/store/file';
|
10
12
|
import { FileUploadStatus } from '@/types/files/upload';
|
11
13
|
|
14
|
+
import { useDragAndDrop } from '../hooks/useDragAndDrop';
|
15
|
+
import { useConfigPanelStyles } from '../style';
|
16
|
+
|
12
17
|
// ======== Business Types ======== //
|
13
18
|
|
14
19
|
export interface ImageUploadProps {
|
@@ -100,11 +105,6 @@ const useStyles = createStyles(({ css, token }) => {
|
|
100
105
|
background: ${token.colorErrorBg};
|
101
106
|
}
|
102
107
|
`,
|
103
|
-
imageStyle: css`
|
104
|
-
width: 100%;
|
105
|
-
height: 100%;
|
106
|
-
object-fit: cover;
|
107
|
-
`,
|
108
108
|
placeholder: css`
|
109
109
|
cursor: pointer;
|
110
110
|
|
@@ -121,6 +121,12 @@ const useStyles = createStyles(({ css, token }) => {
|
|
121
121
|
border-color: ${token.colorPrimary};
|
122
122
|
background: ${token.colorFillSecondary};
|
123
123
|
}
|
124
|
+
|
125
|
+
&.drag-over {
|
126
|
+
transform: scale(1.02);
|
127
|
+
border-color: ${token.colorPrimary};
|
128
|
+
background: ${token.colorPrimaryBg};
|
129
|
+
}
|
124
130
|
`,
|
125
131
|
placeholderIcon: css`
|
126
132
|
color: ${token.colorTextTertiary};
|
@@ -140,10 +146,13 @@ const useStyles = createStyles(({ css, token }) => {
|
|
140
146
|
|
141
147
|
width: 100%;
|
142
148
|
height: 160px;
|
149
|
+
border: 2px solid transparent;
|
143
150
|
border-radius: ${token.borderRadiusLG}px;
|
144
151
|
|
145
152
|
background: ${token.colorBgContainer};
|
146
153
|
|
154
|
+
transition: all ${token.motionDurationMid} ease;
|
155
|
+
|
147
156
|
&:hover .change-overlay {
|
148
157
|
opacity: 1;
|
149
158
|
}
|
@@ -151,6 +160,12 @@ const useStyles = createStyles(({ css, token }) => {
|
|
151
160
|
&:hover .delete-icon {
|
152
161
|
opacity: 1;
|
153
162
|
}
|
163
|
+
|
164
|
+
&.drag-over {
|
165
|
+
transform: scale(1.02);
|
166
|
+
border-color: ${token.colorPrimary};
|
167
|
+
background: ${token.colorPrimaryBg};
|
168
|
+
}
|
154
169
|
`,
|
155
170
|
uploadingDisplay: css`
|
156
171
|
position: relative;
|
@@ -283,20 +298,27 @@ CircularProgress.displayName = 'CircularProgress';
|
|
283
298
|
* 占位视图组件
|
284
299
|
*/
|
285
300
|
interface PlaceholderProps {
|
301
|
+
isDragOver?: boolean;
|
286
302
|
onClick?: () => void;
|
287
303
|
}
|
288
304
|
|
289
|
-
const Placeholder: FC<PlaceholderProps> = memo(({ onClick }) => {
|
305
|
+
const Placeholder: FC<PlaceholderProps> = memo(({ isDragOver, onClick }) => {
|
290
306
|
const { styles } = useStyles();
|
307
|
+
const { styles: configStyles } = useConfigPanelStyles();
|
291
308
|
const { t } = useTranslation('components');
|
292
309
|
|
293
310
|
return (
|
294
|
-
<Center
|
311
|
+
<Center
|
312
|
+
className={`${styles.placeholder} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
|
313
|
+
gap={16}
|
314
|
+
horizontal={false}
|
315
|
+
onClick={onClick}
|
316
|
+
>
|
295
317
|
<ImageIcon className={styles.placeholderIcon} size={48} strokeWidth={1.5} />
|
296
318
|
<div className={styles.placeholderText}>
|
297
|
-
{t('ImageUpload.placeholder.primary'
|
319
|
+
{t('ImageUpload.placeholder.primary')}
|
298
320
|
<br />
|
299
|
-
{t('ImageUpload.placeholder.secondary'
|
321
|
+
{t('ImageUpload.placeholder.secondary')}
|
300
322
|
</div>
|
301
323
|
</Center>
|
302
324
|
);
|
@@ -317,7 +339,13 @@ const UploadingDisplay: FC<UploadingDisplayProps> = memo(({ previewUrl, progress
|
|
317
339
|
|
318
340
|
return (
|
319
341
|
<div className={styles.uploadingDisplay}>
|
320
|
-
<
|
342
|
+
<Image
|
343
|
+
alt="Uploading preview"
|
344
|
+
fill
|
345
|
+
src={previewUrl}
|
346
|
+
style={{ objectFit: 'cover' }}
|
347
|
+
unoptimized
|
348
|
+
/>
|
321
349
|
<div className={styles.uploadingOverlay}>
|
322
350
|
<CircularProgress value={progress} />
|
323
351
|
</div>
|
@@ -332,42 +360,55 @@ UploadingDisplay.displayName = 'UploadingDisplay';
|
|
332
360
|
*/
|
333
361
|
interface SuccessDisplayProps {
|
334
362
|
imageUrl: string;
|
363
|
+
isDragOver?: boolean;
|
335
364
|
onChangeImage?: () => void;
|
336
365
|
onDelete?: () => void;
|
337
366
|
}
|
338
367
|
|
339
|
-
const SuccessDisplay: FC<SuccessDisplayProps> = memo(
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
event.stopPropagation();
|
345
|
-
onDelete?.();
|
346
|
-
};
|
368
|
+
const SuccessDisplay: FC<SuccessDisplayProps> = memo(
|
369
|
+
({ imageUrl, isDragOver, onDelete, onChangeImage }) => {
|
370
|
+
const { styles } = useStyles();
|
371
|
+
const { styles: configStyles } = useConfigPanelStyles();
|
372
|
+
const { t } = useTranslation('components');
|
347
373
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
374
|
+
const handleDelete = (event: React.MouseEvent) => {
|
375
|
+
event.stopPropagation();
|
376
|
+
onDelete?.();
|
377
|
+
};
|
352
378
|
|
353
|
-
|
354
|
-
|
355
|
-
|
379
|
+
const handleChangeImage = (event: React.MouseEvent) => {
|
380
|
+
event.stopPropagation();
|
381
|
+
onChangeImage?.();
|
382
|
+
};
|
356
383
|
|
357
|
-
|
358
|
-
<div
|
359
|
-
|
360
|
-
|
384
|
+
return (
|
385
|
+
<div
|
386
|
+
className={`${styles.successDisplay} ${configStyles.dragTransition} ${isDragOver ? configStyles.dragOver : ''}`}
|
387
|
+
onClick={onChangeImage}
|
388
|
+
>
|
389
|
+
<Image
|
390
|
+
alt="Uploaded image"
|
391
|
+
fill
|
392
|
+
src={imageUrl}
|
393
|
+
style={{ objectFit: 'cover' }}
|
394
|
+
unoptimized
|
395
|
+
/>
|
361
396
|
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
397
|
+
{/* Delete button */}
|
398
|
+
<div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
|
399
|
+
<X size={14} />
|
400
|
+
</div>
|
401
|
+
|
402
|
+
{/* Change image overlay */}
|
403
|
+
<div className={`${styles.changeOverlay} change-overlay`} onClick={handleChangeImage}>
|
404
|
+
<button className={styles.changeButton} type="button">
|
405
|
+
{t('ImageUpload.actions.changeImage')}
|
406
|
+
</button>
|
407
|
+
</div>
|
367
408
|
</div>
|
368
|
-
|
369
|
-
|
370
|
-
|
409
|
+
);
|
410
|
+
},
|
411
|
+
);
|
371
412
|
|
372
413
|
SuccessDisplay.displayName = 'SuccessDisplay';
|
373
414
|
|
@@ -377,6 +418,8 @@ const ImageUpload: FC<ImageUploadProps> = memo(({ value, onChange, style, classN
|
|
377
418
|
const inputRef = useRef<HTMLInputElement>(null);
|
378
419
|
const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
|
379
420
|
const [uploadState, setUploadState] = useState<UploadState | null>(null);
|
421
|
+
const { t } = useTranslation('components');
|
422
|
+
const { message } = App.useApp();
|
380
423
|
|
381
424
|
// Cleanup blob URLs to prevent memory leaks
|
382
425
|
useEffect(() => {
|
@@ -464,12 +507,90 @@ const ImageUpload: FC<ImageUploadProps> = memo(({ value, onChange, style, classN
|
|
464
507
|
onChange?.(undefined);
|
465
508
|
};
|
466
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];
|
518
|
+
|
519
|
+
// Create preview URL
|
520
|
+
const previewUrl = URL.createObjectURL(file);
|
521
|
+
|
522
|
+
// Set initial upload state
|
523
|
+
setUploadState({
|
524
|
+
previewUrl,
|
525
|
+
progress: 0,
|
526
|
+
status: 'pending',
|
527
|
+
});
|
528
|
+
|
529
|
+
try {
|
530
|
+
// Start upload using the same logic as handleFileChange
|
531
|
+
const result = await uploadWithProgress({
|
532
|
+
file,
|
533
|
+
onStatusUpdate: (updateData) => {
|
534
|
+
if (updateData.type === 'updateFile') {
|
535
|
+
setUploadState((prev) => {
|
536
|
+
if (!prev) return null;
|
537
|
+
|
538
|
+
const fileStatus = updateData.value.status;
|
539
|
+
if (!fileStatus) return prev;
|
540
|
+
|
541
|
+
return {
|
542
|
+
...prev,
|
543
|
+
error: fileStatus === 'error' ? 'Upload failed' : undefined,
|
544
|
+
progress: updateData.value.uploadState?.progress || 0,
|
545
|
+
status: fileStatus,
|
546
|
+
};
|
547
|
+
});
|
548
|
+
} else if (updateData.type === 'removeFile') {
|
549
|
+
setUploadState(null);
|
550
|
+
}
|
551
|
+
},
|
552
|
+
skipCheckFileType: true,
|
553
|
+
});
|
554
|
+
|
555
|
+
if (result?.url) {
|
556
|
+
// Upload successful
|
557
|
+
onChange?.(result.url);
|
558
|
+
}
|
559
|
+
} catch {
|
560
|
+
// Upload failed
|
561
|
+
setUploadState((prev) =>
|
562
|
+
prev
|
563
|
+
? {
|
564
|
+
...prev,
|
565
|
+
error: 'Upload failed',
|
566
|
+
status: 'error',
|
567
|
+
}
|
568
|
+
: null,
|
569
|
+
);
|
570
|
+
} finally {
|
571
|
+
// Cleanup
|
572
|
+
if (isLocalBlobUrl(previewUrl)) {
|
573
|
+
URL.revokeObjectURL(previewUrl);
|
574
|
+
}
|
575
|
+
|
576
|
+
// Clear upload state after a delay to show completion
|
577
|
+
setTimeout(() => {
|
578
|
+
setUploadState(null);
|
579
|
+
}, 1000);
|
580
|
+
}
|
581
|
+
};
|
582
|
+
|
583
|
+
const { isDragOver, dragHandlers } = useDragAndDrop({
|
584
|
+
accept: 'image/*',
|
585
|
+
onDrop: handleDrop,
|
586
|
+
});
|
587
|
+
|
467
588
|
// Determine which view to render
|
468
589
|
const hasImage = Boolean(value);
|
469
590
|
const isUploading = Boolean(uploadState);
|
470
591
|
|
471
592
|
return (
|
472
|
-
<div className={className} style={style}>
|
593
|
+
<div className={className} {...dragHandlers} style={style}>
|
473
594
|
{/* Hidden file input */}
|
474
595
|
<input
|
475
596
|
accept="image/*"
|
@@ -489,11 +610,12 @@ const ImageUpload: FC<ImageUploadProps> = memo(({ value, onChange, style, classN
|
|
489
610
|
) : hasImage ? (
|
490
611
|
<SuccessDisplay
|
491
612
|
imageUrl={value!}
|
613
|
+
isDragOver={isDragOver}
|
492
614
|
onChangeImage={handleFileSelect}
|
493
615
|
onDelete={handleDelete}
|
494
616
|
/>
|
495
617
|
) : (
|
496
|
-
<Placeholder onClick={handleFileSelect} />
|
618
|
+
<Placeholder isDragOver={isDragOver} onClick={handleFileSelect} />
|
497
619
|
)}
|
498
620
|
</div>
|
499
621
|
);
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import { Button, Modal } from '@lobehub/ui';
|
4
4
|
import { createStyles } from 'antd-style';
|
5
5
|
import { Upload, X } from 'lucide-react';
|
6
|
+
import Image from 'next/image';
|
6
7
|
import React, { type FC, memo, useEffect, useRef, useState } from 'react';
|
7
8
|
import { useTranslation } from 'react-i18next';
|
8
9
|
|
@@ -154,13 +155,6 @@ const useStyles = createStyles(({ css, token }) => ({
|
|
154
155
|
&.selected {
|
155
156
|
border-color: ${token.colorPrimary};
|
156
157
|
}
|
157
|
-
|
158
|
-
img {
|
159
|
-
display: block;
|
160
|
-
width: 100%;
|
161
|
-
height: 100%;
|
162
|
-
object-fit: cover;
|
163
|
-
}
|
164
158
|
`,
|
165
159
|
thumbnailDelete: css`
|
166
160
|
cursor: pointer;
|
@@ -322,10 +316,12 @@ const ImageManageModal: FC<ImageManageModalProps> = memo(
|
|
322
316
|
key={item.id}
|
323
317
|
onClick={() => setSelectedIndex(index)}
|
324
318
|
>
|
325
|
-
<
|
319
|
+
<Image
|
326
320
|
alt={`Image ${index + 1}`}
|
321
|
+
fill
|
327
322
|
src={displayUrl}
|
328
|
-
style={{
|
323
|
+
style={{ objectFit: 'cover' }}
|
324
|
+
unoptimized
|
329
325
|
/>
|
330
326
|
|
331
327
|
{/* 新文件标识 */}
|
@@ -380,10 +376,14 @@ const ImageManageModal: FC<ImageManageModalProps> = memo(
|
|
380
376
|
<div className={styles.previewArea}>
|
381
377
|
{selectedItem ? (
|
382
378
|
<>
|
383
|
-
<
|
379
|
+
<Image
|
384
380
|
alt="Preview"
|
385
381
|
className={styles.previewImage}
|
382
|
+
height={320}
|
386
383
|
src={getDisplayUrl(selectedItem)}
|
384
|
+
style={{ objectFit: 'contain' }}
|
385
|
+
unoptimized
|
386
|
+
width={400}
|
387
387
|
/>
|
388
388
|
<div className={styles.fileName}>{getDisplayFileName(selectedItem)}</div>
|
389
389
|
</>
|