@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.
Files changed (68) hide show
  1. package/.cursor/rules/project-introduce.mdc +1 -15
  2. package/.cursor/rules/project-structure.mdc +227 -0
  3. package/.cursor/rules/testing-guide/db-model-test.mdc +5 -3
  4. package/.cursor/rules/testing-guide/testing-guide.mdc +153 -168
  5. package/.github/workflows/claude.yml +1 -1
  6. package/.github/workflows/test.yml +9 -0
  7. package/.prettierignore +0 -1
  8. package/.vscode/settings.json +86 -80
  9. package/CHANGELOG.md +50 -0
  10. package/CLAUDE.md +11 -27
  11. package/changelog/v1.json +10 -0
  12. package/docs/development/basic/feature-development.mdx +1 -1
  13. package/docs/development/basic/feature-development.zh-CN.mdx +1 -1
  14. package/package.json +5 -5
  15. package/packages/const/src/image.ts +28 -0
  16. package/packages/const/src/index.ts +1 -0
  17. package/packages/database/package.json +4 -2
  18. package/packages/database/src/repositories/aiInfra/index.ts +1 -1
  19. package/packages/database/tests/setup-db.ts +3 -0
  20. package/packages/database/vitest.config.mts +33 -0
  21. package/packages/model-runtime/src/utils/modelParse.ts +1 -1
  22. package/packages/utils/src/client/imageDimensions.test.ts +95 -0
  23. package/packages/utils/src/client/imageDimensions.ts +54 -0
  24. package/packages/utils/src/number.test.ts +3 -1
  25. package/packages/utils/src/number.ts +1 -2
  26. package/src/app/[variants]/(main)/files/[id]/page.tsx +0 -2
  27. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +1 -1
  28. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +0 -1
  29. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +206 -185
  30. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +16 -4
  31. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +52 -3
  32. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +33 -19
  33. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +40 -12
  34. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useAutoDimensions.ts +56 -0
  35. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/hooks/useUploadFilesValidation.ts +77 -0
  36. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +82 -5
  37. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/dimensionConstraints.test.ts +235 -0
  38. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/__tests__/imageValidation.test.ts +401 -0
  39. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/dimensionConstraints.ts +54 -0
  40. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/utils/imageValidation.ts +117 -0
  41. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +3 -1
  42. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +15 -2
  43. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +5 -4
  44. package/src/libs/standard-parameters/index.ts +4 -1
  45. package/src/locales/default/components.ts +8 -0
  46. package/src/server/services/generation/index.ts +1 -1
  47. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +29 -29
  48. package/src/store/aiInfra/slices/aiProvider/action.ts +80 -36
  49. package/src/store/chat/slices/builtinTool/actions/dalle.test.ts +20 -13
  50. package/src/store/file/slices/upload/action.ts +18 -7
  51. package/src/store/image/slices/generationConfig/hooks.ts +11 -1
  52. package/tsconfig.json +1 -10
  53. package/packages/const/src/imageGeneration.ts +0 -16
  54. package/src/app/(backend)/trpc/desktop/[trpc]/route.ts +0 -26
  55. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +0 -24
  56. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +0 -15
  57. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +0 -91
  58. package/src/app/desktop/devtools/page.tsx +0 -89
  59. package/src/app/desktop/layout.tsx +0 -31
  60. /package/apps/desktop/{vitest.config.ts → vitest.config.mts} +0 -0
  61. /package/packages/database/{vitest.config.ts → vitest.config.server.mts} +0 -0
  62. /package/packages/electron-server-ipc/{vitest.config.ts → vitest.config.mts} +0 -0
  63. /package/packages/file-loaders/{vitest.config.ts → vitest.config.mts} +0 -0
  64. /package/packages/model-runtime/{vitest.config.ts → vitest.config.mts} +0 -0
  65. /package/packages/prompts/{vitest.config.ts → vitest.config.mts} +0 -0
  66. /package/packages/utils/{vitest.config.ts → vitest.config.mts} +0 -0
  67. /package/packages/web-crawler/{vitest.config.ts → vitest.config.mts} +0 -0
  68. /package/{vitest.config.ts → vitest.config.mts} +0 -0
