cozy-viewer 26.1.2 → 26.2.1
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 +14 -0
- package/dist/Panel/AI/AIAssistantPanel.js +124 -31
- package/dist/helpers.d.ts +0 -1
- package/dist/helpers.js +4 -29
- package/package.json +4 -4
- package/src/Panel/AI/AIAssistantPanel.jsx +77 -24
- package/src/helpers.js +2 -25
- package/src/helpers.spec.js +16 -26
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [26.2.1](https://github.com/cozy/cozy-libs/compare/cozy-viewer@26.2.0...cozy-viewer@26.2.1) (2025-12-15)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package cozy-viewer
|
|
9
|
+
|
|
10
|
+
# [26.2.0](https://github.com/cozy/cozy-libs/compare/cozy-viewer@26.1.2...cozy-viewer@26.2.0) (2025-12-15)
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- Remove json parser when getting drive.summary flag :bug: ([a565f84](https://github.com/cozy/cozy-libs/commit/a565f848918c9dbf2819e370b3b84a77188ba28f))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- Save result from summarization by AI :sparkles: ([c89a034](https://github.com/cozy/cozy-libs/commit/c89a034ea912e0ff444920a7611488fcc8734c23))
|
|
19
|
+
|
|
6
20
|
## [26.1.2](https://github.com/cozy/cozy-libs/compare/cozy-viewer@26.1.1...cozy-viewer@26.1.2) (2025-12-12)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package cozy-viewer
|
|
@@ -9,6 +9,8 @@ exports.default = void 0;
|
|
|
9
9
|
|
|
10
10
|
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
|
11
11
|
|
|
12
|
+
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
13
|
+
|
|
12
14
|
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
|
13
15
|
|
|
14
16
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
@@ -29,6 +31,8 @@ var _ai = require("cozy-client/dist/models/ai");
|
|
|
29
31
|
|
|
30
32
|
var _file = require("cozy-client/dist/models/file");
|
|
31
33
|
|
|
34
|
+
var _cozyFlags = _interopRequireDefault(require("cozy-flags"));
|
|
35
|
+
|
|
32
36
|
var _cozyLogger = _interopRequireDefault(require("cozy-logger"));
|
|
33
37
|
|
|
34
38
|
var _Buttons = _interopRequireDefault(require("cozy-ui/transpiled/react/Buttons"));
|
|
@@ -63,6 +67,10 @@ function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "functio
|
|
|
63
67
|
|
|
64
68
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
65
69
|
|
|
70
|
+
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
71
|
+
|
|
72
|
+
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
73
|
+
|
|
66
74
|
var styles = {
|
|
67
75
|
"loaderContainer": "styles__loaderContainer___RuLAO",
|
|
68
76
|
"loaderBar": "styles__loaderBar___2_kw1",
|
|
@@ -101,6 +109,9 @@ var AIAssistantPanel = function AIAssistantPanel(_ref) {
|
|
|
101
109
|
|
|
102
110
|
var location = (0, _reactRouterDom.useLocation)();
|
|
103
111
|
var navigate = (0, _reactRouterDom.useNavigate)();
|
|
112
|
+
var fetchedFileIdRef = (0, _react.useRef)(null);
|
|
113
|
+
var inFlightFileIdRef = (0, _react.useRef)(null);
|
|
114
|
+
var activeFileIdRef = (0, _react.useRef)((file === null || file === void 0 ? void 0 : file._id) || null);
|
|
104
115
|
|
|
105
116
|
var handleClose = function handleClose() {
|
|
106
117
|
var _location$state;
|
|
@@ -135,7 +146,7 @@ var AIAssistantPanel = function AIAssistantPanel(_ref) {
|
|
|
135
146
|
|
|
136
147
|
case 7:
|
|
137
148
|
textContent = _context.sent;
|
|
138
|
-
summaryConfig = (0,
|
|
149
|
+
summaryConfig = (0, _cozyFlags.default)('drive.summary');
|
|
139
150
|
|
|
140
151
|
if (!(summaryConfig !== null && summaryConfig !== void 0 && summaryConfig.maxTokens && (0, _helpers.roughTokensEstimation)(textContent) > summaryConfig.maxTokens)) {
|
|
141
152
|
_context.next = 13;
|
|
@@ -185,63 +196,145 @@ var AIAssistantPanel = function AIAssistantPanel(_ref) {
|
|
|
185
196
|
};
|
|
186
197
|
}();
|
|
187
198
|
|
|
188
|
-
var
|
|
189
|
-
var
|
|
190
|
-
|
|
199
|
+
var persistedSummary = /*#__PURE__*/function () {
|
|
200
|
+
var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(fileMetadata, targetFileId, summaryContent) {
|
|
201
|
+
return _regenerator.default.wrap(function _callee2$(_context2) {
|
|
202
|
+
while (1) {
|
|
203
|
+
switch (_context2.prev = _context2.next) {
|
|
204
|
+
case 0:
|
|
205
|
+
_context2.prev = 0;
|
|
206
|
+
_context2.next = 3;
|
|
207
|
+
return client.collection('io.cozy.files').updateMetadataAttribute(targetFileId, _objectSpread(_objectSpread({}, fileMetadata), {}, {
|
|
208
|
+
description: summaryContent
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
case 3:
|
|
212
|
+
fetchedFileIdRef.current = targetFileId;
|
|
213
|
+
_context2.next = 9;
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 6:
|
|
217
|
+
_context2.prev = 6;
|
|
218
|
+
_context2.t0 = _context2["catch"](0);
|
|
219
|
+
|
|
220
|
+
_cozyLogger.default.error('Error when persisting summary to file metadata:', _context2.t0);
|
|
221
|
+
|
|
222
|
+
case 9:
|
|
223
|
+
case "end":
|
|
224
|
+
return _context2.stop();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}, _callee2, null, [[0, 6]]);
|
|
228
|
+
}));
|
|
229
|
+
|
|
230
|
+
return function persistedSummary(_x2, _x3, _x4) {
|
|
231
|
+
return _ref4.apply(this, arguments);
|
|
232
|
+
};
|
|
233
|
+
}();
|
|
234
|
+
|
|
235
|
+
(0, _react.useEffect)(function () {
|
|
236
|
+
activeFileIdRef.current = (file === null || file === void 0 ? void 0 : file._id) || null;
|
|
237
|
+
}, [file]);
|
|
238
|
+
var fetchSummary = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3() {
|
|
239
|
+
var force,
|
|
240
|
+
targetFileId,
|
|
241
|
+
_response$choices,
|
|
242
|
+
_response$choices$,
|
|
243
|
+
_response$choices$$me,
|
|
244
|
+
response,
|
|
245
|
+
summaryContent,
|
|
246
|
+
fileMetadata,
|
|
247
|
+
errorMessage,
|
|
248
|
+
_args3 = arguments;
|
|
249
|
+
|
|
250
|
+
return _regenerator.default.wrap(function _callee3$(_context3) {
|
|
191
251
|
while (1) {
|
|
192
|
-
switch (
|
|
252
|
+
switch (_context3.prev = _context3.next) {
|
|
193
253
|
case 0:
|
|
194
|
-
|
|
195
|
-
|
|
254
|
+
force = _args3.length > 0 && _args3[0] !== undefined ? _args3[0] : false;
|
|
255
|
+
targetFileId = file === null || file === void 0 ? void 0 : file._id;
|
|
256
|
+
|
|
257
|
+
if (targetFileId) {
|
|
258
|
+
_context3.next = 4;
|
|
196
259
|
break;
|
|
197
260
|
}
|
|
198
261
|
|
|
199
|
-
return
|
|
262
|
+
return _context3.abrupt("return");
|
|
263
|
+
|
|
264
|
+
case 4:
|
|
265
|
+
if (!(!force && (fetchedFileIdRef.current === targetFileId || inFlightFileIdRef.current === targetFileId))) {
|
|
266
|
+
_context3.next = 6;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
200
269
|
|
|
201
|
-
|
|
270
|
+
return _context3.abrupt("return");
|
|
271
|
+
|
|
272
|
+
case 6:
|
|
273
|
+
inFlightFileIdRef.current = targetFileId;
|
|
202
274
|
setIsLoading(true);
|
|
203
275
|
setError(null);
|
|
204
|
-
|
|
205
|
-
|
|
276
|
+
_context3.prev = 9;
|
|
277
|
+
_context3.next = 12;
|
|
206
278
|
return summarizeFile({
|
|
207
279
|
client: client,
|
|
208
280
|
file: file,
|
|
209
281
|
stream: false
|
|
210
282
|
});
|
|
211
283
|
|
|
212
|
-
case
|
|
213
|
-
response =
|
|
284
|
+
case 12:
|
|
285
|
+
response = _context3.sent;
|
|
286
|
+
summaryContent = (response === null || response === void 0 ? void 0 : response.content) || (response === null || response === void 0 ? void 0 : (_response$choices = response.choices) === null || _response$choices === void 0 ? void 0 : (_response$choices$ = _response$choices[0]) === null || _response$choices$ === void 0 ? void 0 : (_response$choices$$me = _response$choices$.message) === null || _response$choices$$me === void 0 ? void 0 : _response$choices$$me.content); // Ignore results if the user switched to another file meanwhile
|
|
214
287
|
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
setSummary(response.choices[0].message.content);
|
|
288
|
+
if (!(activeFileIdRef.current !== targetFileId)) {
|
|
289
|
+
_context3.next = 16;
|
|
290
|
+
break;
|
|
219
291
|
}
|
|
220
292
|
|
|
221
|
-
|
|
293
|
+
return _context3.abrupt("return");
|
|
294
|
+
|
|
295
|
+
case 16:
|
|
296
|
+
setSummary(summaryContent);
|
|
297
|
+
fileMetadata = file.metadata || {};
|
|
298
|
+
_context3.next = 20;
|
|
299
|
+
return persistedSummary(fileMetadata, targetFileId, summaryContent);
|
|
300
|
+
|
|
301
|
+
case 20:
|
|
302
|
+
_context3.next = 25;
|
|
222
303
|
break;
|
|
223
304
|
|
|
224
|
-
case
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
errorMessage = _context2.t0.code === 'DOCUMENT_TOO_LARGE' ? t('Viewer.ai.error.documentTooLarge') : t('Viewer.ai.error.summary');
|
|
228
|
-
setError(errorMessage);
|
|
305
|
+
case 22:
|
|
306
|
+
_context3.prev = 22;
|
|
307
|
+
_context3.t0 = _context3["catch"](9);
|
|
229
308
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
309
|
+
if (activeFileIdRef.current === targetFileId) {
|
|
310
|
+
errorMessage = _context3.t0.code === 'DOCUMENT_TOO_LARGE' ? t('Viewer.ai.error.documentTooLarge') : t('Viewer.ai.error.summary');
|
|
311
|
+
setError(errorMessage);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case 25:
|
|
315
|
+
_context3.prev = 25;
|
|
316
|
+
|
|
317
|
+
if (inFlightFileIdRef.current === targetFileId) {
|
|
318
|
+
inFlightFileIdRef.current = null;
|
|
319
|
+
}
|
|
234
320
|
|
|
235
|
-
|
|
321
|
+
if (activeFileIdRef.current === targetFileId) {
|
|
322
|
+
setIsLoading(false);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return _context3.finish(25);
|
|
326
|
+
|
|
327
|
+
case 29:
|
|
236
328
|
case "end":
|
|
237
|
-
return
|
|
329
|
+
return _context3.stop();
|
|
238
330
|
}
|
|
239
331
|
}
|
|
240
|
-
},
|
|
332
|
+
}, _callee3, null, [[9, 22, 25, 29]]);
|
|
241
333
|
})), [client, file, t]);
|
|
242
334
|
|
|
243
335
|
var handleRefresh = function handleRefresh() {
|
|
244
|
-
|
|
336
|
+
fetchedFileIdRef.current = null;
|
|
337
|
+
fetchSummary(true);
|
|
245
338
|
};
|
|
246
339
|
|
|
247
340
|
var handleCopy = function handleCopy() {
|
package/dist/helpers.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export function makeWebLink({ client, slug, path }: {
|
|
|
13
13
|
}): string;
|
|
14
14
|
export function removeFilenameFromPath(path: string): string;
|
|
15
15
|
export function roughTokensEstimation(text: string): number;
|
|
16
|
-
export function getSummaryConfig(): object | null;
|
|
17
16
|
export function isFileSummaryCompatible(file: object, options?: {
|
|
18
17
|
pageCount: number;
|
|
19
18
|
}): boolean;
|
package/dist/helpers.js
CHANGED
|
@@ -5,7 +5,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", {
|
|
6
6
|
value: true
|
|
7
7
|
});
|
|
8
|
-
exports.roughTokensEstimation = exports.removeFilenameFromPath = exports.normalizeAndSpreadAttributes = exports.makeWebLink = exports.isFileSummaryCompatible = exports.isFileEncrypted = exports.isEditableAttribute = exports.
|
|
8
|
+
exports.roughTokensEstimation = exports.removeFilenameFromPath = exports.normalizeAndSpreadAttributes = exports.makeWebLink = exports.isFileSummaryCompatible = exports.isFileEncrypted = exports.isEditableAttribute = exports.formatDate = void 0;
|
|
9
9
|
|
|
10
10
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
11
11
|
|
|
@@ -15,8 +15,6 @@ var _file = require("cozy-client/dist/models/file");
|
|
|
15
15
|
|
|
16
16
|
var _cozyFlags = _interopRequireDefault(require("cozy-flags"));
|
|
17
17
|
|
|
18
|
-
var _cozyLogger = _interopRequireDefault(require("cozy-logger"));
|
|
19
|
-
|
|
20
18
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
21
19
|
|
|
22
20
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
@@ -117,29 +115,6 @@ exports.removeFilenameFromPath = removeFilenameFromPath;
|
|
|
117
115
|
var roughTokensEstimation = function roughTokensEstimation(text) {
|
|
118
116
|
return Math.ceil(text.length / 4);
|
|
119
117
|
};
|
|
120
|
-
/**
|
|
121
|
-
* Get and parse the drive.summary flag configuration
|
|
122
|
-
* @returns {object|null} Parsed summary config or null if not available/invalid
|
|
123
|
-
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
exports.roughTokensEstimation = roughTokensEstimation;
|
|
127
|
-
|
|
128
|
-
var getSummaryConfig = function getSummaryConfig() {
|
|
129
|
-
var summaryConfigRawValue = (0, _cozyFlags.default)('drive.summary');
|
|
130
|
-
|
|
131
|
-
if (!summaryConfigRawValue) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
return JSON.parse(summaryConfigRawValue);
|
|
137
|
-
} catch (e) {
|
|
138
|
-
_cozyLogger.default.error('Failed to parse drive.summary flag:', e);
|
|
139
|
-
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
118
|
/**
|
|
144
119
|
* Check if a file is compatible with AI summary feature
|
|
145
120
|
* Compatible file types are defined in the drive.summary flag
|
|
@@ -151,7 +126,7 @@ var getSummaryConfig = function getSummaryConfig() {
|
|
|
151
126
|
*/
|
|
152
127
|
|
|
153
128
|
|
|
154
|
-
exports.
|
|
129
|
+
exports.roughTokensEstimation = roughTokensEstimation;
|
|
155
130
|
|
|
156
131
|
var isFileSummaryCompatible = function isFileSummaryCompatible(file) {
|
|
157
132
|
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
|
|
@@ -162,13 +137,13 @@ var isFileSummaryCompatible = function isFileSummaryCompatible(file) {
|
|
|
162
137
|
return false;
|
|
163
138
|
}
|
|
164
139
|
|
|
165
|
-
var summaryConfig =
|
|
140
|
+
var summaryConfig = (0, _cozyFlags.default)('drive.summary');
|
|
166
141
|
|
|
167
142
|
if (!summaryConfig) {
|
|
168
143
|
return false;
|
|
169
144
|
}
|
|
170
145
|
|
|
171
|
-
if (!
|
|
146
|
+
if (!Array.isArray(summaryConfig.types) || summaryConfig.types.length === 0) {
|
|
172
147
|
return false;
|
|
173
148
|
}
|
|
174
149
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cozy-viewer",
|
|
3
|
-
"version": "26.1
|
|
3
|
+
"version": "26.2.1",
|
|
4
4
|
"description": "Cozy-Viewer provides a component to show files in a viewer.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"babel-preset-cozy-app": "^2.8.2",
|
|
32
32
|
"cozy-client": "^60.20.0",
|
|
33
33
|
"cozy-device-helper": "2.0.0",
|
|
34
|
-
"cozy-harvest-lib": "^36.0.
|
|
34
|
+
"cozy-harvest-lib": "^36.0.10",
|
|
35
35
|
"cozy-intent": "^2.30.1",
|
|
36
36
|
"cozy-logger": "^1.17.0",
|
|
37
|
-
"cozy-sharing": "^28.1.
|
|
37
|
+
"cozy-sharing": "^28.1.2",
|
|
38
38
|
"cozy-ui": "^135.0.0",
|
|
39
39
|
"cozy-ui-plus": "^4.1.0",
|
|
40
40
|
"identity-obj-proxy": "3.0.0",
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"react-router-dom": ">=6.14.2",
|
|
71
71
|
"twake-i18n": ">=0.3.0"
|
|
72
72
|
},
|
|
73
|
-
"gitHead": "
|
|
73
|
+
"gitHead": "ea1e051ec4fb1033cdddc76cab56bc7ec174d6be"
|
|
74
74
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import cx from 'classnames'
|
|
2
2
|
import PropTypes from 'prop-types'
|
|
3
|
-
import React, { useCallback, useState, useEffect } from 'react'
|
|
3
|
+
import React, { useCallback, useState, useEffect, useRef } from 'react'
|
|
4
4
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
5
5
|
import { useI18n } from 'twake-i18n'
|
|
6
6
|
|
|
7
7
|
import { useClient } from 'cozy-client'
|
|
8
8
|
import { extractText, chatCompletion } from 'cozy-client/dist/models/ai'
|
|
9
9
|
import { fetchBlobFileById } from 'cozy-client/dist/models/file'
|
|
10
|
+
import flag from 'cozy-flags'
|
|
10
11
|
import logger from 'cozy-logger'
|
|
11
12
|
import Button from 'cozy-ui/transpiled/react/Buttons'
|
|
12
13
|
import Icon from 'cozy-ui/transpiled/react/Icon'
|
|
@@ -22,7 +23,7 @@ import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert'
|
|
|
22
23
|
|
|
23
24
|
import { SUMMARY_SYSTEM_PROMPT, getSummaryUserPrompt } from './prompts'
|
|
24
25
|
import styles from './styles.styl'
|
|
25
|
-
import {
|
|
26
|
+
import { roughTokensEstimation } from '../../helpers'
|
|
26
27
|
import { useViewer } from '../../providers/ViewerProvider'
|
|
27
28
|
|
|
28
29
|
const AIAssistantPanel = ({ className }) => {
|
|
@@ -37,6 +38,9 @@ const AIAssistantPanel = ({ className }) => {
|
|
|
37
38
|
|
|
38
39
|
const location = useLocation()
|
|
39
40
|
const navigate = useNavigate()
|
|
41
|
+
const fetchedFileIdRef = useRef(null)
|
|
42
|
+
const inFlightFileIdRef = useRef(null)
|
|
43
|
+
const activeFileIdRef = useRef(file?._id || null)
|
|
40
44
|
|
|
41
45
|
const handleClose = () => {
|
|
42
46
|
setIsOpenAiAssistant(false)
|
|
@@ -54,7 +58,7 @@ const AIAssistantPanel = ({ className }) => {
|
|
|
54
58
|
mime: file.mime
|
|
55
59
|
})
|
|
56
60
|
|
|
57
|
-
const summaryConfig =
|
|
61
|
+
const summaryConfig = flag('drive.summary')
|
|
58
62
|
if (
|
|
59
63
|
summaryConfig?.maxTokens &&
|
|
60
64
|
roughTokensEstimation(textContent) > summaryConfig.maxTokens
|
|
@@ -84,31 +88,80 @@ const AIAssistantPanel = ({ className }) => {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const persistedSummary = async (
|
|
92
|
+
fileMetadata,
|
|
93
|
+
targetFileId,
|
|
94
|
+
summaryContent
|
|
95
|
+
) => {
|
|
92
96
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
? t('Viewer.ai.error.documentTooLarge')
|
|
103
|
-
: t('Viewer.ai.error.summary')
|
|
104
|
-
setError(errorMessage)
|
|
105
|
-
} finally {
|
|
106
|
-
setIsLoading(false)
|
|
97
|
+
await client
|
|
98
|
+
.collection('io.cozy.files')
|
|
99
|
+
.updateMetadataAttribute(targetFileId, {
|
|
100
|
+
...fileMetadata,
|
|
101
|
+
description: summaryContent
|
|
102
|
+
})
|
|
103
|
+
fetchedFileIdRef.current = targetFileId
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error('Error when persisting summary to file metadata:', error)
|
|
107
106
|
}
|
|
108
|
-
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
activeFileIdRef.current = file?._id || null
|
|
111
|
+
}, [file])
|
|
112
|
+
|
|
113
|
+
const fetchSummary = useCallback(
|
|
114
|
+
async (force = false) => {
|
|
115
|
+
const targetFileId = file?._id
|
|
116
|
+
if (!targetFileId) return
|
|
117
|
+
|
|
118
|
+
// Prevent duplicate fetches for the same file
|
|
119
|
+
if (
|
|
120
|
+
!force &&
|
|
121
|
+
(fetchedFileIdRef.current === targetFileId ||
|
|
122
|
+
inFlightFileIdRef.current === targetFileId)
|
|
123
|
+
) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
inFlightFileIdRef.current = targetFileId
|
|
128
|
+
setIsLoading(true)
|
|
129
|
+
setError(null)
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const response = await summarizeFile({ client, file, stream: false })
|
|
133
|
+
const summaryContent =
|
|
134
|
+
response?.content || response?.choices?.[0]?.message?.content
|
|
135
|
+
// Ignore results if the user switched to another file meanwhile
|
|
136
|
+
if (activeFileIdRef.current !== targetFileId) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
setSummary(summaryContent)
|
|
140
|
+
const fileMetadata = file.metadata || {}
|
|
141
|
+
await persistedSummary(fileMetadata, targetFileId, summaryContent)
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (activeFileIdRef.current === targetFileId) {
|
|
144
|
+
const errorMessage =
|
|
145
|
+
err.code === 'DOCUMENT_TOO_LARGE'
|
|
146
|
+
? t('Viewer.ai.error.documentTooLarge')
|
|
147
|
+
: t('Viewer.ai.error.summary')
|
|
148
|
+
setError(errorMessage)
|
|
149
|
+
}
|
|
150
|
+
} finally {
|
|
151
|
+
if (inFlightFileIdRef.current === targetFileId) {
|
|
152
|
+
inFlightFileIdRef.current = null
|
|
153
|
+
}
|
|
154
|
+
if (activeFileIdRef.current === targetFileId) {
|
|
155
|
+
setIsLoading(false)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[client, file, t]
|
|
160
|
+
)
|
|
109
161
|
|
|
110
162
|
const handleRefresh = () => {
|
|
111
|
-
|
|
163
|
+
fetchedFileIdRef.current = null
|
|
164
|
+
fetchSummary(true)
|
|
112
165
|
}
|
|
113
166
|
|
|
114
167
|
const handleCopy = () => {
|
package/src/helpers.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
normalize
|
|
6
6
|
} from 'cozy-client/dist/models/file'
|
|
7
7
|
import flag from 'cozy-flags'
|
|
8
|
-
import logger from 'cozy-logger'
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* @typedef {object} Reference
|
|
@@ -85,24 +84,6 @@ export const roughTokensEstimation = text => {
|
|
|
85
84
|
return Math.ceil(text.length / 4)
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
/**
|
|
89
|
-
* Get and parse the drive.summary flag configuration
|
|
90
|
-
* @returns {object|null} Parsed summary config or null if not available/invalid
|
|
91
|
-
*/
|
|
92
|
-
export const getSummaryConfig = () => {
|
|
93
|
-
const summaryConfigRawValue = flag('drive.summary')
|
|
94
|
-
if (!summaryConfigRawValue) {
|
|
95
|
-
return null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
return JSON.parse(summaryConfigRawValue)
|
|
100
|
-
} catch (e) {
|
|
101
|
-
logger.error('Failed to parse drive.summary flag:', e)
|
|
102
|
-
return null
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
87
|
/**
|
|
107
88
|
* Check if a file is compatible with AI summary feature
|
|
108
89
|
* Compatible file types are defined in the drive.summary flag
|
|
@@ -120,16 +101,12 @@ export const isFileSummaryCompatible = (
|
|
|
120
101
|
return false
|
|
121
102
|
}
|
|
122
103
|
|
|
123
|
-
const summaryConfig =
|
|
104
|
+
const summaryConfig = flag('drive.summary')
|
|
124
105
|
if (!summaryConfig) {
|
|
125
106
|
return false
|
|
126
107
|
}
|
|
127
108
|
|
|
128
|
-
if (
|
|
129
|
-
!summaryConfig ||
|
|
130
|
-
!Array.isArray(summaryConfig.types) ||
|
|
131
|
-
summaryConfig.types.length === 0
|
|
132
|
-
) {
|
|
109
|
+
if (!Array.isArray(summaryConfig.types) || summaryConfig.types.length === 0) {
|
|
133
110
|
return false
|
|
134
111
|
}
|
|
135
112
|
|
package/src/helpers.spec.js
CHANGED
|
@@ -11,11 +11,11 @@ jest.mock('cozy-flags')
|
|
|
11
11
|
// Default mock for drive.summary flag
|
|
12
12
|
flag.mockImplementation(flagName => {
|
|
13
13
|
if (flagName === 'drive.summary') {
|
|
14
|
-
return
|
|
14
|
+
return {
|
|
15
15
|
types: ['application/pdf', 'text/*'],
|
|
16
16
|
pageLimit: 50,
|
|
17
17
|
maxTokens: 100000
|
|
18
|
-
}
|
|
18
|
+
}
|
|
19
19
|
}
|
|
20
20
|
return null
|
|
21
21
|
})
|
|
@@ -110,64 +110,58 @@ describe('helpers', () => {
|
|
|
110
110
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it('should return false if flag
|
|
114
|
-
flag.mockReturnValue('invalid
|
|
113
|
+
it('should return false if flag config is invalid', () => {
|
|
114
|
+
flag.mockReturnValue('invalid config')
|
|
115
115
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
116
116
|
})
|
|
117
117
|
|
|
118
118
|
it('should return false if flag config is missing types array', () => {
|
|
119
|
-
flag.mockReturnValue(
|
|
119
|
+
flag.mockReturnValue({})
|
|
120
120
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
121
121
|
})
|
|
122
122
|
|
|
123
123
|
it('should return false if types array is empty', () => {
|
|
124
|
-
flag.mockReturnValue(
|
|
124
|
+
flag.mockReturnValue({ types: [] })
|
|
125
125
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
it('should return false if types is not an array', () => {
|
|
129
|
-
flag.mockReturnValue(
|
|
129
|
+
flag.mockReturnValue({ types: 'application/pdf' })
|
|
130
130
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
131
131
|
})
|
|
132
132
|
|
|
133
133
|
it('should return true if mime matches exactly', () => {
|
|
134
|
-
flag.mockReturnValue(
|
|
135
|
-
JSON.stringify({ types: ['application/pdf', 'text/plain'] })
|
|
136
|
-
)
|
|
134
|
+
flag.mockReturnValue({ types: ['application/pdf', 'text/plain'] })
|
|
137
135
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(true)
|
|
138
136
|
expect(isFileSummaryCompatible({ mime: 'text/plain' })).toBe(true)
|
|
139
137
|
})
|
|
140
138
|
|
|
141
139
|
it('should handle case-insensitive mime matching', () => {
|
|
142
|
-
flag.mockReturnValue(
|
|
143
|
-
JSON.stringify({ types: ['application/PDF', 'TEXT/plain'] })
|
|
144
|
-
)
|
|
140
|
+
flag.mockReturnValue({ types: ['application/PDF', 'TEXT/plain'] })
|
|
145
141
|
expect(isFileSummaryCompatible({ mime: 'APPLICATION/pdf' })).toBe(true)
|
|
146
142
|
expect(isFileSummaryCompatible({ mime: 'text/PLAIN' })).toBe(true)
|
|
147
143
|
})
|
|
148
144
|
|
|
149
145
|
it('should return false if mime does not match', () => {
|
|
150
|
-
flag.mockReturnValue(
|
|
146
|
+
flag.mockReturnValue({ types: ['application/pdf'] })
|
|
151
147
|
expect(isFileSummaryCompatible({ mime: 'text/plain' })).toBe(false)
|
|
152
148
|
})
|
|
153
149
|
|
|
154
150
|
it('should handle wildcard types', () => {
|
|
155
|
-
flag.mockReturnValue(
|
|
151
|
+
flag.mockReturnValue({ types: ['text/*'] })
|
|
156
152
|
expect(isFileSummaryCompatible({ mime: 'text/plain' })).toBe(true)
|
|
157
153
|
expect(isFileSummaryCompatible({ mime: 'text/markdown' })).toBe(true)
|
|
158
154
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(false)
|
|
159
155
|
})
|
|
160
156
|
|
|
161
157
|
it('should handle case-insensitive wildcard matching', () => {
|
|
162
|
-
flag.mockReturnValue(
|
|
158
|
+
flag.mockReturnValue({ types: ['TEXT/*'] })
|
|
163
159
|
expect(isFileSummaryCompatible({ mime: 'text/plain' })).toBe(true)
|
|
164
160
|
expect(isFileSummaryCompatible({ mime: 'TEXT/markdown' })).toBe(true)
|
|
165
161
|
})
|
|
166
162
|
|
|
167
163
|
it('should respect page limit when pageCount is provided', () => {
|
|
168
|
-
flag.mockReturnValue(
|
|
169
|
-
JSON.stringify({ types: ['application/pdf'], pageLimit: 50 })
|
|
170
|
-
)
|
|
164
|
+
flag.mockReturnValue({ types: ['application/pdf'], pageLimit: 50 })
|
|
171
165
|
expect(
|
|
172
166
|
isFileSummaryCompatible({ mime: 'application/pdf' }, { pageCount: 10 })
|
|
173
167
|
).toBe(true)
|
|
@@ -180,9 +174,7 @@ describe('helpers', () => {
|
|
|
180
174
|
})
|
|
181
175
|
|
|
182
176
|
it('should return false if pageCount is 0 or negative', () => {
|
|
183
|
-
flag.mockReturnValue(
|
|
184
|
-
JSON.stringify({ types: ['application/pdf'], pageLimit: 50 })
|
|
185
|
-
)
|
|
177
|
+
flag.mockReturnValue({ types: ['application/pdf'], pageLimit: 50 })
|
|
186
178
|
expect(
|
|
187
179
|
isFileSummaryCompatible({ mime: 'application/pdf' }, { pageCount: 0 })
|
|
188
180
|
).toBe(false)
|
|
@@ -192,14 +184,12 @@ describe('helpers', () => {
|
|
|
192
184
|
})
|
|
193
185
|
|
|
194
186
|
it('should return true if pageCount is not provided and no pageLimit is set', () => {
|
|
195
|
-
flag.mockReturnValue(
|
|
187
|
+
flag.mockReturnValue({ types: ['application/pdf'] })
|
|
196
188
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(true)
|
|
197
189
|
})
|
|
198
190
|
|
|
199
191
|
it('should return true if pageCount is not provided but pageLimit is set', () => {
|
|
200
|
-
flag.mockReturnValue(
|
|
201
|
-
JSON.stringify({ types: ['application/pdf'], pageLimit: 50 })
|
|
202
|
-
)
|
|
192
|
+
flag.mockReturnValue({ types: ['application/pdf'], pageLimit: 50 })
|
|
203
193
|
expect(isFileSummaryCompatible({ mime: 'application/pdf' })).toBe(true)
|
|
204
194
|
})
|
|
205
195
|
})
|