@lobehub/chat 0.100.4 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +1 -0
- package/CHANGELOG.md +50 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/__mocks__/zustand/traditional.ts +25 -0
- package/package.json +7 -3
- package/src/app/api/files/image/imgur.ts +72 -0
- package/src/app/api/files/image/route.ts +42 -0
- package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
- package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
- package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +69 -0
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +60 -19
- package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
- package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
- package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
- package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
- package/src/app/chat/components/FileList/FileItem.tsx +69 -0
- package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
- package/src/app/chat/components/FileList/index.tsx +63 -0
- package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +48 -0
- package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
- package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
- package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
- package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
- package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
- package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
- package/src/app/chat/features/ChatInput/useSend.ts +21 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
- package/src/config/__tests__/client.test.ts +76 -0
- package/src/config/__tests__/server.test.ts +52 -0
- package/src/config/server.ts +8 -0
- package/src/const/layoutTokens.ts +1 -1
- package/src/const/llm.ts +3 -0
- package/src/const/settings.ts +2 -0
- package/src/database/core/__tests__/db.test.ts +77 -0
- package/src/database/core/__tests__/model.test.ts +55 -0
- package/src/database/core/db.ts +25 -0
- package/src/database/core/index.ts +1 -0
- package/src/database/core/model.ts +49 -0
- package/src/database/core/schema.ts +9 -0
- package/src/database/models/file.test.ts +71 -0
- package/src/database/models/file.ts +26 -0
- package/src/locales/default/chat.ts +5 -0
- package/src/services/__tests__/file.test.ts +88 -0
- package/src/services/chatModel.ts +11 -6
- package/src/services/file.ts +36 -0
- package/src/store/files/index.ts +2 -0
- package/src/store/files/initialState.ts +7 -0
- package/src/store/files/selectors.test.ts +75 -0
- package/src/store/files/selectors.ts +34 -0
- package/src/store/files/slices/images/action.test.ts +186 -0
- package/src/store/files/slices/images/action.ts +83 -0
- package/src/store/files/slices/images/index.ts +3 -0
- package/src/store/files/slices/images/initialState.ts +11 -0
- package/src/store/files/store.ts +27 -0
- package/src/store/global/selectors/settings.test.ts +1 -3
- package/src/store/session/slices/agentConfig/selectors.ts +7 -1
- package/src/store/session/slices/chat/actions/message.ts +83 -18
- package/src/store/session/slices/chat/initialState.ts +4 -1
- package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
- package/src/store/session/slices/chat/reducers/files.ts +37 -0
- package/src/store/session/slices/chat/selectors/chat.ts +3 -2
- package/src/store/session/slices/session/initialState.ts +1 -0
- package/src/store/session/slices/session/reducers/session.test.ts +98 -1
- package/src/store/session/slices/session/reducers/session.ts +26 -1
- package/src/types/chatMessage.ts +3 -2
- package/src/types/database/db.ts +4 -0
- package/src/types/database/files.ts +38 -0
- package/src/types/files.ts +8 -0
- package/src/types/openai/chat.ts +15 -1
- package/src/types/session.ts +1 -0
- package/tests/setup.ts +8 -0
- package/vitest.config.ts +2 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -35
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
- package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
- /package/src/app/chat/features/{ChatInputContent → ChatInput}/Topic/index.tsx +0 -0
package/.eslintrc.js
CHANGED
|
@@ -13,5 +13,6 @@ config.rules['no-extra-boolean-cast'] = 0;
|
|
|
13
13
|
config.rules['unicorn/no-useless-undefined'] = 0;
|
|
14
14
|
config.rules['react/no-unknown-property'] = 0;
|
|
15
15
|
config.rules['unicorn/prefer-ternary'] = 0;
|
|
16
|
+
config.rules['unicorn/prefer-spread'] = 0;
|
|
16
17
|
|
|
17
18
|
module.exports = config;
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.101.0](https://github.com/lobehub/lobe-chat/compare/v0.100.5...v0.101.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-11-14**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: Support upload images to chat with gpt4-vision model.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: Support upload images to chat with gpt4-vision model, closes [#440](https://github.com/lobehub/lobe-chat/issues/440) ([858d047](https://github.com/lobehub/lobe-chat/commit/858d047))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 0.100.5](https://github.com/lobehub/lobe-chat/compare/v0.100.4...v0.100.5)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2023-11-11**</sup>
|
|
33
|
+
|
|
34
|
+
#### ♻ Code Refactoring
|
|
35
|
+
|
|
36
|
+
- **misc**: Refactor the input area to suit the files upload feature.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### Code refactoring
|
|
44
|
+
|
|
45
|
+
- **misc**: Refactor the input area to suit the files upload feature, closes [#442](https://github.com/lobehub/lobe-chat/issues/442) ([57a61fd](https://github.com/lobehub/lobe-chat/commit/57a61fd))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
### [Version 0.100.4](https://github.com/lobehub/lobe-chat/compare/v0.100.3...v0.100.4)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2023-11-11**</sup>
|
package/README.md
CHANGED
|
@@ -126,12 +126,12 @@ In our agent market. We have accumulated a large number of practical, prompt age
|
|
|
126
126
|
|
|
127
127
|
| Recent Submits | Description |
|
|
128
128
|
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
129
|
+
| [Academic Writing Enhancement Bot](https://chat-preview.lobehub.com/market?agent=academic-writing-eb)<br/><sup>By **[Ruler27](https://github.com/Ruler27)** on **2023-11-11**</sup> | Academic English spelling and rhetoric refinement.<br/>`proofreading` `rhetoric` `academic` `research` `english` `editing` |
|
|
129
130
|
| [LobeChat Technical Documentation Expert](https://chat-preview.lobehub.com/market?agent=lobe-chat-develop-doc-writter)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2023-11-05**</sup> | LobeChat is an AI conversation application built with the Next.js framework. I will help you write the development documentation for LobeChat.<br/>`development-documentation` `technical-introduction` `next-js` `react` `lobe-chat` |
|
|
130
131
|
| [Sketch Feature Summary Expert](https://chat-preview.lobehub.com/market?agent=sketch-changelog-highlighter)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2023-11-02**</sup> | Skilled at extracting key changes from Sketch release notes<br/>`experience-design` `sketch` `update` `feature` `text-summary` |
|
|
131
132
|
| [Master of Debating](https://chat-preview.lobehub.com/market?agent=tqg-20231026)<br/><sup>By **[cake79](https://github.com/cake79)** on **2023-10-26**</sup> | Simulates people who enjoy debating and can express counterarguments to any user input<br/>`writing` `dialogue` |
|
|
132
|
-
| [Graph Generator](https://chat-preview.lobehub.com/market?agent=graph-generator)<br/><sup>By **[choldrim](https://github.com/choldrim)** on **2023-10-23**</sup> | Automatic Graph Generator<br/>`graph` |
|
|
133
133
|
|
|
134
|
-
> 📊 Total agents: [<kbd>**
|
|
134
|
+
> 📊 Total agents: [<kbd>**39**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
135
135
|
|
|
136
136
|
<!-- AGENT LIST -->
|
|
137
137
|
|
package/README.zh-CN.md
CHANGED
|
@@ -124,12 +124,12 @@ LobeChat 是一个开源的、可扩展的([Function Calling][fc-link])高
|
|
|
124
124
|
|
|
125
125
|
| 最近新增 | 助手说明 |
|
|
126
126
|
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| [学术写作增强机器人](https://chat-preview.lobehub.com/market?agent=academic-writing-eb)<br/><sup>By **[Ruler27](https://github.com/Ruler27)** on **2023-11-11**</sup> | 学术英语拼写和修辞改进。<br/>`校对` `修辞` `学术` `研究` `英文` `润色` |
|
|
127
128
|
| [LobeChat 技术文档专家](https://chat-preview.lobehub.com/market?agent=lobe-chat-develop-doc-writter)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2023-11-05**</sup> | LobeChat 是一个 Next.js 框架构建的 AI 会话应用,我将会帮你一起书写 LobeChat 的开发文档<br/>`开发文档` `技术介绍` `next-js` `react` `lobe-chat` |
|
|
128
129
|
| [Sketch 特性总结专家](https://chat-preview.lobehub.com/market?agent=sketch-changelog-highlighter)<br/><sup>By **[arvinxx](https://github.com/arvinxx)** on **2023-11-02**</sup> | 擅长从 Sketch 的更新日志中提取关键变更要点<br/>`体验设计` `sketch` `更新` `特性` `文本总结` |
|
|
129
130
|
| [抬杠高手](https://chat-preview.lobehub.com/market?agent=tqg-20231026)<br/><sup>By **[cake79](https://github.com/cake79)** on **2023-10-26**</sup> | 模拟那些喜欢抬杠的人,能对用户输入的任何观点进行抬杠表达的角色<br/>`写作` `对话` |
|
|
130
|
-
| [图表生成器](https://chat-preview.lobehub.com/market?agent=graph-generator)<br/><sup>By **[choldrim](https://github.com/choldrim)** on **2023-10-23**</sup> | 自动图表生成器<br/>`图表` |
|
|
131
131
|
|
|
132
|
-
> 📊 Total agents: [<kbd>**
|
|
132
|
+
> 📊 Total agents: [<kbd>**39**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
|
|
133
133
|
|
|
134
134
|
<!-- AGENT LIST -->
|
|
135
135
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { act } from 'react-dom/test-utils';
|
|
2
|
+
import { beforeEach } from 'vitest';
|
|
3
|
+
import { createWithEqualityFn as actualCreate } from 'zustand/traditional';
|
|
4
|
+
|
|
5
|
+
// a variable to hold reset functions for all stores declared in the app
|
|
6
|
+
const storeResetFns = new Set<() => void>();
|
|
7
|
+
|
|
8
|
+
// when creating a store, we get its initial state, create a reset function and add it in the set
|
|
9
|
+
const createImpl = (createState: any) => {
|
|
10
|
+
const store = actualCreate(createState, Object.is);
|
|
11
|
+
const initialState = store.getState();
|
|
12
|
+
storeResetFns.add(() => store.setState(initialState, true));
|
|
13
|
+
return store;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Reset all stores after each test run
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
act(() =>
|
|
19
|
+
{ for (const resetFn of storeResetFns) {
|
|
20
|
+
resetFn();
|
|
21
|
+
} },
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const createWithEqualityFn = (f: any) => (f === undefined ? createImpl : createImpl(f));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.0",
|
|
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",
|
|
@@ -73,13 +73,14 @@
|
|
|
73
73
|
"@lobehub/ui": "latest",
|
|
74
74
|
"@vercel/analytics": "^1",
|
|
75
75
|
"ahooks": "^3",
|
|
76
|
-
"ai": "2.2.
|
|
76
|
+
"ai": "^2.2.22",
|
|
77
77
|
"antd": "^5",
|
|
78
78
|
"antd-style": "^3",
|
|
79
79
|
"brotli-wasm": "^1",
|
|
80
80
|
"chroma-js": "^2",
|
|
81
81
|
"copy-to-clipboard": "^3",
|
|
82
82
|
"dayjs": "^1",
|
|
83
|
+
"dexie": "^3",
|
|
83
84
|
"fast-deep-equal": "^3",
|
|
84
85
|
"gpt-tokenizer": "^2",
|
|
85
86
|
"i18next": "^23",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"modern-screenshot": "^4",
|
|
94
95
|
"nanoid": "^5",
|
|
95
96
|
"next": "14.0.1",
|
|
96
|
-
"openai": "
|
|
97
|
+
"openai": "^4.17.3",
|
|
97
98
|
"polished": "^4",
|
|
98
99
|
"posthog-js": "^1",
|
|
99
100
|
"query-string": "^8",
|
|
@@ -104,6 +105,7 @@
|
|
|
104
105
|
"react-intersection-observer": "^9",
|
|
105
106
|
"react-layout-kit": "^1",
|
|
106
107
|
"react-lazy-load": "^4",
|
|
108
|
+
"react-spring-lightbox": "^1",
|
|
107
109
|
"remark": "^14",
|
|
108
110
|
"remark-gfm": "^3",
|
|
109
111
|
"remark-html": "^15",
|
|
@@ -116,6 +118,7 @@
|
|
|
116
118
|
"use-merge-value": "^1",
|
|
117
119
|
"utility-types": "^3",
|
|
118
120
|
"uuid": "^9",
|
|
121
|
+
"zod": "^3",
|
|
119
122
|
"zustand": "^4.4",
|
|
120
123
|
"zustand-utils": "^1"
|
|
121
124
|
},
|
|
@@ -144,6 +147,7 @@
|
|
|
144
147
|
"consola": "^3",
|
|
145
148
|
"dpdm": "^3",
|
|
146
149
|
"eslint": "^8",
|
|
150
|
+
"fake-indexeddb": "^5",
|
|
147
151
|
"husky": "^8",
|
|
148
152
|
"jsdom": "^22",
|
|
149
153
|
"lint-staged": "^15",
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getServerConfig } from '@/config/server';
|
|
2
|
+
|
|
3
|
+
interface UploadResponse {
|
|
4
|
+
data: UploadData;
|
|
5
|
+
status: number;
|
|
6
|
+
success: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UploadData {
|
|
10
|
+
account_id: any;
|
|
11
|
+
account_url: any;
|
|
12
|
+
ad_type: any;
|
|
13
|
+
ad_url: any;
|
|
14
|
+
animated: boolean;
|
|
15
|
+
bandwidth: number;
|
|
16
|
+
datetime: number;
|
|
17
|
+
deletehash: string;
|
|
18
|
+
description: any;
|
|
19
|
+
favorite: boolean;
|
|
20
|
+
has_sound: boolean;
|
|
21
|
+
height: number;
|
|
22
|
+
hls: string;
|
|
23
|
+
id: string;
|
|
24
|
+
in_gallery: boolean;
|
|
25
|
+
in_most_viral: boolean;
|
|
26
|
+
is_ad: boolean;
|
|
27
|
+
link: string;
|
|
28
|
+
mp4: string;
|
|
29
|
+
name: string;
|
|
30
|
+
nsfw: any;
|
|
31
|
+
section: any;
|
|
32
|
+
size: number;
|
|
33
|
+
tags: any[];
|
|
34
|
+
title: any;
|
|
35
|
+
type: string;
|
|
36
|
+
views: number;
|
|
37
|
+
vote: any;
|
|
38
|
+
width: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Imgur {
|
|
42
|
+
clientId: string;
|
|
43
|
+
api = 'https://api.imgur.com/3';
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
this.clientId = getServerConfig().IMGUR_CLIENT_ID;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async upload(image: Blob) {
|
|
50
|
+
const formData = new FormData();
|
|
51
|
+
|
|
52
|
+
formData.append('image', image, 'image.png');
|
|
53
|
+
|
|
54
|
+
const res = await fetch(`${this.api}/upload`, {
|
|
55
|
+
body: formData,
|
|
56
|
+
headers: {
|
|
57
|
+
Authorization: `Client-ID ${this.clientId}`,
|
|
58
|
+
},
|
|
59
|
+
method: 'POST',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
console.log(await res.text());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data: UploadResponse = await res.json();
|
|
67
|
+
if (data.success) {
|
|
68
|
+
return data.data.link;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Imgur } from './imgur';
|
|
2
|
+
|
|
3
|
+
const updateByImgur = async ({ url, blob }: { blob?: Blob; url?: string }) => {
|
|
4
|
+
let imageBlob: Blob;
|
|
5
|
+
|
|
6
|
+
if (url) {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
imageBlob = await res.blob();
|
|
9
|
+
} else if (blob) {
|
|
10
|
+
imageBlob = blob;
|
|
11
|
+
} else {
|
|
12
|
+
// TODO: error handle
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const imgur = new Imgur();
|
|
17
|
+
|
|
18
|
+
return await imgur.upload(imageBlob);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const POST = async (req: Request) => {
|
|
22
|
+
const { url } = await req.json();
|
|
23
|
+
|
|
24
|
+
const cdnUrl = await updateByImgur({ url });
|
|
25
|
+
|
|
26
|
+
return new Response(JSON.stringify({ url: cdnUrl }));
|
|
27
|
+
};
|
|
28
|
+
// import { Imgur } from './imgur';
|
|
29
|
+
|
|
30
|
+
export const runtime = 'edge';
|
|
31
|
+
|
|
32
|
+
// export const POST = async (req: Request) => {
|
|
33
|
+
// const { url } = await req.json();
|
|
34
|
+
//
|
|
35
|
+
// const imgur = new Imgur();
|
|
36
|
+
//
|
|
37
|
+
// const image = await fetch(url);
|
|
38
|
+
//
|
|
39
|
+
// const cdnUrl = await imgur.upload(await image.blob());
|
|
40
|
+
//
|
|
41
|
+
// return new Response(JSON.stringify({ url: cdnUrl }));
|
|
42
|
+
// };
|
|
@@ -14,11 +14,12 @@ export const createChatCompletion = async ({ payload, openai }: CreateChatComple
|
|
|
14
14
|
// ============ 1. preprocess messages ============ //
|
|
15
15
|
const { messages, ...params } = payload;
|
|
16
16
|
|
|
17
|
+
// remove unnecessary fields like `plugins` or `files` by lobe-chat
|
|
17
18
|
const formatMessages = messages.map((m) => ({
|
|
18
19
|
content: m.content,
|
|
19
20
|
name: m.name,
|
|
20
21
|
role: m.role,
|
|
21
|
-
}));
|
|
22
|
+
})) as OpenAI.ChatCompletionMessageParam[];
|
|
22
23
|
|
|
23
24
|
// ============ 2. send api ============ //
|
|
24
25
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
|
2
|
+
import { createStyles } from 'antd-style';
|
|
3
|
+
import { FileImage, FileText, FileUpIcon } from 'lucide-react';
|
|
4
|
+
import { rgba } from 'polished';
|
|
5
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import { useFileStore } from '@/store/files';
|
|
10
|
+
|
|
11
|
+
const useStyles = createStyles(({ css, token, stylish }) => {
|
|
12
|
+
return {
|
|
13
|
+
container: css`
|
|
14
|
+
width: 300px;
|
|
15
|
+
height: 300px;
|
|
16
|
+
padding: 16px;
|
|
17
|
+
|
|
18
|
+
color: ${token.colorWhite};
|
|
19
|
+
|
|
20
|
+
background: ${token.geekblue};
|
|
21
|
+
border-radius: 16px;
|
|
22
|
+
box-shadow:
|
|
23
|
+
${rgba(token.geekblue, 0.1)} 0 1px 1px 0 inset,
|
|
24
|
+
${rgba(token.geekblue, 0.1)} 0 50px 100px -20px,
|
|
25
|
+
${rgba(token.geekblue, 0.3)} 0 30px 60px -30px;
|
|
26
|
+
`,
|
|
27
|
+
content: css`
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
padding: 16px;
|
|
31
|
+
|
|
32
|
+
border: 2px dashed ${token.colorWhite};
|
|
33
|
+
border-radius: 12px;
|
|
34
|
+
`,
|
|
35
|
+
desc: css`
|
|
36
|
+
color: ${rgba(token.colorTextLightSolid, 0.6)};
|
|
37
|
+
`,
|
|
38
|
+
title: css`
|
|
39
|
+
font-size: 24px;
|
|
40
|
+
font-weight: bold;
|
|
41
|
+
`,
|
|
42
|
+
wrapper: css`
|
|
43
|
+
position: fixed;
|
|
44
|
+
z-index: 10000000;
|
|
45
|
+
top: 0;
|
|
46
|
+
left: 0;
|
|
47
|
+
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
|
|
51
|
+
transition: all 0.3s ease-in-out;
|
|
52
|
+
|
|
53
|
+
background: ${token.colorBgMask};
|
|
54
|
+
${stylish.blur};
|
|
55
|
+
`,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const handleDragOver = (e: DragEvent) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const DragUpload = memo(() => {
|
|
64
|
+
const { styles } = useStyles();
|
|
65
|
+
const { t } = useTranslation('chat');
|
|
66
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
67
|
+
// When a file is dragged to a different area, the 'dragleave' event may be triggered,
|
|
68
|
+
// causing isDragging to be mistakenly set to false.
|
|
69
|
+
// to fix this issue, use a counter to ensure the status change only when drag event left the browser window .
|
|
70
|
+
const dragCounter = useRef(0);
|
|
71
|
+
|
|
72
|
+
const uploadFile = useFileStore((s) => s.uploadFile);
|
|
73
|
+
|
|
74
|
+
const uploadImages = async (fileList: FileList | undefined) => {
|
|
75
|
+
if (!fileList || fileList.length === 0) return;
|
|
76
|
+
|
|
77
|
+
const pools = Array.from(fileList).map(async (file) => {
|
|
78
|
+
// skip none-file items
|
|
79
|
+
if (!file.type.startsWith('image')) return;
|
|
80
|
+
await uploadFile(file);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await Promise.all(pools);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleDragEnter = (e: DragEvent) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
|
|
89
|
+
dragCounter.current += 1;
|
|
90
|
+
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
|
|
91
|
+
setIsDragging(true);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleDragLeave = (e: DragEvent) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
|
|
98
|
+
// reset counter
|
|
99
|
+
dragCounter.current -= 1;
|
|
100
|
+
|
|
101
|
+
if (dragCounter.current === 0) {
|
|
102
|
+
setIsDragging(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleDrop = async (e: DragEvent) => {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
// reset counter
|
|
109
|
+
dragCounter.current = 0;
|
|
110
|
+
|
|
111
|
+
setIsDragging(false);
|
|
112
|
+
|
|
113
|
+
// get filesList
|
|
114
|
+
// TODO: support folder files upload
|
|
115
|
+
const files = e.dataTransfer?.files;
|
|
116
|
+
|
|
117
|
+
// upload files
|
|
118
|
+
uploadImages(files);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handlePaste = (event: ClipboardEvent) => {
|
|
122
|
+
// get files from clipboard
|
|
123
|
+
|
|
124
|
+
const files = event.clipboardData?.files;
|
|
125
|
+
|
|
126
|
+
uploadImages(files);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
window.addEventListener('dragenter', handleDragEnter);
|
|
131
|
+
window.addEventListener('dragover', handleDragOver);
|
|
132
|
+
window.addEventListener('dragleave', handleDragLeave);
|
|
133
|
+
window.addEventListener('drop', handleDrop);
|
|
134
|
+
window.addEventListener('paste', handlePaste);
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
window.removeEventListener('dragenter', handleDragEnter);
|
|
138
|
+
window.removeEventListener('dragover', handleDragOver);
|
|
139
|
+
window.removeEventListener('dragleave', handleDragLeave);
|
|
140
|
+
window.removeEventListener('drop', handleDrop);
|
|
141
|
+
window.removeEventListener('paste', handlePaste);
|
|
142
|
+
};
|
|
143
|
+
}, []);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
isDragging && (
|
|
147
|
+
<Center className={styles.wrapper}>
|
|
148
|
+
<div className={styles.container}>
|
|
149
|
+
<Center className={styles.content} gap={40}>
|
|
150
|
+
<Flexbox horizontal>
|
|
151
|
+
<Icon icon={FileImage} size={{ fontSize: 64, strokeWidth: 1 }} />
|
|
152
|
+
<Icon icon={FileUpIcon} size={{ fontSize: 64, strokeWidth: 1 }} />
|
|
153
|
+
<Icon icon={FileText} size={{ fontSize: 64, strokeWidth: 1 }} />
|
|
154
|
+
</Flexbox>
|
|
155
|
+
<Flexbox align={'center'} gap={8} style={{ textAlign: 'center' }}>
|
|
156
|
+
<Flexbox className={styles.title}>{t('upload.dragTitle')}</Flexbox>
|
|
157
|
+
<Flexbox className={styles.desc}>{t('upload.dragDesc')}</Flexbox>
|
|
158
|
+
</Flexbox>
|
|
159
|
+
</Center>
|
|
160
|
+
</div>
|
|
161
|
+
</Center>
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
export default DragUpload;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
|
|
3
|
+
import FileList from '@/app/chat/components/FileList';
|
|
4
|
+
import { useFileStore } from '@/store/files';
|
|
5
|
+
|
|
6
|
+
export const LocalFiles = memo(() => {
|
|
7
|
+
const inputFilesList = useFileStore((s) => s.inputFilesList);
|
|
8
|
+
|
|
9
|
+
return <FileList items={inputFilesList} />;
|
|
10
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
|
2
|
+
import { Button } from 'antd';
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
4
|
+
import { ArrowBigUp, CornerDownLeft, Loader2 } from 'lucide-react';
|
|
5
|
+
import { memo } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Flexbox } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
|
|
10
|
+
import { useSendMessage } from '@/app/chat/features/ChatInput/useSend';
|
|
11
|
+
import { useSessionStore } from '@/store/session';
|
|
12
|
+
import { agentSelectors } from '@/store/session/selectors';
|
|
13
|
+
|
|
14
|
+
import { LocalFiles } from './LocalFiles';
|
|
15
|
+
|
|
16
|
+
const Footer = memo(() => {
|
|
17
|
+
const { t } = useTranslation('chat');
|
|
18
|
+
const theme = useTheme();
|
|
19
|
+
const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
|
|
20
|
+
|
|
21
|
+
const onSend = useSendMessage();
|
|
22
|
+
const canUpload = useSessionStore(agentSelectors.modelHasVisionAbility);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Flexbox
|
|
26
|
+
align={'end'}
|
|
27
|
+
distribution={'space-between'}
|
|
28
|
+
flex={'none'}
|
|
29
|
+
gap={8}
|
|
30
|
+
horizontal
|
|
31
|
+
padding={'0 24px'}
|
|
32
|
+
>
|
|
33
|
+
<Flexbox>{canUpload && <LocalFiles />}</Flexbox>
|
|
34
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
35
|
+
<Flexbox
|
|
36
|
+
gap={4}
|
|
37
|
+
horizontal
|
|
38
|
+
style={{ color: theme.colorTextDescription, fontSize: 12, marginRight: 12 }}
|
|
39
|
+
>
|
|
40
|
+
<Icon icon={CornerDownLeft} />
|
|
41
|
+
<span>{t('send')}</span>
|
|
42
|
+
<span>/</span>
|
|
43
|
+
<Flexbox horizontal>
|
|
44
|
+
<Icon icon={ArrowBigUp} />
|
|
45
|
+
<Icon icon={CornerDownLeft} />
|
|
46
|
+
</Flexbox>
|
|
47
|
+
<span>{t('warp')}</span>
|
|
48
|
+
</Flexbox>
|
|
49
|
+
<SaveTopic />
|
|
50
|
+
{loading ? (
|
|
51
|
+
<Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop}>
|
|
52
|
+
{t('stop')}
|
|
53
|
+
</Button>
|
|
54
|
+
) : (
|
|
55
|
+
<Button onClick={() => onSend()} type={'primary'}>
|
|
56
|
+
{t('send')}
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
59
|
+
</Flexbox>
|
|
60
|
+
</Flexbox>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export default Footer;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { TextArea } from '@lobehub/ui';
|
|
2
|
+
import { createStyles } from 'antd-style';
|
|
3
|
+
import { memo, useRef } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
import { useSessionStore } from '@/store/session';
|
|
7
|
+
|
|
8
|
+
import { useSendMessage } from '../../../features/ChatInput/useSend';
|
|
9
|
+
|
|
10
|
+
const useStyles = createStyles(({ css }) => {
|
|
11
|
+
return {
|
|
12
|
+
textarea: css`
|
|
13
|
+
height: 100% !important;
|
|
14
|
+
padding: 0 24px;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
`,
|
|
17
|
+
textareaContainer: css`
|
|
18
|
+
position: relative;
|
|
19
|
+
flex: 1;
|
|
20
|
+
`,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const InputArea = memo(() => {
|
|
25
|
+
const { t } = useTranslation('common');
|
|
26
|
+
const { cx, styles } = useStyles();
|
|
27
|
+
|
|
28
|
+
const isChineseInput = useRef(false);
|
|
29
|
+
|
|
30
|
+
const [loading, message, updateInputMessage] = useSessionStore((s) => [
|
|
31
|
+
!!s.chatLoadingId,
|
|
32
|
+
s.inputMessage,
|
|
33
|
+
s.updateInputMessage,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const handleSend = useSendMessage();
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cx(styles.textareaContainer)}>
|
|
40
|
+
<TextArea
|
|
41
|
+
className={styles.textarea}
|
|
42
|
+
onBlur={(e) => {
|
|
43
|
+
updateInputMessage(e.target.value);
|
|
44
|
+
}}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
updateInputMessage(e.target.value);
|
|
47
|
+
}}
|
|
48
|
+
onCompositionEnd={() => {
|
|
49
|
+
isChineseInput.current = false;
|
|
50
|
+
}}
|
|
51
|
+
onCompositionStart={() => {
|
|
52
|
+
isChineseInput.current = true;
|
|
53
|
+
}}
|
|
54
|
+
onPressEnter={(e) => {
|
|
55
|
+
if (!loading && !e.shiftKey && !isChineseInput.current) {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
handleSend();
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
placeholder={t('sendPlaceholder', { ns: 'chat' })}
|
|
61
|
+
resize={false}
|
|
62
|
+
type="pure"
|
|
63
|
+
value={message}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export default InputArea;
|