@lobehub/chat 0.100.5 → 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.
Files changed (68) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/__mocks__/zustand/traditional.ts +25 -0
  6. package/package.json +7 -3
  7. package/src/app/api/files/image/imgur.ts +72 -0
  8. package/src/app/api/files/image/route.ts +42 -0
  9. package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
  10. package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
  12. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
  13. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +3 -3
  14. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +38 -30
  15. package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
  16. package/src/app/chat/components/FileList/FileItem.tsx +69 -0
  17. package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
  18. package/src/app/chat/components/FileList/index.tsx +63 -0
  19. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +13 -4
  20. package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
  21. package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
  22. package/src/app/chat/{(desktop)/features → features}/ChatInput/useSend.ts +5 -1
  23. package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
  24. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
  25. package/src/config/__tests__/client.test.ts +76 -0
  26. package/src/config/__tests__/server.test.ts +52 -0
  27. package/src/config/server.ts +8 -0
  28. package/src/const/layoutTokens.ts +1 -1
  29. package/src/const/llm.ts +3 -0
  30. package/src/const/settings.ts +2 -0
  31. package/src/database/core/__tests__/db.test.ts +77 -0
  32. package/src/database/core/__tests__/model.test.ts +55 -0
  33. package/src/database/core/db.ts +25 -0
  34. package/src/database/core/index.ts +1 -0
  35. package/src/database/core/model.ts +49 -0
  36. package/src/database/core/schema.ts +9 -0
  37. package/src/database/models/file.test.ts +71 -0
  38. package/src/database/models/file.ts +26 -0
  39. package/src/locales/default/chat.ts +5 -0
  40. package/src/services/__tests__/file.test.ts +88 -0
  41. package/src/services/chatModel.ts +11 -6
  42. package/src/services/file.ts +36 -0
  43. package/src/store/files/index.ts +2 -0
  44. package/src/store/files/initialState.ts +7 -0
  45. package/src/store/files/selectors.test.ts +75 -0
  46. package/src/store/files/selectors.ts +34 -0
  47. package/src/store/files/slices/images/action.test.ts +186 -0
  48. package/src/store/files/slices/images/action.ts +83 -0
  49. package/src/store/files/slices/images/index.ts +3 -0
  50. package/src/store/files/slices/images/initialState.ts +11 -0
  51. package/src/store/files/store.ts +27 -0
  52. package/src/store/global/selectors/settings.test.ts +1 -3
  53. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  54. package/src/store/session/slices/chat/actions/message.ts +78 -17
  55. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  56. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  57. package/src/store/session/slices/session/initialState.ts +1 -0
  58. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  59. package/src/store/session/slices/session/reducers/session.ts +26 -1
  60. package/src/types/chatMessage.ts +3 -2
  61. package/src/types/database/db.ts +4 -0
  62. package/src/types/database/files.ts +38 -0
  63. package/src/types/files.ts +8 -0
  64. package/src/types/openai/chat.ts +15 -1
  65. package/src/types/session.ts +1 -0
  66. package/tests/setup.ts +8 -0
  67. package/vitest.config.ts +2 -0
  68. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -63
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,31 @@
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
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ### [Version 0.100.5](https://github.com/lobehub/lobe-chat/compare/v0.100.4...v0.100.5)
6
31
 
7
32
  <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>**38**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
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>**38**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
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.100.5",
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.20",
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": "~4.15.0",
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;
@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useSessionStore } from '@/store/session';
7
7
 
8
- import { useSendMessage } from './useSend';
8
+ import { useSendMessage } from '../../../features/ChatInput/useSend';
9
9
 
10
10
  const useStyles = createStyles(({ css }) => {
11
11
  return {
@@ -23,10 +23,10 @@ const useStyles = createStyles(({ css }) => {
23
23
 
24
24
  const InputArea = memo(() => {
25
25
  const { t } = useTranslation('common');
26
- const isChineseInput = useRef(false);
27
-
28
26
  const { cx, styles } = useStyles();
29
27
 
28
+ const isChineseInput = useRef(false);
29
+
30
30
  const [loading, message, updateInputMessage] = useSessionStore((s) => [
31
31
  !!s.chatLoadingId,
32
32
  s.inputMessage,
@@ -3,11 +3,14 @@ import { createStyles } from 'antd-style';
3
3
  import { Maximize2, Minimize2 } from 'lucide-react';
4
4
  import { memo, useState } from 'react';
5
5
 
6
- import Footer from '@/app/chat/(desktop)/features/ChatInput/Footer';
7
6
  import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
8
7
  import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
9
8
  import { useGlobalStore } from '@/store/global';
9
+ import { useSessionStore } from '@/store/session';
10
+ import { agentSelectors } from '@/store/session/selectors';
10
11
 
12
+ import DragUpload from './DragUpload';
13
+ import Footer from './Footer';
11
14
  import InputArea from './InputArea';
12
15
 
13
16
  const useStyles = createStyles(({ css }) => {
@@ -34,37 +37,42 @@ const ChatInputDesktopLayout = memo(() => {
34
37
  s.updatePreference,
35
38
  ]);
36
39
 
40
+ const canUpload = useSessionStore(agentSelectors.modelHasVisionAbility);
37
41
  return (
38
- <DraggablePanel
39
- fullscreen={expand}
40
- headerHeight={HEADER_HEIGHT}
41
- minHeight={CHAT_TEXTAREA_HEIGHT}
42
- onSizeChange={(_, size) => {
43
- if (!size) return;
42
+ <>
43
+ {canUpload && <DragUpload />}
44
+ <DraggablePanel
45
+ fullscreen={expand}
46
+ headerHeight={HEADER_HEIGHT}
47
+ minHeight={CHAT_TEXTAREA_HEIGHT}
48
+ onSizeChange={(_, size) => {
49
+ if (!size) return;
44
50
 
45
- updatePreference({
46
- inputHeight: typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
47
- });
48
- }}
49
- placement="bottom"
50
- size={{ height: inputHeight, width: '100%' }}
51
- style={{ zIndex: 10 }}
52
- >
53
- <section className={styles.container} style={{ minHeight: CHAT_TEXTAREA_HEIGHT }}>
54
- <ActionBar
55
- rightAreaEndRender={
56
- <ActionIcon
57
- icon={expand ? Minimize2 : Maximize2}
58
- onClick={() => {
59
- setExpand(!expand);
60
- }}
61
- />
62
- }
63
- />
64
- <InputArea />
65
- <Footer />
66
- </section>
67
- </DraggablePanel>
51
+ updatePreference({
52
+ inputHeight:
53
+ typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
54
+ });
55
+ }}
56
+ placement="bottom"
57
+ size={{ height: inputHeight, width: '100%' }}
58
+ style={{ zIndex: 10 }}
59
+ >
60
+ <section className={styles.container} style={{ minHeight: CHAT_TEXTAREA_HEIGHT }}>
61
+ <ActionBar
62
+ rightAreaEndRender={
63
+ <ActionIcon
64
+ icon={expand ? Minimize2 : Maximize2}
65
+ onClick={() => {
66
+ setExpand(!expand);
67
+ }}
68
+ />
69
+ }
70
+ />
71
+ <InputArea />
72
+ <Footer />
73
+ </section>
74
+ </DraggablePanel>
75
+ </>
68
76
  );
69
77
  });
70
78