@@ -7,6 +7,7 @@ import { LOBE_CHAT_CLOUD } from '@/const/branding';
7
7
  import { fileService } from '@/services/file';
8
8
  import { uploadService } from '@/services/upload';
9
9
  import { FileMetadata, UploadFileItem } from '@/types/files';
10
+ import { getImageDimensions } from '@/utils/client/imageDimensions';
10
11
 
11
12
  import { FileStore } from '../../store';
12
13
 
@@ -36,6 +37,10 @@ interface UploadWithProgressParams {
36
37
  }
37
38
 
38
39
  interface UploadWithProgressResult {
40
+ dimensions?: {
41
+ height: number;
42
+ width: number;
43
+ };
39
44
  filename?: string;
40
45
  id: string;
41
46
  url: string;
@@ -61,6 +66,9 @@ export const createFileUploadSlice: StateCreator<
61
66
  FileUploadAction
62
67
  > = () => ({
63
68
  uploadBase64FileWithProgress: async (base64) => {
69
+ // Extract image dimensions from base64 data
70
+ const dimensions = await getImageDimensions(base64);
71
+
64
72
  const { metadata, fileType, size, hash } = await uploadService.uploadBase64ToS3(base64);
65
73
 
66
74
  const res = await fileService.createFile({
@@ -71,18 +79,21 @@ export const createFileUploadSlice: StateCreator<
71
79
  size: size,
72
80
  url: metadata.path,
73
81
  });
74
- return { ...res, filename: metadata.filename };
82
+ return { ...res, dimensions, filename: metadata.filename };
75
83
  },
76
84
  uploadWithProgress: async ({ file, onStatusUpdate, knowledgeBaseId, skipCheckFileType }) => {
77
85
  const fileArrayBuffer = await file.arrayBuffer();
78
86
 
79
- // 1. check file hash
87
+ // 1. extract image dimensions if applicable
88
+ const dimensions = await getImageDimensions(file);
89
+
90
+ // 2. check file hash
80
91
  const hash = sha256(fileArrayBuffer);
81
92
 
82
93
  const checkStatus = await fileService.checkFileHash(hash);
83
94
  let metadata: FileMetadata;
84
95
 
85
- // 2. if file exist, just skip upload
96
+ // 3. if file exist, just skip upload
86
97
  if (checkStatus.isExist) {
87
98
  metadata = checkStatus.metadata as FileMetadata;
88
99
  onStatusUpdate?.({
@@ -91,7 +102,7 @@ export const createFileUploadSlice: StateCreator<
91
102
  value: { status: 'processing', uploadState: { progress: 100, restTime: 0, speed: 0 } },
92
103
  });
93
104
  }
94
- // 2. if file don't exist, need upload files
105
+ // 3. if file don't exist, need upload files
95
106
  else {
96
107
  const { data, success } = await uploadService.uploadFileToS3(file, {
97
108
  onNotSupported: () => {
@@ -119,7 +130,7 @@ export const createFileUploadSlice: StateCreator<
119
130
  metadata = data;
120
131
  }
121
132
 
122
- // 3. use more powerful file type detector to get file type
133
+ // 4. use more powerful file type detector to get file type
123
134
  let fileType = file.type;
124
135
 
125
136
  if (!file.type) {
@@ -129,7 +140,7 @@ export const createFileUploadSlice: StateCreator<
129
140
  fileType = type?.mime || 'text/plain';
130
141
  }
131
142
 
132
- // 4. create file to db
143
+ // 5. create file to db
133
144
  const data = await fileService.createFile(
134
145
  {
135
146
  fileType,
@@ -153,6 +164,6 @@ export const createFileUploadSlice: StateCreator<
153
164
  },
154
165
  });
155
166
 
156
- return { ...data, filename: file.name };
167
+ return { ...data, dimensions, filename: file.name };
157
168
  },
158
169
  });
@@ -44,6 +44,14 @@ export function useGenerationConfigParam<
44
44
  paramConfig && typeof paramConfig === 'object' && 'enum' in paramConfig
45
45
  ? paramConfig.enum
46
46
  : undefined;
47
+ const maxFileSize =
48
+ paramConfig && typeof paramConfig === 'object' && 'maxFileSize' in paramConfig
49
+ ? paramConfig.maxFileSize
50
+ : undefined;
51
+ const maxCount =
52
+ paramConfig && typeof paramConfig === 'object' && 'maxCount' in paramConfig
53
+ ? paramConfig.maxCount
54
+ : undefined;
47
55
 
48
56
  return {
49
57
  description,
@@ -51,6 +59,8 @@ export function useGenerationConfigParam<
51
59
  min,
52
60
  step,
53
61
  enumValues,
62
+ maxFileSize,
63
+ maxCount,
54
64
  };
55
65
  }, [paramConfig]);
56
66
 
@@ -87,7 +97,7 @@ export function useDimensionControl() {
87
97
  }, [paramsSchema]);
