@lobehub/chat 0.107.12 → 0.107.13

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 CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.107.13](https://github.com/lobehub/lobe-chat/compare/v0.107.12...v0.107.13)
6
+
7
+ <sup>Released on **2023-12-03**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor Image components.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Fix a bug that can't send only images with empty content, Fix image gallery sort index, Fix image gallery sort index, Fix image sort index, Fix image upload error, Fix import.
16
+
17
+ #### 💄 Styles
18
+
19
+ - **misc**: Change image fit to cover mode, Fix empty files style, Move file inside chat input in mobile mode, Update editable image style, Update image default background color, Update image editable style, Update image grid, Update Image grid, Update image remove button hover style.
20
+
21
+ <br/>
22
+
23
+ <details>
24
+ <summary><kbd>Improvements and Fixes</kbd></summary>
25
+
26
+ #### Code refactoring
27
+
28
+ - **misc**: Refactor Image components ([72dcd18](https://github.com/lobehub/lobe-chat/commit/72dcd18))
29
+
30
+ #### What's fixed
31
+
32
+ - **misc**: Fix a bug that can't send only images with empty content ([9601520](https://github.com/lobehub/lobe-chat/commit/9601520))
33
+ - **misc**: Fix image gallery sort index ([16548d3](https://github.com/lobehub/lobe-chat/commit/16548d3))
34
+ - **misc**: Fix image gallery sort index ([5636599](https://github.com/lobehub/lobe-chat/commit/5636599))
35
+ - **misc**: Fix image sort index ([29cf223](https://github.com/lobehub/lobe-chat/commit/29cf223))
36
+ - **misc**: Fix image upload error ([c7745c7](https://github.com/lobehub/lobe-chat/commit/c7745c7))
37
+ - **misc**: Fix import ([af797d6](https://github.com/lobehub/lobe-chat/commit/af797d6))
38
+
39
+ #### Styles
40
+
41
+ - **misc**: Change image fit to cover mode ([9fdc459](https://github.com/lobehub/lobe-chat/commit/9fdc459))
42
+ - **misc**: Fix empty files style ([bbe14c4](https://github.com/lobehub/lobe-chat/commit/bbe14c4))
43
+ - **misc**: Move file inside chat input in mobile mode ([b6401c1](https://github.com/lobehub/lobe-chat/commit/b6401c1))
44
+ - **misc**: Update editable image style ([a96ca4c](https://github.com/lobehub/lobe-chat/commit/a96ca4c))
45
+ - **misc**: Update image default background color ([fadc024](https://github.com/lobehub/lobe-chat/commit/fadc024))
46
+ - **misc**: Update image editable style ([8dea792](https://github.com/lobehub/lobe-chat/commit/8dea792))
47
+ - **misc**: Update image grid ([901d10c](https://github.com/lobehub/lobe-chat/commit/901d10c))
48
+ - **misc**: Update Image grid ([c68788d](https://github.com/lobehub/lobe-chat/commit/c68788d))
49
+ - **misc**: Update image remove button hover style ([5bc916c](https://github.com/lobehub/lobe-chat/commit/5bc916c))
50
+
51
+ </details>
52
+
53
+ <div align="right">
54
+
55
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
56
+
57
+ </div>
58
+
5
59
  ### [Version 0.107.12](https://github.com/lobehub/lobe-chat/compare/v0.107.11...v0.107.12)
6
60
 
7
61
  <sup>Released on **2023-12-02**</sup>
package/README.md CHANGED
@@ -395,6 +395,8 @@ If you want to deploy this service yourself on Zeabur, you can follow these step
395
395
 
396
396
  </div>
397
397
 
398
+ <br/>
399
+
398
400
  ### Environment Variable
399
401
 
400
402
  This project provides some additional configuration items set with environment variables:
@@ -485,7 +487,37 @@ Contributions of all types are more than welcome; if you are interested in contr
485
487
  [![][submit-agents-shield]][submit-agents-link]
486
488
  [![][submit-plugin-shield]][submit-plugin-link]
487
489
 
488
- [![][contributors-contrib]][contributors-link]
490
+ <a href="https://github.com/lobehub/lobe-chat/graphs/contributors" target="_blank">
491
+ <table>
492
+ <tr>
493
+ <th colspan="2">
494
+ <br><img src="https://contrib.rocks/image?repo=lobehub/lobe-chat"><br><br>
495
+ </th>
496
+ </tr>
497
+ <tr>
498
+ <td>
499
+ <picture>
500
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=dark">
501
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=light">
502
+ </picture>
503
+ </td>
504
+ <td rowspan="2">
505
+ <picture>
506
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-participants-growth/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=4x7&color_scheme=dark">
507
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-participants-growth/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=4x7&color_scheme=light">
508
+ </picture>
509
+ </td>
510
+ </tr>
511
+ <tr>
512
+ <td>
513
+ <picture>
514
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=new&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=dark">
515
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=new&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=light">
516
+ </picture>
517
+ </td>
518
+ </tr>
519
+ </table>
520
+ </a>
489
521
 
490
522
  <div align="right">
491
523
 
@@ -530,8 +562,6 @@ This project is [MIT](./LICENSE) licensed.
530
562
  [chat-plugins-gateway]: https://github.com/lobehub/chat-plugins-gateway
531
563
  [codespaces-link]: https://codespaces.new/lobehub/lobe-chat
532
564
  [codespaces-shield]: https://github.com/codespaces/badge.svg
533
- [contributors-contrib]: https://contrib.rocks/image?repo=lobehub/lobe-chat
534
- [contributors-link]: https://github.com/lobehub/lobe-chat/graphs/contributors
535
565
  [deploy-button-image]: https://vercel.com/button
536
566
  [deploy-link]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat&env=OPENAI_API_KEY&envDescription=Find%20your%20OpenAI%20API%20Key%20by%20click%20the%20right%20Learn%20More%20button.&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys&project-name=lobe-chat&repository-name=lobe-chat
537
567
  [deploy-on-zeabur-button-image]: https://zeabur.com/button.svg
package/README.zh-CN.md CHANGED
@@ -369,6 +369,8 @@ $ docker run -d -p 3210:3210 \
369
369
 
370
370
  </div>
371
371
 
372
+ <br/>
373
+
372
374
  ### 环境变量
373
375
 
374
376
  本项目提供了一些额外的配置项,使用环境变量进行设置:
@@ -459,7 +461,37 @@ $ bun run dev
459
461
  [![][submit-agents-shield]][submit-agents-link]
460
462
  [![][submit-plugin-shield]][submit-plugin-link]
461
463
 
462
- [![][contributors-contrib]][contributors-link]
464
+ <a href="https://github.com/lobehub/lobe-chat/graphs/contributors" target="_blank">
465
+ <table>
466
+ <tr>
467
+ <th colspan="2">
468
+ <br><img src="https://contrib.rocks/image?repo=lobehub/lobe-chat"><br><br>
469
+ </th>
470
+ </tr>
471
+ <tr>
472
+ <td>
473
+ <picture>
474
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=dark">
475
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=light">
476
+ </picture>
477
+ </td>
478
+ <td rowspan="2">
479
+ <picture>
480
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-participants-growth/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=4x7&color_scheme=dark">
481
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-participants-growth/thumbnail.png?activity=active&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=4x7&color_scheme=light">
482
+ </picture>
483
+ </td>
484
+ </tr>
485
+ <tr>
486
+ <td>
487
+ <picture>
488
+ <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=new&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=dark">
489
+ <img src="https://next.ossinsight.io/widgets/official/compose-org-active-contributors/thumbnail.png?activity=new&period=past_28_days&owner_id=131470832&repo_ids=643445235&image_size=2x3&color_scheme=light">
490
+ </picture>
491
+ </td>
492
+ </tr>
493
+ </table>
494
+ </a>
463
495
 
464
496
  <div align="right">
465
497
 
@@ -504,8 +536,6 @@ This project is [MIT](./LICENSE) licensed.
504
536
  [chat-plugins-gateway]: https://github.com/lobehub/chat-plugins-gateway
505
537
  [codespaces-link]: https://codespaces.new/lobehub/lobe-chat
506
538
  [codespaces-shield]: https://github.com/codespaces/badge.svg
507
- [contributors-contrib]: https://contrib.rocks/image?repo=lobehub/lobe-chat
508
- [contributors-link]: https://github.com/lobehub/lobe-chat/graphs/contributors
509
539
  [deploy-button-image]: https://vercel.com/button
510
540
  [deploy-link]: https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flobehub%2Flobe-chat&env=OPENAI_API_KEY&envDescription=Find%20your%20OpenAI%20API%20Key%20by%20click%20the%20right%20Learn%20More%20button.&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys&project-name=lobe-chat&repository-name=lobe-chat
511
541
  [deploy-on-zeabur-button-image]: https://zeabur.com/button.svg
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.107.12",
3
+ "version": "0.107.13",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -106,7 +106,6 @@
106
106
  "react-intersection-observer": "^9",
107
107
  "react-layout-kit": "^1",
108
108
  "react-lazy-load": "^4",
109
- "react-spring-lightbox": "^1",
110
109
  "react-wrap-balancer": "^1.1.0",
111
110
  "remark": "^14",
112
111
  "remark-gfm": "^3",
@@ -1,10 +1,10 @@
1
1
  import { memo } from 'react';
2
2
 
3
- import FileList from '@/app/chat/components/FileList';
3
+ import EditableFileList from '@/components/FileList/EditableFileList';
4
4
  import { useFileStore } from '@/store/file';
5
5
 
6
6
  export const LocalFiles = memo(() => {
7
7
  const inputFilesList = useFileStore((s) => s.inputFilesList);
8
8
 
9
- return <FileList items={inputFilesList} />;
9
+ return <EditableFileList items={inputFilesList} padding={0} />;
10
10
  });
@@ -1,16 +1,17 @@
1
1
  import { memo } from 'react';
2
- import { Flexbox } from 'react-layout-kit';
3
2
 
4
- import FileList from '@/app/chat/components/FileList';
3
+ import EditableFileList from '@/components/FileList/EditableFileList';
5
4
  import { useFileStore } from '@/store/file';
6
5
 
7
6
  const Files = memo(() => {
8
7
  const inputFilesList = useFileStore((s) => s.inputFilesList);
9
8
 
9
+ if (!inputFilesList || inputFilesList?.length === 0) return null;
10
+
10
11
  return (
11
- <Flexbox padding={12}>
12
- <FileList alwaysShowClose items={inputFilesList} />
13
- </Flexbox>
12
+ <div style={{ position: 'relative', width: '100vw' }}>
13
+ <EditableFileList alwaysShowClose items={inputFilesList} padding={'0 8px'} />
14
+ </div>
14
15
  );
15
16
  });
16
17
 
@@ -3,6 +3,7 @@ import { rgba } from 'polished';
3
3
  import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
+ import Files from '@/app/chat/(mobile)/features/ChatInput/Files';
6
7
  import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
7
8
  import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
8
9
  import STT from '@/app/chat/features/ChatInput/STT';
@@ -28,6 +29,7 @@ const ChatInputArea = memo(() => {
28
29
 
29
30
  return (
30
31
  <Flexbox className={styles.container} gap={12}>
32
+ <Files />
31
33
  <ActionBar mobile padding={'0 8px'} rightAreaStartRender={<SaveTopic mobile />} />
32
34
  <Flexbox className={styles.inner} gap={8} horizontal>
33
35
  <STT mobile />
@@ -4,7 +4,6 @@ import { memo } from 'react';
4
4
  import SafeSpacing from '@/components/SafeSpacing';
5
5
  import { CHAT_TEXTAREA_HEIGHT_MOBILE } from '@/const/layoutTokens';
6
6
 
7
- import Files from './Files';
8
7
  import ChatInputArea from './Mobile';
9
8
 
10
9
  const useStyles = createStyles(
@@ -25,7 +24,6 @@ const ChatInputMobileLayout = memo(() => {
25
24
 
26
25
  return (
27
26
  <>
28
- <Files />
29
27
  <SafeSpacing height={CHAT_TEXTAREA_HEIGHT_MOBILE} mobile position={'bottom'} />
30
28
  <div className={styles}>
31
29
  <ChatInputArea />
@@ -6,9 +6,9 @@ import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
6
6
  import { memo, useCallback, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
- import ChatList from 'src/app/chat/features/Conversation/ChatList';
10
9
 
11
10
  import pkg from '@/../package.json';
11
+ import ChatList from '@/app/chat/features/Conversation/ChatList';
12
12
  import { useSessionStore } from '@/store/session';
13
13
  import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
14
14
 
@@ -1,7 +1,7 @@
1
1
  import { ReactNode, memo } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
- import FileList from '@/app/chat/components/FileList';
4
+ import FileList from '@/components/FileList';
5
5
  import { LOADING_FLAT } from '@/const/message';
6
6
  import { ChatMessage } from '@/types/chatMessage';
7
7
 
@@ -17,7 +17,7 @@ export const UserMessage = memo<
17
17
  return (
18
18
  <Flexbox gap={8} id={id}>
19
19
  {editableContent}
20
- {res.files && res.files?.length > 0 && <FileList editable={false} items={res.files} />}
20
+ {res.files && res.files?.length > 0 && <FileList items={res.files} />}
21
21
  </Flexbox>
22
22
  );
23
23
  });
@@ -0,0 +1,35 @@
1
+ import { ImageGallery } from '@lobehub/ui';
2
+ import { useResponsive } from 'antd-style';
3
+ import { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import FileItem from '@/components/FileList/FileItem';
7
+
8
+ interface EditableFileListProps {
9
+ alwaysShowClose?: boolean;
10
+ editable?: boolean;
11
+ items: string[];
12
+ padding?: number | string;
13
+ }
14
+
15
+ const EditableFileList = memo<EditableFileListProps>(
16
+ ({ items, editable = true, alwaysShowClose, padding = 12 }) => {
17
+ const { mobile } = useResponsive();
18
+ return (
19
+ <Flexbox
20
+ gap={mobile ? 4 : 6}
21
+ horizontal
22
+ padding={padding}
23
+ style={{ overflow: 'auto', width: '100%' }}
24
+ >
25
+ <ImageGallery>
26
+ {items.map((i) => (
27
+ <FileItem alwaysShowClose={alwaysShowClose} editable={editable} id={i} key={i} />
28
+ ))}
29
+ </ImageGallery>
30
+ </Flexbox>
31
+ );
32
+ },
33
+ );
34
+
35
+ export default EditableFileList;
@@ -0,0 +1,36 @@
1
+ import { CSSProperties, ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { MAX_SIZE_DESKTOP, MIN_IMAGE_SIZE, useStyles } from '@/components/FileList/style';
5
+
6
+ interface FileGridProps {
7
+ children: ReactNode;
8
+ className?: string;
9
+ col?: number;
10
+ gap?: number;
11
+ max?: number;
12
+ min?: number;
13
+ style?: CSSProperties;
14
+ }
15
+
16
+ const FileGrid = memo<FileGridProps>(
17
+ ({
18
+ gap = 4,
19
+ col = 3,
20
+ max = MAX_SIZE_DESKTOP,
21
+ min = MIN_IMAGE_SIZE,
22
+ children,
23
+ className,
24
+ style,
25
+ }) => {
26
+ const { styles, cx } = useStyles({ col, gap, max, min });
27
+
28
+ return (
29
+ <Flexbox className={cx(styles.container, className)} gap={gap} horizontal style={style}>
30
+ {children}
31
+ </Flexbox>
32
+ );
33
+ },
34
+ );
35
+
36
+ export default FileGrid;
@@ -0,0 +1,73 @@
1
+ import { ActionIcon, Image } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { Trash } from 'lucide-react';
4
+ import { CSSProperties, memo, useCallback } from 'react';
5
+
6
+ import { useFileStore } from '@/store/file';
7
+
8
+ import { MIN_IMAGE_SIZE } from './style';
9
+
10
+ export const useStyles = createStyles(({ css, token }) => ({
11
+ deleteButton: css`
12
+ color: #fff;
13
+ background: ${token.colorBgMask};
14
+
15
+ &:hover {
16
+ background: ${token.colorError};
17
+ }
18
+ `,
19
+ editableImage: css`
20
+ background: ${token.colorBgContainer};
21
+ border: 1px solid ${token.colorBorderSecondary};
22
+ `,
23
+ image: css`
24
+ margin-block: 0 !important;
25
+ `,
26
+ }));
27
+
28
+ interface FileItemProps {
29
+ alwaysShowClose?: boolean;
30
+ className?: string;
31
+ editable?: boolean;
32
+ id: string;
33
+ onClick?: () => void;
34
+ style?: CSSProperties;
35
+ }
36
+ const FileItem = memo<FileItemProps>(({ editable, id, alwaysShowClose }) => {
37
+ const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
38
+ const IMAGE_SIZE = editable ? MIN_IMAGE_SIZE : '100%';
39
+ const { data, isLoading } = useFetchFile(id);
40
+ const { styles, cx } = useStyles();
41
+
42
+ const handleRemoveFile = useCallback(
43
+ (e: any) => {
44
+ e.stopPropagation();
45
+ removeFile(id);
46
+ },
47
+ [id],
48
+ );
49
+
50
+ return (
51
+ <Image
52
+ actions={
53
+ editable && (
54
+ <ActionIcon
55
+ className={styles.deleteButton}
56
+ glass
57
+ icon={Trash}
58
+ onClick={handleRemoveFile}
59
+ size={'small'}
60
+ />
61
+ )
62
+ }
63
+ alt={data?.name || id || ''}
64
+ alwaysShowActions={alwaysShowClose}
65
+ isLoading={isLoading}
66
+ size={IMAGE_SIZE as any}
67
+ src={data?.url}
68
+ wrapperClassName={cx(styles.image, editable && styles.editableImage)}
69
+ />
70
+ );
71
+ });
72
+
73
+ export default FileItem;
@@ -0,0 +1,62 @@
1
+ import { ImageGallery } from '@lobehub/ui';
2
+ import { useResponsive } from 'antd-style';
3
+ import { memo, useMemo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { MAX_SIZE_DESKTOP, MAX_SIZE_MOBILE } from '@/components/FileList/style';
7
+
8
+ import FileGrid from './FileGrid';
9
+ import FileItem from './FileItem';
10
+
11
+ interface FileListProps {
12
+ items: string[];
13
+ }
14
+
15
+ const FileList = memo<FileListProps>(({ items }) => {
16
+ const { mobile } = useResponsive();
17
+
18
+ const { firstRow, lastRow } = useMemo(() => {
19
+ if (items.length === 4) {
20
+ return {
21
+ firstRow: items.slice(0, 2),
22
+ lastRow: items.slice(2, 4),
23
+ };
24
+ }
25
+
26
+ const firstCol = items.length % 3 === 0 ? 3 : items.length % 3;
27
+
28
+ return {
29
+ firstRow: items.slice(0, firstCol),
30
+ lastRow: items.slice(firstCol, items.length),
31
+ };
32
+ }, [items]);
33
+
34
+ const { gap, max } = useMemo(
35
+ () => ({
36
+ gap: mobile ? 4 : 6,
37
+ max: mobile ? MAX_SIZE_MOBILE : MAX_SIZE_DESKTOP,
38
+ }),
39
+ [mobile],
40
+ );
41
+
42
+ return (
43
+ <ImageGallery>
44
+ <Flexbox gap={gap}>
45
+ <FileGrid col={firstRow.length} gap={gap} max={max}>
46
+ {firstRow.map((i) => (
47
+ <FileItem id={i} key={i} />
48
+ ))}
49
+ </FileGrid>
50
+ {lastRow.length > 0 && (
51
+ <FileGrid col={lastRow.length > 2 ? 3 : lastRow.length} gap={gap} max={max}>
52
+ {lastRow.map((i) => (
53
+ <FileItem id={i} key={i} />
54
+ ))}
55
+ </FileGrid>
56
+ )}
57
+ </Flexbox>
58
+ </ImageGallery>
59
+ );
60
+ });
61
+
62
+ export default FileList;
@@ -0,0 +1,24 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const MIN_IMAGE_SIZE = 64;
4
+ export const MAX_SIZE_DESKTOP = 640;
5
+ export const MAX_SIZE_MOBILE = 280;
6
+ export const useStyles = createStyles(
7
+ ({ css }, { col, gap, max, min }: { col: number; gap: number; max: number; min: number }) => ({
8
+ container: css`
9
+ display: grid;
10
+ grid-gap: ${gap}px;
11
+ grid-template-columns: repeat(${col}, 1fr);
12
+
13
+ width: 100%;
14
+ max-width: ${max}px;
15
+
16
+ & > div {
17
+ width: 100%;
18
+ min-width: ${min}px;
19
+ min-height: ${min}px;
20
+ max-height: ${(max - gap * (col - 1)) / col}px;
21
+ }
22
+ `,
23
+ }),
24
+ );
@@ -6,9 +6,9 @@ import { LucideBlocks, LucideSettings, LucideStore, LucideTrash2 } from 'lucide-
6
6
  import { memo, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
- import DevModal from 'src/features/PluginDevModal';
10
9
 
11
10
  import { FORM_STYLE } from '@/const/layoutTokens';
11
+ import DevModal from '@/features/PluginDevModal';
12
12
  import { pluginHelpers, usePluginStore } from '@/store/plugin';
13
13
  import { pluginSelectors } from '@/store/plugin/selectors';
14
14
 
@@ -8,6 +8,7 @@ import { useIsPWA } from '@/hooks/useIsPWA';
8
8
  const AppLayoutDesktop = memo<PropsWithChildren>(({ children }) => {
9
9
  const isPWA = useIsPWA();
10
10
  const theme = useTheme();
11
+
11
12
  return (
12
13
  <Flexbox
13
14
  height={'100%'}
@@ -16,6 +16,7 @@ const InnerLocale = memo<LocaleLayoutProps>(({ children, lang }) => {
16
16
  lang,
17
17
  async () =>
18
18
  await import(`antd/locale/${lang?.includes('-') ? lang?.replace('-', '_') : 'en-US'}.js`),
19
+ { revalidateOnFocus: false },
19
20
  );
20
21
  const [i18n] = useState(createI18nNext(lang));
21
22
 
@@ -0,0 +1,481 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import useSWR, { mutate } from 'swr';
3
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LOADING_FLAT } from '@/const/message';
6
+ import { chatService } from '@/services/chat';
7
+ import { messageService } from '@/services/message';
8
+ import { topicService } from '@/services/topic';
9
+ import { chatSelectors } from '@/store/chat/selectors';
10
+ import { ChatMessage } from '@/types/chatMessage';
11
+
12
+ import { useChatStore } from '../store';
13
+
14
+ // Mock service
15
+ vi.mock('@/services/message', () => ({
16
+ messageService: {
17
+ getMessages: vi.fn(),
18
+ updateMessageError: vi.fn(),
19
+ removeMessage: vi.fn(),
20
+ createAssistantMessage: vi.fn(() => Promise.resolve('content-content-content')),
21
+ removeMessages: vi.fn(() => Promise.resolve()),
22
+ create: vi.fn(() => Promise.resolve('new-message-id')),
23
+ updateMessageContent: vi.fn(),
24
+ clearAllMessage: vi.fn(() => Promise.resolve()),
25
+ },
26
+ }));
27
+ vi.mock('@/services/topic', () => ({
28
+ topicService: {
29
+ removeTopic: vi.fn(() => Promise.resolve()),
30
+ },
31
+ }));
32
+ vi.mock('@/services/chat', () => ({
33
+ chatService: {
34
+ createAssistantMessage: vi.fn(() => Promise.resolve('assistant-message')),
35
+ },
36
+ }));
37
+
38
+ vi.mock('@/store/chat/selectors', () => ({
39
+ chatSelectors: {
40
+ currentChats: vi.fn(),
41
+ },
42
+ }));
43
+
44
+ const realCoreProcessMessage = useChatStore.getState().coreProcessMessage;
45
+ const realRefreshMessages = useChatStore.getState().refreshMessages;
46
+ // Mock state
47
+ const mockState = {
48
+ activeId: 'session-id',
49
+ activeTopicId: 'topic-id',
50
+ messages: [],
51
+ refreshMessages: vi.fn(),
52
+ refreshTopic: vi.fn(),
53
+ coreProcessMessage: vi.fn(),
54
+ saveToTopic: vi.fn(),
55
+ };
56
+
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ useChatStore.setState(mockState, false);
60
+ });
61
+ afterEach(() => {
62
+ vi.restoreAllMocks();
63
+ });
64
+
65
+ describe('chatMessage actions', () => {
66
+ it('clearMessage should remove messages from the active session and topic', async () => {
67
+ const { result } = renderHook(() => useChatStore());
68
+ const clearSpy = vi.spyOn(result.current, 'clearMessage');
69
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
70
+
71
+ await act(async () => {
72
+ await result.current.clearMessage();
73
+ });
74
+
75
+ expect(clearSpy).toHaveBeenCalled();
76
+ expect(result.current.refreshMessages).toHaveBeenCalled();
77
+ expect(result.current.refreshTopic).toHaveBeenCalled();
78
+ expect(switchTopicSpy).toHaveBeenCalled();
79
+ });
80
+
81
+ it('deleteMessage should remove a message by id', async () => {
82
+ const { result } = renderHook(() => useChatStore());
83
+ const messageId = 'message-id';
84
+ const deleteSpy = vi.spyOn(result.current, 'deleteMessage');
85
+
86
+ await act(async () => {
87
+ await result.current.deleteMessage(messageId);
88
+ });
89
+
90
+ expect(deleteSpy).toHaveBeenCalledWith(messageId);
91
+ expect(result.current.refreshMessages).toHaveBeenCalled();
92
+ });
93
+
94
+ it('clearAllMessages should remove all messages', async () => {
95
+ const { result } = renderHook(() => useChatStore());
96
+ const clearAllSpy = vi.spyOn(result.current, 'clearAllMessages');
97
+
98
+ await act(async () => {
99
+ await result.current.clearAllMessages();
100
+ });
101
+
102
+ expect(clearAllSpy).toHaveBeenCalled();
103
+ expect(result.current.refreshMessages).toHaveBeenCalled();
104
+ });
105
+
106
+ it('updateInputMessage should update the input message state', () => {
107
+ const { result } = renderHook(() => useChatStore());
108
+ const newInputMessage = 'Updated message';
109
+ act(() => {
110
+ result.current.updateInputMessage(newInputMessage);
111
+ });
112
+
113
+ expect(result.current.inputMessage).toEqual(newInputMessage);
114
+ });
115
+
116
+ describe('clearMessage', () => {
117
+ beforeEach(() => {
118
+ vi.clearAllMocks(); // 清除 mocks
119
+ useChatStore.setState(mockState, false); // 重置 state
120
+ });
121
+
122
+ afterEach(() => {
123
+ vi.restoreAllMocks(); // 恢复所有模拟
124
+ });
125
+
126
+ it('should remove messages from the active session and topic, then refresh topics and messages', async () => {
127
+ const { result } = renderHook(() => useChatStore());
128
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
129
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
130
+
131
+ await act(async () => {
132
+ await result.current.clearMessage();
133
+ });
134
+
135
+ expect(mockState.refreshMessages).toHaveBeenCalled();
136
+ expect(refreshTopicSpy).toHaveBeenCalled();
137
+ expect(switchTopicSpy).toHaveBeenCalled();
138
+
139
+ // 检查 activeTopicId 是否被清除,需要在状态更新后进行检查
140
+ expect(useChatStore.getState().activeTopicId).toBeUndefined();
141
+ });
142
+
143
+ it('should call removeTopic if there is an activeTopicId', async () => {
144
+ const { result } = renderHook(() => useChatStore());
145
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
146
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
147
+
148
+ await act(async () => {
149
+ await result.current.clearMessage();
150
+ });
151
+
152
+ expect(mockState.activeTopicId).not.toBeUndefined(); // 确保在测试前 activeTopicId 存在
153
+ expect(refreshTopicSpy).toHaveBeenCalled();
154
+ expect(mockState.refreshMessages).toHaveBeenCalled();
155
+ expect(topicService.removeTopic).toHaveBeenCalledWith(mockState.activeTopicId);
156
+ expect(switchTopicSpy).toHaveBeenCalled();
157
+ });
158
+ });
159
+
160
+ describe('sendMessage', () => {
161
+ it('should not send message if there is no active session', async () => {
162
+ useChatStore.setState({ activeId: undefined });
163
+ const { result } = renderHook(() => useChatStore());
164
+ const message = 'Test message';
165
+
166
+ await act(async () => {
167
+ await result.current.sendMessage(message);
168
+ });
169
+
170
+ expect(messageService.create).not.toHaveBeenCalled();
171
+ expect(result.current.refreshMessages).not.toHaveBeenCalled();
172
+ expect(result.current.coreProcessMessage).not.toHaveBeenCalled();
173
+ });
174
+
175
+ it('should not send message if message is empty and there are no files', async () => {
176
+ const { result } = renderHook(() => useChatStore());
177
+ const message = '';
178
+
179
+ await act(async () => {
180
+ await result.current.sendMessage(message);
181
+ });
182
+
183
+ expect(messageService.create).not.toHaveBeenCalled();
184
+ expect(result.current.refreshMessages).not.toHaveBeenCalled();
185
+ expect(result.current.coreProcessMessage).not.toHaveBeenCalled();
186
+ });
187
+
188
+ it('should not send message if message is empty and there are empty files', async () => {
189
+ const { result } = renderHook(() => useChatStore());
190
+ const message = '';
191
+
192
+ await act(async () => {
193
+ await result.current.sendMessage(message, []);
194
+ });
195
+
196
+ expect(messageService.create).not.toHaveBeenCalled();
197
+ expect(result.current.refreshMessages).not.toHaveBeenCalled();
198
+ expect(result.current.coreProcessMessage).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it('should create message and call coreProcessMessage if message or files are provided', async () => {
202
+ const { result } = renderHook(() => useChatStore());
203
+ const message = 'Test message';
204
+ const files = [{ id: 'file-id', url: 'file-url' }];
205
+
206
+ // Mock messageService.create to resolve with a message id
207
+ (messageService.create as Mock).mockResolvedValue('new-message-id');
208
+
209
+ await act(async () => {
210
+ await result.current.sendMessage(message, files);
211
+ });
212
+
213
+ expect(messageService.create).toHaveBeenCalledWith({
214
+ content: message,
215
+ files: files.map((f) => f.id),
216
+ role: 'user',
217
+ sessionId: mockState.activeId,
218
+ topicId: mockState.activeTopicId,
219
+ });
220
+ expect(result.current.refreshMessages).toHaveBeenCalled();
221
+ expect(result.current.coreProcessMessage).toHaveBeenCalled();
222
+ });
223
+
224
+ // it('should auto-create topic and switch to it if enabled and threshold is reached', async () => {
225
+ // const { result } = renderHook(() => useChatStore());
226
+ // const message = 'Test message';
227
+ // const autoCreateTopicThreshold = 5;
228
+ // const enableAutoCreateTopic = true;
229
+ //
230
+ // // Mock state with the necessary settings
231
+ // useChatStore.setState({
232
+ // ...mockState,
233
+ // messages: Array(autoCreateTopicThreshold).fill({}), // Fill with dummy messages to reach threshold
234
+ // });
235
+ //
236
+ // // Mock messageService.create to resolve with a message id
237
+ // (messageService.create as vi.Mock).mockResolvedValue('new-message-id');
238
+ //
239
+ // await act(async () => {
240
+ // await result.current.sendMessage(message);
241
+ // });
242
+ //
243
+ // expect(result.current.saveToTopic).toHaveBeenCalled();
244
+ // expect(result.current.switchTopic).toHaveBeenCalled();
245
+ // });
246
+ // 其他可能的测试用例...
247
+ });
248
+
249
+ describe('resendMessage action', () => {
250
+ it('should resend a message by id and refresh messages', async () => {
251
+ const { result } = renderHook(() => useChatStore());
252
+ const messageId = 'message-id';
253
+
254
+ // Mock the currentChats selector to return a list that includes the message to be resent
255
+ (chatSelectors.currentChats as Mock).mockReturnValue([
256
+ // ... other messages
257
+ { id: messageId, role: 'user', content: 'Resend this message' },
258
+ // ... other messages
259
+ ]);
260
+
261
+ // Mock the coreProcessMessage function to resolve immediately
262
+ mockState.coreProcessMessage.mockResolvedValue(undefined);
263
+
264
+ await act(async () => {
265
+ await result.current.resendMessage(messageId);
266
+ });
267
+
268
+ expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
269
+ expect(mockState.coreProcessMessage).toHaveBeenCalledWith(expect.any(Array), messageId);
270
+ });
271
+
272
+ it('should not perform any action if the message id does not exist', async () => {
273
+ const { result } = renderHook(() => useChatStore());
274
+ const messageId = 'non-existing-message-id';
275
+
276
+ // Mock the currentChats selector to return a list that does not include the message to be resent
277
+ (chatSelectors.currentChats as Mock).mockReturnValue([
278
+ // ... other messages
279
+ ]);
280
+
281
+ await act(async () => {
282
+ await result.current.resendMessage(messageId);
283
+ });
284
+
285
+ expect(messageService.removeMessage).not.toHaveBeenCalledWith(messageId);
286
+ expect(mockState.coreProcessMessage).not.toHaveBeenCalled();
287
+ expect(mockState.refreshMessages).not.toHaveBeenCalled();
288
+ });
289
+ });
290
+
291
+ describe('updateMessageContent action', () => {
292
+ it('should call messageService.updateMessageContent with correct parameters', async () => {
293
+ const { result } = renderHook(() => useChatStore());
294
+ const messageId = 'message-id';
295
+ const newContent = 'Updated content';
296
+
297
+ await act(async () => {
298
+ await result.current.updateMessageContent(messageId, newContent);
299
+ });
300
+
301
+ expect(messageService.updateMessageContent).toHaveBeenCalledWith(messageId, newContent);
302
+ });
303
+
304
+ it('should dispatch message update action', async () => {
305
+ const { result } = renderHook(() => useChatStore());
306
+ const messageId = 'message-id';
307
+ const newContent = 'Updated content';
308
+ const dispatchMessageSpy = vi.spyOn(result.current, 'dispatchMessage');
309
+
310
+ await act(async () => {
311
+ await result.current.updateMessageContent(messageId, newContent);
312
+ });
313
+
314
+ expect(dispatchMessageSpy).toHaveBeenCalledWith({
315
+ id: messageId,
316
+ key: 'content',
317
+ type: 'updateMessage',
318
+ value: newContent,
319
+ });
320
+ });
321
+
322
+ it('should refresh messages after updating content', async () => {
323
+ const { result } = renderHook(() => useChatStore());
324
+ const messageId = 'message-id';
325
+ const newContent = 'Updated content';
326
+
327
+ await act(async () => {
328
+ await result.current.updateMessageContent(messageId, newContent);
329
+ });
330
+
331
+ expect(result.current.refreshMessages).toHaveBeenCalled();
332
+ });
333
+ });
334
+
335
+ describe('coreProcessMessage action', () => {
336
+ it('should handle the core AI message processing', async () => {
337
+ useChatStore.setState({ coreProcessMessage: realCoreProcessMessage });
338
+
339
+ const { result } = renderHook(() => useChatStore());
340
+ const userMessage = {
341
+ id: 'user-message-id',
342
+ role: 'user',
343
+ content: 'Hello, world!',
344
+ sessionId: mockState.activeId,
345
+ topicId: mockState.activeTopicId,
346
+ } as ChatMessage;
347
+ const messages = [userMessage];
348
+
349
+ // 模拟 AI 响应
350
+ const aiResponse = 'Hello, human!';
351
+ (chatService.createAssistantMessage as Mock).mockResolvedValue(aiResponse);
352
+
353
+ // 模拟消息创建
354
+ (messageService.create as Mock).mockResolvedValue('assistant-message-id');
355
+
356
+ await act(async () => {
357
+ await result.current.coreProcessMessage(messages, userMessage.id);
358
+ });
359
+
360
+ // 验证是否创建了代表 AI 响应的消息
361
+ expect(messageService.create).toHaveBeenCalledWith(
362
+ expect.objectContaining({
363
+ role: 'assistant',
364
+ content: LOADING_FLAT,
365
+ fromModel: expect.anything(),
366
+ parentId: userMessage.id,
367
+ sessionId: mockState.activeId,
368
+ topicId: mockState.activeTopicId,
369
+ }),
370
+ );
371
+
372
+ // 验证 AI 服务是否被调用
373
+ expect(chatService.createAssistantMessage).toHaveBeenCalled();
374
+
375
+ // 验证消息列表是否刷新
376
+ expect(mockState.refreshMessages).toHaveBeenCalled();
377
+ });
378
+ });
379
+
380
+ describe('stopGenerateMessage action', () => {
381
+ it('should stop generating message and set loading states correctly', async () => {
382
+ const { result } = renderHook(() => useChatStore());
383
+ const toggleChatLoadingSpy = vi.spyOn(result.current, 'toggleChatLoading');
384
+ const abortController = new AbortController();
385
+
386
+ // 设置模拟 abortController
387
+ useChatStore.setState({ abortController });
388
+
389
+ await act(async () => {
390
+ result.current.stopGenerateMessage();
391
+ });
392
+
393
+ expect(abortController.signal.aborted).toBe(true);
394
+ expect(toggleChatLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
395
+ });
396
+
397
+ it('should not do anything if there is no abortController', async () => {
398
+ const { result } = renderHook(() => useChatStore());
399
+
400
+ // 确保没有设置 abortController
401
+ useChatStore.setState({ abortController: undefined });
402
+
403
+ await act(async () => {
404
+ result.current.stopGenerateMessage();
405
+ });
406
+
407
+ // 由于没有 abortController,不应调用任何方法
408
+ expect(result.current.abortController).toBeUndefined();
409
+ });
410
+ });
411
+
412
+ describe('refreshMessages action', () => {
413
+ beforeEach(() => {
414
+ vi.mock('swr', async () => {
415
+ const actual = await vi.importActual('swr');
416
+ return {
417
+ ...(actual as any),
418
+ mutate: vi.fn(),
419
+ };
420
+ });
421
+ });
422
+ afterEach(() => {
423
+ // 在每个测试用例开始前恢复到实际的 SWR 实现
424
+ vi.resetAllMocks();
425
+ });
426
+ it('should refresh messages by calling mutate with current activeId and activeTopicId', async () => {
427
+ useChatStore.setState({ refreshMessages: realRefreshMessages });
428
+
429
+ const { result } = renderHook(() => useChatStore());
430
+ const activeId = useChatStore.getState().activeId;
431
+ const activeTopicId = useChatStore.getState().activeTopicId;
432
+
433
+ // 在这里,我们不需要再次模拟 mutate,因为它已经在顶部被模拟了
434
+ await act(async () => {
435
+ await result.current.refreshMessages();
436
+ });
437
+
438
+ // 确保 mutate 调用了正确的参数
439
+ expect(mutate).toHaveBeenCalledWith([activeId, activeTopicId]);
440
+ });
441
+ it('should handle errors during refreshing messages', async () => {
442
+ useChatStore.setState({ refreshMessages: realRefreshMessages });
443
+ const { result } = renderHook(() => useChatStore());
444
+
445
+ // 设置模拟错误
446
+ (mutate as Mock).mockImplementation(() => {
447
+ throw new Error('Mutate error');
448
+ });
449
+
450
+ await act(async () => {
451
+ await expect(result.current.refreshMessages()).rejects.toThrow('Mutate error');
452
+ });
453
+
454
+ // 确保恢复 mutate 的模拟,以免影响其他测试
455
+ (mutate as Mock).mockReset();
456
+ });
457
+ });
458
+
459
+ describe('useFetchMessages hook', () => {
460
+ // beforeEach(() => {
461
+ // vi.mocked(useSWR).mockRestore();
462
+ // });
463
+
464
+ it('should fetch messages for given session and topic ids', async () => {
465
+ const sessionId = 'session-id';
466
+ const topicId = 'topic-id';
467
+ const messages = [{ id: 'message-id', content: 'Hello' }];
468
+
469
+ // 设置模拟返回值
470
+ (messageService.getMessages as Mock).mockResolvedValue(messages);
471
+
472
+ // renderHook 在 @testing-library/react-hooks 中使用,所以我们使用 render 来替代
473
+ const { result } = renderHook(() => useChatStore().useFetchMessages(sessionId, topicId));
474
+
475
+ // 等待异步操作完成
476
+ await waitFor(() => {
477
+ expect(result.current.data).toEqual(messages);
478
+ });
479
+ });
480
+ });
481
+ });
@@ -146,10 +146,13 @@ export const chatMessage: StateCreator<
146
146
  },
147
147
  sendMessage: async (message, files) => {
148
148
  const { coreProcessMessage, activeTopicId, activeId } = get();
149
- if (!message || !activeId) return;
149
+ if (!activeId) return;
150
150
 
151
151
  const fileIdList = files?.map((f) => f.id);
152
152
 
153
+ // if message is empty and no files, then stop
154
+ if (!message && (!fileIdList || fileIdList?.length === 0)) return;
155
+
153
156
  let newMessage: CreateMessageParams = {
154
157
  content: message,
155
158
  // if message has attached with files, then add files to message and the agent
@@ -1,84 +0,0 @@
1
- import { createStyles } from 'antd-style';
2
-
3
- export const IMAGE_SIZE = 64;
4
- const imageBorderRaidus = 8;
5
-
6
- export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
7
- const closeIcon = cx(css`
8
- cursor: pointer;
9
-
10
- position: absolute;
11
- top: -6px;
12
- right: -6px;
13
-
14
- width: 16px;
15
- height: 16px;
16
-
17
- color: ${isDarkMode ? token.colorTextQuaternary : token.colorTextTertiary};
18
-
19
- opacity: 0;
20
- background: ${isDarkMode ? token.colorTextSecondary : token.colorBgContainer};
21
- border-radius: 50%;
22
-
23
- transition: all 0.2s ease-in;
24
-
25
- &:hover {
26
- scale: 1.2;
27
- }
28
- `);
29
-
30
- return {
31
- alwaysShowClose: css`
32
- opacity: 1 !important;
33
- `,
34
- closeIcon,
35
- container: css`
36
- cursor: pointer;
37
-
38
- position: relative;
39
-
40
- width: ${IMAGE_SIZE}px;
41
- height: ${IMAGE_SIZE}px;
42
-
43
- border-radius: ${imageBorderRaidus}px;
44
-
45
- &:hover {
46
- .${closeIcon} {
47
- opacity: 1;
48
- }
49
- }
50
- `,
51
- image: css`
52
- opacity: ${isDarkMode ? 0.6 : 1};
53
- object-fit: cover;
54
- border-radius: 8px;
55
- animation: fade-in 0.3s ease-in;
56
-
57
- @keyframes fade-in {
58
- from {
59
- scale: 1.2;
60
- opacity: 1;
61
- }
62
-
63
- to {
64
- scale: 1;
65
- opacity: 1;
66
- }
67
- }
68
- `,
69
- imageCtn: css`
70
- position: relative;
71
- `,
72
- imageWrapper: css`
73
- overflow: hidden;
74
- width: ${IMAGE_SIZE}px;
75
- height: ${IMAGE_SIZE}px;
76
- border-radius: ${imageBorderRaidus}px;
77
- `,
78
- notFound: css`
79
- color: ${token.colorTextSecondary};
80
- background: ${token.colorFillTertiary};
81
- border-radius: ${imageBorderRaidus}px;
82
- `,
83
- };
84
- });
@@ -1,73 +0,0 @@
1
- import { CloseCircleFilled } from '@ant-design/icons';
2
- import { Icon } from '@lobehub/ui';
3
- import { Skeleton } from 'antd';
4
- import { LucideImageOff } from 'lucide-react';
5
- import Image from 'next/image';
6
- import { memo } from 'react';
7
- import { Center, Flexbox } from 'react-layout-kit';
8
-
9
- import { useFileStore } from '@/store/file';
10
-
11
- import { IMAGE_SIZE, useStyles } from './FileItem.style';
12
-
13
- interface FileItemProps {
14
- alwaysShowClose?: boolean;
15
- editable: boolean;
16
- id: string;
17
- onClick: () => void;
18
- }
19
- const FileItem = memo<FileItemProps>(({ editable, id, onClick, alwaysShowClose }) => {
20
- const { styles, cx } = useStyles();
21
- const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
22
-
23
- const { data, isLoading } = useFetchFile(id);
24
-
25
- return (
26
- <Flexbox className={styles.container} onClick={onClick}>
27
- {isLoading ? (
28
- <Skeleton
29
- active
30
- title={{
31
- style: { borderRadius: 8, height: IMAGE_SIZE },
32
- width: IMAGE_SIZE,
33
- }}
34
- />
35
- ) : (
36
- <Flexbox className={styles.imageCtn}>
37
- <div className={styles.imageWrapper}>
38
- {data ? (
39
- <Image
40
- alt={data.name || ''}
41
- className={styles.image}
42
- fetchPriority={'high'}
43
- height={IMAGE_SIZE}
44
- loading={'lazy'}
45
- src={data.url}
46
- width={IMAGE_SIZE}
47
- />
48
- ) : (
49
- <Center className={styles.notFound} height={'100%'}>
50
- <Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
51
- </Center>
52
- )}
53
- </div>
54
- </Flexbox>
55
- )}
56
- {/* only show close icon when editable */}
57
- {editable && (
58
- <Center
59
- className={cx(styles.closeIcon, alwaysShowClose && styles.alwaysShowClose)}
60
- onClick={(e) => {
61
- e.stopPropagation();
62
-
63
- removeFile(id);
64
- }}
65
- >
66
- <CloseCircleFilled />
67
- </Center>
68
- )}
69
- </Flexbox>
70
- );
71
- });
72
-
73
- export default FileItem;
@@ -1,46 +0,0 @@
1
- import { createStyles } from 'antd-style';
2
- import { memo } from 'react';
3
- import Lightbox from 'react-spring-lightbox';
4
-
5
- import { filesSelectors, useFileStore } from '@/store/file';
6
-
7
- const useStyles = createStyles(({ css, token }) => ({
8
- wrapper: css`
9
- background: ${token.colorBgMask};
10
- backdrop-filter: blur(4px);
11
- `,
12
- }));
13
-
14
- interface LightBoxProps {
15
- index: number;
16
- items: string[];
17
- onNext: () => void;
18
- onOpenChange: (open: boolean) => void;
19
- onPrev: () => void;
20
- open: boolean;
21
- }
22
-
23
- const LightBox = memo<LightBoxProps>(({ onOpenChange, open, items, index, onNext, onPrev }) => {
24
- const { styles } = useStyles();
25
-
26
- const list = useFileStore(filesSelectors.getImageDetailByList(items));
27
- return (
28
- <Lightbox
29
- className={styles.wrapper}
30
- currentIndex={index}
31
- images={list.map((i) => ({
32
- alt: i.name,
33
- loading: 'lazy',
34
- src: i.url,
35
- }))}
36
- isOpen={open}
37
- onClose={() => {
38
- onOpenChange(false);
39
- }}
40
- onNext={onNext}
41
- onPrev={onPrev}
42
- />
43
- );
44
- });
45
-
46
- export default LightBox;
@@ -1,60 +0,0 @@
1
- import { memo, useState } from 'react';
2
- import { Flexbox } from 'react-layout-kit';
3
-
4
- import FileItem from './FileItem';
5
- import Lightbox from './Lightbox';
6
-
7
- // TODO:后续看情况是否做图片过多的滚动功能
8
- // const useStyles = createStyles(({ css }) => ({
9
- // container: css`
10
- // width: ${IMAGE_SIZE * 5 + 4 * 8 + IMAGE_SIZE / 2}px;
11
- // overflow-x: scroll;
12
- // height: ${IMAGE_SIZE + 12}px;
13
- // `,
14
- // }));
15
-
16
- interface FileListProps {
17
- alwaysShowClose?: boolean;
18
- editable?: boolean;
19
- items: string[];
20
- }
21
-
22
- const FileList = memo<FileListProps>(({ items, editable = true, alwaysShowClose }) => {
23
- // const { styles } = useStyles();
24
-
25
- const [showLightbox, setShowLightbox] = useState(false);
26
- const [currentImageIndex, setCurrentIndex] = useState(0);
27
-
28
- const gotoPrevious = () => currentImageIndex > 0 && setCurrentIndex(currentImageIndex - 1);
29
- const gotoNext = () =>
30
- currentImageIndex + 1 < items.length && setCurrentIndex(currentImageIndex + 1);
31
-
32
- return (
33
- <>
34
- <Flexbox align={'flex-end'} gap={8} horizontal>
35
- {items.map((i, index) => (
36
- <FileItem
37
- alwaysShowClose={alwaysShowClose}
38
- editable={editable}
39
- id={i}
40
- key={i}
41
- onClick={() => {
42
- setCurrentIndex(index);
43
- setShowLightbox(true);
44
- }}
45
- />
46
- ))}
47
- </Flexbox>
48
- <Lightbox
49
- index={currentImageIndex}
50
- items={items}
51
- onNext={gotoNext}
52
- onOpenChange={setShowLightbox}
53
- onPrev={gotoPrevious}
54
- open={showLightbox}
55
- />
56
- </>
57
- );
58
- });
59
-
60
- export default FileList;