plugin-file-preview-auth 1.3.10 → 1.3.11

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.
@@ -1,256 +1,256 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
11
- import { FileTextOutlined, RobotOutlined } from '@ant-design/icons';
12
- import { Button, Space, Tooltip, message } from 'antd';
13
- import type { Application } from '@nocobase/client';
14
- import { useChatBoxActions, useAIConfigRepository, type AIEmployee } from '@nocobase/plugin-ai/client';
15
- import { useT } from './locale';
16
-
17
- export const FILE_PREVIEW_WORK_CONTEXT_TYPE = 'file-preview';
18
-
19
- const AI_EMPLOYEE_STORAGE_KEY = 'plugin-file-preview-auth.aiEmployee';
20
-
21
- function getFileDisplayName(file: any): string {
22
- if (!file) return 'file';
23
- if (file.title && file.extname) return `${file.title}${file.extname}`;
24
- return file.filename || file.name || file.title || 'file';
25
- }
26
-
27
- function getFileContextUid(file: any): string {
28
- const stableValue = file?.id ?? file?.uid ?? file?.url ?? file?.path ?? getFileDisplayName(file);
29
- return `file-preview:${String(stableValue)}`;
30
- }
31
-
32
- function normalizePreviewFile(file: any) {
33
- return {
34
- id: file?.id,
35
- uid: file?.uid,
36
- url: file?.url,
37
- preview: file?.preview,
38
- filename: file?.filename || file?.name,
39
- name: file?.name || file?.filename,
40
- title: file?.title,
41
- extname: file?.extname,
42
- mimetype: file?.mimetype,
43
- size: file?.size,
44
- path: file?.path,
45
- storageId: file?.storageId ?? file?.storage_id ?? file?.storage?.id,
46
- storage_id: file?.storage_id,
47
- storageType: file?.storageType || file?.storage?.type,
48
- storageName: file?.storageName || file?.storage?.name,
49
- storage: file?.storage,
50
- collectionName: file?.collectionName,
51
- };
52
- }
53
-
54
- export function createFilePreviewWorkContext(file: any) {
55
- return {
56
- type: FILE_PREVIEW_WORK_CONTEXT_TYPE,
57
- uid: getFileContextUid(file),
58
- title: getFileDisplayName(file),
59
- content: {
60
- source: 'plugin-file-preview-auth',
61
- file: normalizePreviewFile(file),
62
- },
63
- };
64
- }
65
-
66
- function getStoredAIEmployeeUsername() {
67
- try {
68
- return window.localStorage.getItem(AI_EMPLOYEE_STORAGE_KEY) || '';
69
- } catch {
70
- return '';
71
- }
72
- }
73
-
74
- function setStoredAIEmployeeUsername(username: string) {
75
- try {
76
- window.localStorage.setItem(AI_EMPLOYEE_STORAGE_KEY, username);
77
- } catch {
78
- // Ignore storage restrictions in embedded/sandboxed clients.
79
- }
80
- }
81
-
82
- class AIFilePreviewActionBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
83
- constructor(props: { children: React.ReactNode }) {
84
- super(props);
85
- this.state = { hasError: false };
86
- }
87
-
88
- static getDerivedStateFromError() {
89
- return { hasError: true };
90
- }
91
-
92
- render() {
93
- if (this.state.hasError) {
94
- return null;
95
- }
96
- return this.props.children;
97
- }
98
- }
99
-
100
- const AIFilePreviewActionInner: React.FC<{ file: any }> = ({ file }) => {
101
- const t = useT();
102
- const aiConfigRepository = useAIConfigRepository();
103
- const { triggerTask } = useChatBoxActions();
104
- const [employees, setEmployees] = useState<AIEmployee[]>([]);
105
- const [loading, setLoading] = useState(false);
106
- const [asking, setAsking] = useState(false);
107
-
108
- useEffect(() => {
109
- let cancelled = false;
110
- if (!aiConfigRepository?.getAIEmployees) {
111
- return;
112
- }
113
-
114
- const cached = aiConfigRepository.aiEmployees || [];
115
- if (cached.length) {
116
- setEmployees([...cached]);
117
- return;
118
- }
119
-
120
- setLoading(true);
121
- aiConfigRepository
122
- .getAIEmployees()
123
- .then((list) => {
124
- if (!cancelled) {
125
- setEmployees([...(list || [])]);
126
- setLoading(false);
127
- }
128
- })
129
- .catch(() => {
130
- if (!cancelled) {
131
- setEmployees([]);
132
- setLoading(false);
133
- }
134
- });
135
-
136
- return () => {
137
- cancelled = true;
138
- };
139
- }, [aiConfigRepository]);
140
-
141
- const orderedEmployees = useMemo(() => {
142
- const selected = getStoredAIEmployeeUsername();
143
- if (!selected) {
144
- return employees;
145
- }
146
- return [...employees].sort((a, b) => {
147
- if (a.username === selected) return -1;
148
- if (b.username === selected) return 1;
149
- return 0;
150
- });
151
- }, [employees]);
152
-
153
- const openAIChat = useCallback(
154
- async (employee: AIEmployee) => {
155
- if (!employee || !file) {
156
- return;
157
- }
158
-
159
- setAsking(true);
160
- try {
161
- setStoredAIEmployeeUsername(employee.username);
162
- await triggerTask({
163
- aiEmployee: employee,
164
- tasks: [
165
- {
166
- title: getFileDisplayName(file),
167
- message: {
168
- user: t('Please help me analyze the file currently open in preview.'),
169
- workContext: [createFilePreviewWorkContext(file)],
170
- },
171
- autoSend: false,
172
- },
173
- ],
174
- });
175
- } catch {
176
- message.error(t('Failed to open AI chat'));
177
- } finally {
178
- setAsking(false);
179
- }
180
- },
181
- [file, t, triggerTask],
182
- );
183
-
184
- if (!loading && !orderedEmployees.length) {
185
- return null;
186
- }
187
-
188
- // Single click attaches the file content to the chat box using the preferred
189
- // employee (last used, otherwise the first available) without listing employees.
190
- return (
191
- <Tooltip title={t('Ask AI')}>
192
- <Button
193
- type="text"
194
- size="small"
195
- icon={<RobotOutlined />}
196
- loading={asking || loading}
197
- disabled={!orderedEmployees.length}
198
- onClick={(event) => {
199
- event.stopPropagation();
200
- openAIChat(orderedEmployees[0]);
201
- }}
202
- >
203
- {t('Ask AI')}
204
- </Button>
205
- </Tooltip>
206
- );
207
- };
208
-
209
- export const AIFilePreviewAction: React.FC<{ file: any }> = ({ file }) => {
210
- return (
211
- <AIFilePreviewActionBoundary>
212
- <AIFilePreviewActionInner file={file} />
213
- </AIFilePreviewActionBoundary>
214
- );
215
- };
216
-
217
- export function registerFilePreviewAIWorkContext(app: Application) {
218
- let aiPlugin: any;
219
- try {
220
- aiPlugin = app.pm.get('ai') as any;
221
- } catch {
222
- return;
223
- }
224
- const aiManager = aiPlugin?.aiManager;
225
- if (!aiManager?.registerWorkContext) {
226
- return;
227
- }
228
-
229
- const options = {
230
- name: FILE_PREVIEW_WORK_CONTEXT_TYPE,
231
- tag: {
232
- Component: ({ item }: { item: any }) => (
233
- <Space>
234
- <FileTextOutlined />
235
- <span>{item?.title || ''}</span>
236
- </Space>
237
- ),
238
- },
239
- chatbox: {
240
- Component: ({ item }: { item: any }) => (
241
- <Space>
242
- <FileTextOutlined />
243
- <span>{item?.title || ''}</span>
244
- </Space>
245
- ),
246
- },
247
- };
248
-
249
- try {
250
- if (!aiManager.getWorkContext?.(FILE_PREVIEW_WORK_CONTEXT_TYPE)) {
251
- aiManager.registerWorkContext(FILE_PREVIEW_WORK_CONTEXT_TYPE, options);
252
- }
253
- } catch {
254
- // Duplicate registration can happen during hot reload. It is harmless.
255
- }
256
- }
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
11
+ import { FileTextOutlined, RobotOutlined } from '@ant-design/icons';
12
+ import { Button, Space, Tooltip, message } from 'antd';
13
+ import type { Application } from '@nocobase/client';
14
+ import { useChatBoxActions, useAIConfigRepository, type AIEmployee } from '@nocobase/plugin-ai/client';
15
+ import { useT } from './locale';
16
+
17
+ export const FILE_PREVIEW_WORK_CONTEXT_TYPE = 'file-preview';
18
+
19
+ const AI_EMPLOYEE_STORAGE_KEY = 'plugin-file-preview-auth.aiEmployee';
20
+
21
+ function getFileDisplayName(file: any): string {
22
+ if (!file) return 'file';
23
+ if (file.title && file.extname) return `${file.title}${file.extname}`;
24
+ return file.filename || file.name || file.title || 'file';
25
+ }
26
+
27
+ function getFileContextUid(file: any): string {
28
+ const stableValue = file?.id ?? file?.uid ?? file?.url ?? file?.path ?? getFileDisplayName(file);
29
+ return `file-preview:${String(stableValue)}`;
30
+ }
31
+
32
+ function normalizePreviewFile(file: any) {
33
+ return {
34
+ id: file?.id,
35
+ uid: file?.uid,
36
+ url: file?.url,
37
+ preview: file?.preview,
38
+ filename: file?.filename || file?.name,
39
+ name: file?.name || file?.filename,
40
+ title: file?.title,
41
+ extname: file?.extname,
42
+ mimetype: file?.mimetype,
43
+ size: file?.size,
44
+ path: file?.path,
45
+ storageId: file?.storageId ?? file?.storage_id ?? file?.storage?.id,
46
+ storage_id: file?.storage_id,
47
+ storageType: file?.storageType || file?.storage?.type,
48
+ storageName: file?.storageName || file?.storage?.name,
49
+ storage: file?.storage,
50
+ collectionName: file?.collectionName,
51
+ };
52
+ }
53
+
54
+ export function createFilePreviewWorkContext(file: any) {
55
+ return {
56
+ type: FILE_PREVIEW_WORK_CONTEXT_TYPE,
57
+ uid: getFileContextUid(file),
58
+ title: getFileDisplayName(file),
59
+ content: {
60
+ source: 'plugin-file-preview-auth',
61
+ file: normalizePreviewFile(file),
62
+ },
63
+ };
64
+ }
65
+
66
+ function getStoredAIEmployeeUsername() {
67
+ try {
68
+ return window.localStorage.getItem(AI_EMPLOYEE_STORAGE_KEY) || '';
69
+ } catch {
70
+ return '';
71
+ }
72
+ }
73
+
74
+ function setStoredAIEmployeeUsername(username: string) {
75
+ try {
76
+ window.localStorage.setItem(AI_EMPLOYEE_STORAGE_KEY, username);
77
+ } catch {
78
+ // Ignore storage restrictions in embedded/sandboxed clients.
79
+ }
80
+ }
81
+
82
+ class AIFilePreviewActionBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
83
+ constructor(props: { children: React.ReactNode }) {
84
+ super(props);
85
+ this.state = { hasError: false };
86
+ }
87
+
88
+ static getDerivedStateFromError() {
89
+ return { hasError: true };
90
+ }
91
+
92
+ render() {
93
+ if (this.state.hasError) {
94
+ return null;
95
+ }
96
+ return this.props.children;
97
+ }
98
+ }
99
+
100
+ const AIFilePreviewActionInner: React.FC<{ file: any }> = ({ file }) => {
101
+ const t = useT();
102
+ const aiConfigRepository = useAIConfigRepository();
103
+ const { triggerTask } = useChatBoxActions();
104
+ const [employees, setEmployees] = useState<AIEmployee[]>([]);
105
+ const [loading, setLoading] = useState(false);
106
+ const [asking, setAsking] = useState(false);
107
+
108
+ useEffect(() => {
109
+ let cancelled = false;
110
+ if (!aiConfigRepository?.getAIEmployees) {
111
+ return;
112
+ }
113
+
114
+ const cached = aiConfigRepository.aiEmployees || [];
115
+ if (cached.length) {
116
+ setEmployees([...cached]);
117
+ return;
118
+ }
119
+
120
+ setLoading(true);
121
+ aiConfigRepository
122
+ .getAIEmployees()
123
+ .then((list) => {
124
+ if (!cancelled) {
125
+ setEmployees([...(list || [])]);
126
+ setLoading(false);
127
+ }
128
+ })
129
+ .catch(() => {
130
+ if (!cancelled) {
131
+ setEmployees([]);
132
+ setLoading(false);
133
+ }
134
+ });
135
+
136
+ return () => {
137
+ cancelled = true;
138
+ };
139
+ }, [aiConfigRepository]);
140
+
141
+ const orderedEmployees = useMemo(() => {
142
+ const selected = getStoredAIEmployeeUsername();
143
+ if (!selected) {
144
+ return employees;
145
+ }
146
+ return [...employees].sort((a, b) => {
147
+ if (a.username === selected) return -1;
148
+ if (b.username === selected) return 1;
149
+ return 0;
150
+ });
151
+ }, [employees]);
152
+
153
+ const openAIChat = useCallback(
154
+ async (employee: AIEmployee) => {
155
+ if (!employee || !file) {
156
+ return;
157
+ }
158
+
159
+ setAsking(true);
160
+ try {
161
+ setStoredAIEmployeeUsername(employee.username);
162
+ await triggerTask({
163
+ aiEmployee: employee,
164
+ tasks: [
165
+ {
166
+ title: getFileDisplayName(file),
167
+ message: {
168
+ user: t('Please help me analyze the file currently open in preview.'),
169
+ workContext: [createFilePreviewWorkContext(file)],
170
+ },
171
+ autoSend: false,
172
+ },
173
+ ],
174
+ });
175
+ } catch {
176
+ message.error(t('Failed to open AI chat'));
177
+ } finally {
178
+ setAsking(false);
179
+ }
180
+ },
181
+ [file, t, triggerTask],
182
+ );
183
+
184
+ if (!loading && !orderedEmployees.length) {
185
+ return null;
186
+ }
187
+
188
+ // Single click attaches the file content to the chat box using the preferred
189
+ // employee (last used, otherwise the first available) without listing employees.
190
+ return (
191
+ <Tooltip title={t('Ask AI')}>
192
+ <Button
193
+ type="text"
194
+ size="small"
195
+ icon={<RobotOutlined />}
196
+ loading={asking || loading}
197
+ disabled={!orderedEmployees.length}
198
+ onClick={(event) => {
199
+ event.stopPropagation();
200
+ openAIChat(orderedEmployees[0]);
201
+ }}
202
+ >
203
+ {t('Ask AI')}
204
+ </Button>
205
+ </Tooltip>
206
+ );
207
+ };
208
+
209
+ export const AIFilePreviewAction: React.FC<{ file: any }> = ({ file }) => {
210
+ return (
211
+ <AIFilePreviewActionBoundary>
212
+ <AIFilePreviewActionInner file={file} />
213
+ </AIFilePreviewActionBoundary>
214
+ );
215
+ };
216
+
217
+ export function registerFilePreviewAIWorkContext(app: Application) {
218
+ let aiPlugin: any;
219
+ try {
220
+ aiPlugin = app.pm.get('ai') as any;
221
+ } catch {
222
+ return;
223
+ }
224
+ const aiManager = aiPlugin?.aiManager;
225
+ if (!aiManager?.registerWorkContext) {
226
+ return;
227
+ }
228
+
229
+ const options = {
230
+ name: FILE_PREVIEW_WORK_CONTEXT_TYPE,
231
+ tag: {
232
+ Component: ({ item }: { item: any }) => (
233
+ <Space>
234
+ <FileTextOutlined />
235
+ <span>{item?.title || ''}</span>
236
+ </Space>
237
+ ),
238
+ },
239
+ chatbox: {
240
+ Component: ({ item }: { item: any }) => (
241
+ <Space>
242
+ <FileTextOutlined />
243
+ <span>{item?.title || ''}</span>
244
+ </Space>
245
+ ),
246
+ },
247
+ };
248
+
249
+ try {
250
+ if (!aiManager.getWorkContext?.(FILE_PREVIEW_WORK_CONTEXT_TYPE)) {
251
+ aiManager.registerWorkContext(FILE_PREVIEW_WORK_CONTEXT_TYPE, options);
252
+ }
253
+ } catch {
254
+ // Duplicate registration can happen during hot reload. It is harmless.
255
+ }
256
+ }