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 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, _helpers.getSummaryConfig)();
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 fetchSummary = (0, _react.useCallback)( /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2() {
189
- var response, errorMessage;
190
- return _regenerator.default.wrap(function _callee2$(_context2) {
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 (_context2.prev = _context2.next) {
252
+ switch (_context3.prev = _context3.next) {
193
253
  case 0:
194
- if (file) {
195
- _context2.next = 2;
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 _context2.abrupt("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
- case 2:
270
+ return _context3.abrupt("return");
271
+
272
+ case 6:
273
+ inFlightFileIdRef.current = targetFileId;
202
274
  setIsLoading(true);
203
275
  setError(null);
204
- _context2.prev = 4;
205
- _context2.next = 7;
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 7:
213
- response = _context2.sent;
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 (response && response.content) {
216
- setSummary(response.content);
217
- } else if (response && response.choices && response.choices[0]) {
218
- setSummary(response.choices[0].message.content);
288
+ if (!(activeFileIdRef.current !== targetFileId)) {
289
+ _context3.next = 16;
290
+ break;
219
291
  }
220
292
 
221
- _context2.next = 15;
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 11:
225
- _context2.prev = 11;
226
- _context2.t0 = _context2["catch"](4);
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
- case 15:
231
- _context2.prev = 15;
232
- setIsLoading(false);
233
- return _context2.finish(15);
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
- case 18:
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 _context2.stop();
329
+ return _context3.stop();
238
330
  }
239
331
  }
240
- }, _callee2, null, [[4, 11, 15, 18]]);
332
+ }, _callee3, null, [[9, 22, 25, 29]]);
241
333
  })), [client, file, t]);
242
334
 
243
335
  var handleRefresh = function handleRefresh() {
244
- fetchSummary();
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.getSummaryConfig = exports.formatDate = void 0;
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.getSummaryConfig = getSummaryConfig;
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 = getSummaryConfig();
140
+ var summaryConfig = (0, _cozyFlags.default)('drive.summary');
166
141
 
167
142
  if (!summaryConfig) {
168
143
  return false;
169
144
  }
170
145
 
171
- if (!summaryConfig || !Array.isArray(summaryConfig.types) || summaryConfig.types.length === 0) {
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.2",
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.8",
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.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": "47ff1f9d71e58f22aafb44c29dc68dd9658f5206"
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 { getSummaryConfig, roughTokensEstimation } from '../../helpers'
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 = getSummaryConfig()
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 fetchSummary = useCallback(async () => {
88
- if (!file) return
89
-
90
- setIsLoading(true)
91
- setError(null)
91
+ const persistedSummary = async (
92
+ fileMetadata,
93
+ targetFileId,
94
+ summaryContent
95
+ ) => {
92
96
  try {
93
- const response = await summarizeFile({ client, file, stream: false })
94
- if (response && response.content) {
95
- setSummary(response.content)
96
- } else if (response && response.choices && response.choices[0]) {
97
- setSummary(response.choices[0].message.content)
98
- }
99
- } catch (err) {
100
- const errorMessage =
101
- err.code === 'DOCUMENT_TOO_LARGE'
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
- }, [client, file, t])
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
- fetchSummary()
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 = getSummaryConfig()
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
 
@@ -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 JSON.stringify({
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 JSON is invalid', () => {
114
- flag.mockReturnValue('invalid json')
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(JSON.stringify({}))
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(JSON.stringify({ types: [] }))
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(JSON.stringify({ types: 'application/pdf' }))
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(JSON.stringify({ types: ['application/pdf'] }))
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(JSON.stringify({ types: ['text/*'] }))
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(JSON.stringify({ types: ['TEXT/*'] }))
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(JSON.stringify({ types: ['application/pdf'] }))
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
  })