88
98
 
89
99
  // 只要不是所有维度相关的控件都不显示,那么这个容器就应该显示
90
- const showDimensionControl = !(!isSupportAspectRatio && !isSupportWidth && !isSupportHeight);
100
+ const showDimensionControl = isSupportAspectRatio || isSupportWidth || isSupportHeight;
91
101
 
92
102
  return {
93
103
  isLocked: store.isAspectRatioLocked,
package/tsconfig.json CHANGED
@@ -34,14 +34,5 @@
34
34
  ]
35
35
  },
36
36
  "exclude": ["node_modules", "public/sw.js", "apps/desktop", "tmp", "temp", ".temp"],
37
- "include": [
38
- "**/*.d.ts",
39
- "**/*.ts",
40
- "**/*.tsx",
41
- ".next/types/**/*.ts",
42
- "next-env.d.ts",
43
- "src",
44
- "tests",
45
- "vitest.config.ts"
46
- ]
37
+ "include": ["**/*.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next-env.d.ts"]
47
38
  }
@@ -1,16 +0,0 @@
1
- /**
2
- * Image generation and processing configuration constants
3
- */
4
- export const IMAGE_GENERATION_CONFIG = {
5
- /**
6
- * Maximum cover image size in pixels (longest edge)
7
- * Used for generating cover images from source images
8
- */
9
- COVER_MAX_SIZE: 256,
10
-
11
- /**
12
- * Maximum thumbnail size in pixels (longest edge)
13
- * Used for generating thumbnail images from original images
14
- */
15
- THUMBNAIL_MAX_SIZE: 512,
16
- } as const;
@@ -1,26 +0,0 @@
1
- import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
2
- import type { NextRequest } from 'next/server';
3
-
4
- import { pino } from '@/libs/logger';
5
- import { createLambdaContext } from '@/libs/trpc/lambda/context';
6
- import { desktopRouter } from '@/server/routers/desktop';
7
-
8
- const handler = (req: NextRequest) =>
9
- fetchRequestHandler({
10
- /**
11
- * @link https://trpc.io/docs/v11/context
12
- */
13
- createContext: () => createLambdaContext(req),
14
-
15
- endpoint: '/trpc/desktop',
16
-
17
- onError: ({ error, path, type }) => {
18
- pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
19
- console.error(error);
20
- },
21
-
22
- req,
23
- router: desktopRouter,
24
- });
25
-
26
- export { handler as GET, handler as POST };
@@ -1,24 +0,0 @@
1
- import { memo } from 'react';
2
-
3
- import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
4
-
5
- import Select from '../../../components/AspectRatioSelect';
6
-
7
- const AspectRatioSelect = memo(() => {
8
- const { value, setValue, enumValues } = useGenerationConfigParam('aspectRatio');
9
-
10
- // 如果模型支持 ratio 枚举值,则使用枚举值
11
- if (enumValues && enumValues.length > 0) {
12
- const options = enumValues.map((ratio) => ({
13
- label: ratio,
14
- value: ratio,
15
- }));
16
-
17
- return <Select onChange={setValue} options={options} style={{ width: '100%' }} value={value} />;
18
- }
19
-
20
- // 如果模型不支持 ratio 参数,返回 null(由外部处理是否显示)
21
- return null;
22
- });
23
-
24
- export default AspectRatioSelect;
@@ -1,15 +0,0 @@
1
- import { SliderWithInput } from '@lobehub/ui';
2
- import { memo } from 'react';
3
-
4
- import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
5
-
6
- interface SizeSliderInputProps {
7
- paramName: 'width' | 'height';
8
- }
9
-
10
- const SizeSliderInput = memo(({ paramName }: SizeSliderInputProps) => {
11
- const { value, setValue, min, max } = useGenerationConfigParam(paramName);
12
- return <SliderWithInput max={max} min={min} onChange={setValue} value={value} />;
13
- });
14
-
15
- export default SizeSliderInput;
@@ -1,91 +0,0 @@
1
- 'use client';
2
-
3
- import { Tooltip } from '@lobehub/ui';
4
- import { createStyles } from 'antd-style';
5
- import { type ReactNode } from 'react';
6
- import { Center, CenterProps } from 'react-layout-kit';
7
-
8
- const useStyles = createStyles(({ css, token }) => ({
9
- activeContainer: css`
10
- border: 2px solid ${token.colorPrimary};
11
- background: ${token.colorPrimaryBg};
12
-
13
- &:hover {
14
- background: ${token.colorPrimaryBgHover};
15
- }
16
- `,
17
- container: css`
18
- cursor: pointer;
19
-
20
- position: relative;
21
-
22
- flex: none;
23
-
24
- width: 50px;
25
- height: 50px;
26
- border-radius: 6px;
27
-
28
- background: transparent;
29
-
30
- transition: all 0.2s ease-in-out;
31
-
32
- &::before {
33
- pointer-events: none;
34
- content: '';
35
-
36
- position: absolute;
37
- inset: 0;
38
-
39
- border-radius: 6px;
40
-
41
- opacity: 0;
42
- background: radial-gradient(circle at center, ${token.colorPrimary}20 0%, transparent 70%);
43
-
44
- transition: opacity 0.3s ease;
45
- }
46
-
47
- &:hover {
48
- background: ${token.colorFillSecondary};
49
- }
50
-
51
- &:active {
52
- background: ${token.colorFillTertiary};
53
- }
54
-
55
- &:active::before {
56
- opacity: 1;
57
- }
58
- `,
59
- }));
60
-
61
- interface TopicItemContainerProps extends CenterProps {
62
- active?: boolean;
63
- children: ReactNode;
64
- tooltip?: ReactNode;
65
- }
66
-
67
- const TopicItemContainer = ({
68
- children,
69
- className,
70
- active,
71
- tooltip,
72
- ...rest
73
- }: TopicItemContainerProps) => {
74
- const { styles, cx } = useStyles();
75
-
76
- const content = (
77
- <Center className={cx(styles.container, active && styles.activeContainer, className)} {...rest}>
78
- {children}
79
- </Center>
80
- );
81
-
82
- if (!tooltip) return content;
83
-
84
- return (
85
- <Tooltip arrow placement="left" title={tooltip}>
86
- {content}
87
- </Tooltip>
88
- );
89
- };
90
-
91
- export default TopicItemContainer;
@@ -1,89 +0,0 @@
1
- 'use client';
2
-
3
- import { ActionIcon, SideNav } from '@lobehub/ui';
4
- import { Cog, DatabaseIcon } from 'lucide-react';
5
- import { memo, useState } from 'react';
6
- import { Flexbox } from 'react-layout-kit';
7
-
8
- import { BRANDING_NAME } from '@/const/branding';
9
- import PostgresViewer from '@/features/DevPanel/PostgresViewer';
10
- import SystemInspector from '@/features/DevPanel/SystemInspector';
11
- import { useStyles } from '@/features/DevPanel/features/FloatPanel';
12
- import { electronStylish } from '@/styles/electron';
13
-
14
- const DevTools = memo(() => {
15
- const { styles, theme, cx } = useStyles();
16
-
17
- const items = [
18
- {
19
- children: <PostgresViewer />,
20
- icon: <DatabaseIcon size={16} />,
21
- key: 'Postgres Viewer',
22
- },
23
- {
24
- children: <SystemInspector />,
25
- icon: <Cog size={16} />,
26
- key: 'System Status',
27
- },
28
- ];
29
-
30
- const [tab, setTab] = useState<string>(items[0].key);
31
-
32
- return (
33
- <Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
34
- <Flexbox
35
- align={'center'}
36
- className={cx(`panel-drag-handle`, styles.header, electronStylish.draggable)}
37
- horizontal
38
- justify={'center'}
39
- >
40
- <Flexbox align={'baseline'} gap={6} horizontal>
41
- <b>{BRANDING_NAME} Dev Tools</b>
42
- <span style={{ color: theme.colorTextDescription }}>/</span>
43
- <span style={{ color: theme.colorTextDescription }}>{tab}</span>
44
- </Flexbox>
45
- </Flexbox>
46
- <Flexbox
47
- height={'100%'}
48
- horizontal
49
- style={{ background: theme.colorBgLayout, overflow: 'hidden', position: 'relative' }}
50
- width={'100%'}
51
- >
52
- <SideNav
53
- bottomActions={[]}
54
- style={{
55
- background: 'transparent',
56
- width: 48,
57
- }}
58
- topActions={items.map((item) => (
59
- <ActionIcon
60
- active={tab === item.key}
61
- icon={item.icon}
62
- key={item.key}
63
- onClick={() => setTab(item.key)}
64
- title={item.key}
65
- tooltipProps={{
66
- placement: 'right',
67
- }}
68
- />
69
- ))}
70
- />
71
- {items.map((item) => (
72
- <Flexbox
73
- flex={1}
74
- height={'100%'}
75
- key={item.key}
76
- style={{
77
- display: tab === item.key ? 'flex' : 'none',
78
- overflow: 'hidden',
79
- }}
80
- >
81
- {item.children}
82
- </Flexbox>
83
- ))}
84
- </Flexbox>
85
- </Flexbox>
86
- );
87
- });
88
-
89
- export default DevTools;
@@ -1,31 +0,0 @@
1
- import { notFound } from 'next/navigation';
2
- import { NuqsAdapter } from 'nuqs/adapters/next/app';
3
- import { ReactNode } from 'react';
4
-
5
- import { isDesktop } from '@/const/version';
6
- import GlobalLayout from '@/layout/GlobalProvider';
7
- import { ServerConfigStoreProvider } from '@/store/serverConfig/Provider';
8
-
9
- interface RootLayoutProps {
10
- children: ReactNode;
11
- }
12
-
13
- const RootLayout = async ({ children }: RootLayoutProps) => {
14
- if (!isDesktop) return notFound();
15
-
16
- return (
17
- <html dir="ltr" suppressHydrationWarning>
18
- <body>
19
- <NuqsAdapter>
20
- <ServerConfigStoreProvider>
21
- <GlobalLayout appearance={'auto'} isMobile={false} locale={''}>
22
- {children}
23
- </GlobalLayout>
24
- </ServerConfigStoreProvider>
25
- </NuqsAdapter>
26
- </body>
27
- </html>
28
- );
29
- };
30
-
31
- export default RootLayout;
File without changes