@lobehub/chat 1.100.1 → 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.
Files changed (59) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/changelog/v1.json +21 -0
  5. package/docs/development/basic/add-new-ai-image-model.mdx +36 -0
  6. package/docs/development/basic/add-new-ai-image-model.zh-CN.mdx +0 -0
  7. package/locales/ar/components.json +5 -4
  8. package/locales/ar/models.json +3 -0
  9. package/locales/bg-BG/components.json +5 -4
  10. package/locales/bg-BG/models.json +3 -0
  11. package/locales/de-DE/components.json +5 -4
  12. package/locales/de-DE/models.json +3 -0
  13. package/locales/en-US/components.json +5 -4
  14. package/locales/en-US/models.json +3 -0
  15. package/locales/es-ES/components.json +5 -4
  16. package/locales/es-ES/models.json +3 -0
  17. package/locales/fa-IR/components.json +5 -4
  18. package/locales/fa-IR/models.json +3 -0
  19. package/locales/fr-FR/components.json +5 -4
  20. package/locales/fr-FR/models.json +3 -0
  21. package/locales/it-IT/components.json +5 -4
  22. package/locales/it-IT/models.json +3 -0
  23. package/locales/ja-JP/components.json +5 -4
  24. package/locales/ja-JP/models.json +3 -0
  25. package/locales/ko-KR/components.json +5 -4
  26. package/locales/ko-KR/models.json +3 -0
  27. package/locales/nl-NL/components.json +5 -4
  28. package/locales/nl-NL/models.json +3 -0
  29. package/locales/pl-PL/components.json +5 -4
  30. package/locales/pl-PL/models.json +3 -0
  31. package/locales/pt-BR/components.json +5 -4
  32. package/locales/pt-BR/models.json +3 -0
  33. package/locales/ru-RU/components.json +5 -4
  34. package/locales/ru-RU/models.json +3 -0
  35. package/locales/tr-TR/components.json +5 -4
  36. package/locales/tr-TR/models.json +3 -0
  37. package/locales/vi-VN/components.json +5 -4
  38. package/locales/vi-VN/models.json +3 -0
  39. package/locales/zh-CN/components.json +5 -4
  40. package/locales/zh-CN/models.json +3 -0
  41. package/locales/zh-TW/components.json +5 -4
  42. package/locales/zh-TW/models.json +3 -0
  43. package/package.json +1 -1
  44. package/src/app/(backend)/middleware/auth/index.ts +28 -7
  45. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +24 -9
  46. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
  47. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +161 -39
  48. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +10 -10
  49. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +153 -83
  50. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +2 -11
  51. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useDragAndDrop.ts +71 -0
  52. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/style.ts +17 -0
  53. package/src/config/aiModels/zhipu.ts +29 -6
  54. package/src/const/auth.ts +1 -0
  55. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +20 -3
  56. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +20 -11
  57. package/src/libs/oidc-provider/jwt.ts +22 -13
  58. package/src/libs/trpc/lambda/context.ts +8 -2
  59. package/src/locales/default/components.ts +4 -3
@@ -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 className={styles.placeholder} gap={16} horizontal={false} onClick={onClick}>
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', 'Add Image')}
319
+ {t('ImageUpload.placeholder.primary')}
298
320
  <br />
299
- {t('ImageUpload.placeholder.secondary', 'Click to upload')}
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
- <img alt="Uploading preview" className={styles.imageStyle} src={previewUrl} />
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(({ imageUrl, onDelete, onChangeImage }) => {
340
- const { styles } = useStyles();
341
- const { t } = useTranslation('components');
342
-
343
- const handleDelete = (event: React.MouseEvent) => {
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
- const handleChangeImage = (event: React.MouseEvent) => {
349
- event.stopPropagation();
350
- onChangeImage?.();
351
- };
374
+ const handleDelete = (event: React.MouseEvent) => {
375
+ event.stopPropagation();
376
+ onDelete?.();
377
+ };
352
378
 
353
- return (
354
- <div className={styles.successDisplay} onClick={onChangeImage}>
355
- <img alt="Uploaded image" className={styles.imageStyle} src={imageUrl} />
379
+ const handleChangeImage = (event: React.MouseEvent) => {
380
+ event.stopPropagation();
381
+ onChangeImage?.();
382
+ };
356
383
 
357
- {/* Delete button */}
358
- <div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
359
- <X size={14} />
360
- </div>
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
- {/* Change image overlay */}
363
- <div className={`${styles.changeOverlay} change-overlay`} onClick={handleChangeImage}>
364
- <button className={styles.changeButton} type="button">
365
- {t('ImageUpload.actions.changeImage', 'Click to change image')}
366
- </button>
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
- </div>
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
- <img
319
+ <Image
326
320
  alt={`Image ${index + 1}`}
321
+ fill
327
322
  src={displayUrl}
328
- style={{ height: '100%', objectFit: 'cover', width: '100%' }}
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
- <img
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
  </>