@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 +54 -0
- package/README.md +33 -3
- package/README.zh-CN.md +33 -3
- package/package.json +1 -2
- package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +2 -2
- package/src/app/chat/(mobile)/features/ChatInput/Files.tsx +6 -5
- package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +2 -0
- package/src/app/chat/(mobile)/features/ChatInput/index.tsx +0 -2
- package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +1 -1
- package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +2 -2
- package/src/components/FileList/EditableFileList.tsx +35 -0
- package/src/components/FileList/FileGrid.tsx +36 -0
- package/src/components/FileList/FileItem.tsx +73 -0
- package/src/components/FileList/index.tsx +62 -0
- package/src/components/FileList/style.ts +24 -0
- package/src/features/AgentSetting/AgentPlugin/MarketList.tsx +1 -1
- package/src/layout/AppLayout.desktop.tsx +1 -0
- package/src/layout/GlobalLayout/Locale.tsx +1 -0
- package/src/store/chat/actions/message.test.ts +481 -0
- package/src/store/chat/actions/message.ts +4 -1
- package/src/app/chat/components/FileList/FileItem.style.ts +0 -84
- package/src/app/chat/components/FileList/FileItem.tsx +0 -73
- package/src/app/chat/components/FileList/Lightbox.tsx +0 -46
- package/src/app/chat/components/FileList/index.tsx +0 -60
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
|
+
[](#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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 <
|
|
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
|
|
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
|
-
<
|
|
12
|
-
<
|
|
13
|
-
</
|
|
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 '@/
|
|
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
|
|
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
|
|
|
@@ -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 (!
|
|
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;
|