cozy-search 2.2.18 → 2.2.19

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.
@@ -102,7 +102,7 @@ var ConversationListItem = function ConversationListItem(_ref) {
102
102
  }, (0, _helpers.getNameOfConversation)(conversation)),
103
103
  secondary: /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("span", {
104
104
  className: (0, _classnames.default)('u-db u-ellipsis', styles['conversation-list-item-subtitle'])
105
- }, (0, _helpers.getDescriptionOfConversation)(conversation)), /*#__PURE__*/_react.default.createElement("span", {
105
+ }, (0, _helpers.getDescriptionOfConversation)(conversation, t)), /*#__PURE__*/_react.default.createElement("span", {
106
106
  className: (0, _classnames.default)('u-flex u-flex-items-center', styles['conversation-list-item-subtitle'], styles['conversation-list-item-meta'])
107
107
  }, /*#__PURE__*/_react.default.createElement(_AssistantAvatar.default, {
108
108
  assistant: conversation.assistant,
@@ -106,7 +106,7 @@ var ConversationListItemWider = function ConversationListItemWider(_ref) {
106
106
  secondary: /*#__PURE__*/_react.default.createElement(_Typography.default, {
107
107
  variant: "h6",
108
108
  className: "u-coolGrey"
109
- }, (0, _helpers.getDescriptionOfConversation)(conversation))
109
+ }, (0, _helpers.getDescriptionOfConversation)(conversation, t))
110
110
  }), /*#__PURE__*/_react.default.createElement(_Typography.default, {
111
111
  className: "u-miw-4 u-fz-xsmall u-ta-right u-dn-s"
112
112
  }, (0, _helpers.formatConversationDate)((_conversation$cozyMet = conversation.cozyMetadata) === null || _conversation$cozyMet === void 0 ? void 0 : _conversation$cozyMet.updatedAt, t, lang)));
@@ -63,13 +63,13 @@ function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Re
63
63
 
64
64
  var log = (0, _cozyMinilog.default)('🔍 [CozyAssistantRuntimeProvider]');
65
65
 
66
- var convertMessagesToThreadMessages = function convertMessagesToThreadMessages(messages) {
66
+ var convertMessagesToThreadMessages = function convertMessagesToThreadMessages(messages, t) {
67
67
  if (!messages) return [];
68
68
  return messages.map(function (msg, idx) {
69
69
  return {
70
70
  id: msg.id || "msg-".concat(idx),
71
71
  role: msg.role,
72
- content: (0, _helpers.sanitizeChatContent)(msg.content),
72
+ content: (0, _helpers.formatAnswer)(msg, t),
73
73
  metadata: msg.role === 'assistant' && msg.sources ? {
74
74
  custom: {
75
75
  sources: msg.sources
@@ -85,6 +85,9 @@ var ConversationLoader = function ConversationLoader(_ref) {
85
85
  var children = _ref.children,
86
86
  conversationId = _ref.conversationId;
87
87
 
88
+ var _useI18n = (0, _twakeI18n.useI18n)(),
89
+ t = _useI18n.t;
90
+
88
91
  var _useAssistant = (0, _AssistantProvider.useAssistant)(),
89
92
  setSelectedAssistantId = _useAssistant.setSelectedAssistantId;
90
93
 
@@ -93,8 +96,8 @@ var ConversationLoader = function ConversationLoader(_ref) {
93
96
  var conversation = queryResult.data;
94
97
  var isLoading = (0, _cozyClient.isQueryLoading)(queryResult);
95
98
  var initialMessages = (0, _react2.useMemo)(function () {
96
- return convertMessagesToThreadMessages(conversation === null || conversation === void 0 ? void 0 : conversation.messages);
97
- }, [conversation === null || conversation === void 0 ? void 0 : conversation.messages]);
99
+ return convertMessagesToThreadMessages(conversation === null || conversation === void 0 ? void 0 : conversation.messages, t);
100
+ }, [conversation === null || conversation === void 0 ? void 0 : conversation.messages, t]);
98
101
  (0, _react2.useEffect)(function () {
99
102
  var _conversation$relatio, _conversation$relatio2, _conversation$relatio3;
100
103
 
@@ -121,8 +124,8 @@ var CozyAssistantRuntimeProviderInner = function CozyAssistantRuntimeProviderInn
121
124
  conversationId = _ref2.conversationId,
122
125
  initialMessages = _ref2.initialMessages;
123
126
 
124
- var _useI18n = (0, _twakeI18n.useI18n)(),
125
- t = _useI18n.t;
127
+ var _useI18n2 = (0, _twakeI18n.useI18n)(),
128
+ t = _useI18n2.t;
126
129
 
127
130
  var client = (0, _cozyClient.useClient)();
128
131
  var streamBridgeRef = (0, _react2.useRef)(new _StreamBridge.StreamBridge());
@@ -332,8 +335,8 @@ var CozyAssistantErrorBoundary = /*#__PURE__*/function (_React$Component) {
332
335
  }(_react2.default.Component);
333
336
 
334
337
  var CozyAssistantRuntimeProviderWithErrorBoundary = function CozyAssistantRuntimeProviderWithErrorBoundary(props) {
335
- var _useI18n2 = (0, _twakeI18n.useI18n)(),
336
- t = _useI18n2.t;
338
+ var _useI18n3 = (0, _twakeI18n.useI18n)(),
339
+ t = _useI18n3.t;
337
340
 
338
341
  return /*#__PURE__*/_react2.default.createElement(CozyAssistantErrorBoundary, {
339
342
  t: t
@@ -66,7 +66,7 @@ var createCozyRealtimeChatAdapter = function createCozyRealtimeChatAdapter(optio
66
66
  var messages = _ref.messages,
67
67
  abortSignal = _ref.abortSignal;
68
68
  return (0, _wrapAsyncGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
69
- var client, conversationId, assistantId, websearchEnabled, streamBridge, userQuery, stream, fullText, wasAborted, _iteratorAbruptCompletion, _didIteratorError, _iteratorError, _iterator, _step, chunk, sanitizedText, finalText, sources;
69
+ var client, conversationId, assistantId, websearchEnabled, streamBridge, userQuery, stream, fullText, wasAborted, _iteratorAbruptCompletion, _didIteratorError, _iteratorError, _iterator, _step, chunk, sanitizedText, sources;
70
70
 
71
71
  return _regenerator.default.wrap(function _callee$(_context) {
72
72
  while (1) {
@@ -133,25 +133,24 @@ var createCozyRealtimeChatAdapter = function createCozyRealtimeChatAdapter(optio
133
133
 
134
134
  case 23:
135
135
  if (!(_iteratorAbruptCompletion = !(_step = _context.sent).done)) {
136
- _context.next = 36;
136
+ _context.next = 35;
137
137
  break;
138
138
  }
139
139
 
140
140
  chunk = _step.value;
141
141
 
142
142
  if (!(abortSignal !== null && abortSignal !== void 0 && abortSignal.aborted)) {
143
- _context.next = 29;
143
+ _context.next = 28;
144
144
  break;
145
145
  }
146
146
 
147
147
  wasAborted = true;
148
- streamBridge.cleanup(conversationId);
149
- return _context.abrupt("break", 36);
148
+ return _context.abrupt("break", 35);
150
149
 
151
- case 29:
150
+ case 28:
152
151
  fullText += chunk;
153
152
  sanitizedText = (0, _helpers.sanitizeChatContent)(fullText);
154
- _context.next = 33;
153
+ _context.next = 32;
155
154
  return {
156
155
  content: [{
157
156
  type: 'text',
@@ -162,62 +161,64 @@ var createCozyRealtimeChatAdapter = function createCozyRealtimeChatAdapter(optio
162
161
  }
163
162
  };
164
163
 
165
- case 33:
164
+ case 32:
166
165
  _iteratorAbruptCompletion = false;
167
166
  _context.next = 21;
168
167
  break;
169
168
 
170
- case 36:
171
- _context.next = 42;
169
+ case 35:
170
+ _context.next = 41;
172
171
  break;
173
172
 
174
- case 38:
175
- _context.prev = 38;
173
+ case 37:
174
+ _context.prev = 37;
176
175
  _context.t0 = _context["catch"](19);
177
176
  _didIteratorError = true;
178
177
  _iteratorError = _context.t0;
179
178
 
180
- case 42:
179
+ case 41:
180
+ _context.prev = 41;
181
181
  _context.prev = 42;
182
- _context.prev = 43;
183
182
 
184
183
  if (!(_iteratorAbruptCompletion && _iterator.return != null)) {
185
- _context.next = 47;
184
+ _context.next = 46;
186
185
  break;
187
186
  }
188
187
 
189
- _context.next = 47;
188
+ _context.next = 46;
190
189
  return (0, _awaitAsyncGenerator2.default)(_iterator.return());
191
190
 
192
- case 47:
193
- _context.prev = 47;
191
+ case 46:
192
+ _context.prev = 46;
194
193
 
195
194
  if (!_didIteratorError) {
196
- _context.next = 50;
195
+ _context.next = 49;
197
196
  break;
198
197
  }
199
198
 
200
199
  throw _iteratorError;
201
200
 
201
+ case 49:
202
+ return _context.finish(46);
203
+
202
204
  case 50:
203
- return _context.finish(47);
205
+ return _context.finish(41);
204
206
 
205
207
  case 51:
206
- return _context.finish(42);
207
-
208
- case 52:
209
- if (wasAborted) {
210
- _context.next = 58;
208
+ if (!(!wasAborted && !(abortSignal !== null && abortSignal !== void 0 && abortSignal.aborted))) {
209
+ _context.next = 55;
211
210
  break;
212
211
  }
213
212
 
214
- finalText = (0, _helpers.sanitizeChatContent)(fullText);
215
213
  sources = streamBridge.getSources(conversationId);
216
- _context.next = 57;
214
+ _context.next = 55;
217
215
  return _objectSpread({
218
216
  content: [{
219
217
  type: 'text',
220
- text: finalText
218
+ text: (0, _helpers.formatAnswer)({
219
+ role: 'assistant',
220
+ content: fullText
221
+ }, t)
221
222
  }],
222
223
  status: {
223
224
  type: 'complete',
@@ -231,19 +232,17 @@ var createCozyRealtimeChatAdapter = function createCozyRealtimeChatAdapter(optio
231
232
  }
232
233
  } : {});
233
234
 
234
- case 57:
235
+ case 55:
235
236
  streamBridge.cleanup(conversationId);
236
-
237
- case 58:
238
- _context.next = 66;
237
+ _context.next = 64;
239
238
  break;
240
239
 
241
- case 60:
242
- _context.prev = 60;
240
+ case 58:
241
+ _context.prev = 58;
243
242
  _context.t1 = _context["catch"](10);
244
243
  log.error('Error:', _context.t1);
245
244
  streamBridge.cleanup(conversationId);
246
- _context.next = 66;
245
+ _context.next = 64;
247
246
  return {
248
247
  content: [{
249
248
  type: 'text',
@@ -260,12 +259,12 @@ var createCozyRealtimeChatAdapter = function createCozyRealtimeChatAdapter(optio
260
259
  }
261
260
  };
262
261
 
263
- case 66:
262
+ case 64:
264
263
  case "end":
265
264
  return _context.stop();
266
265
  }
267
266
  }
268
- }, _callee, null, [[10, 60], [19, 38, 42, 52], [43,, 47, 51]]);
267
+ }, _callee, null, [[10, 58], [19, 37, 41, 51], [42,, 46, 50]]);
269
268
  }))();
270
269
  }
271
270
  };
@@ -1,4 +1,7 @@
1
- import type { ChatModelRunOptions } from '@assistant-ui/react'
1
+ import type {
2
+ ChatModelRunOptions,
3
+ ChatModelRunResult
4
+ } from '@assistant-ui/react'
2
5
 
3
6
  import { createCozyRealtimeChatAdapter } from './CozyRealtimeChatAdapter'
4
7
  import type { StreamBridge } from './StreamBridge'
@@ -38,6 +41,27 @@ const runAdapter = async (assistantId?: string): Promise<jest.Mock> => {
38
41
  return fetchJSON
39
42
  }
40
43
 
44
+ const runAdapterAndCollectResults = async (
45
+ streamBridge: StreamBridge
46
+ ): Promise<ChatModelRunResult[]> => {
47
+ const adapter = createCozyRealtimeChatAdapter(
48
+ {
49
+ client: { stackClient: { fetchJSON: jest.fn().mockResolvedValue({}) } },
50
+ conversationId: 'conv-1'
51
+ },
52
+ key => key,
53
+ { current: streamBridge }
54
+ )
55
+ const generator = adapter.run(
56
+ makeRunOptions()
57
+ ) as AsyncGenerator<ChatModelRunResult>
58
+ const results: ChatModelRunResult[] = []
59
+ for await (const result of generator) {
60
+ results.push(result)
61
+ }
62
+ return results
63
+ }
64
+
41
65
  describe('CozyRealtimeChatAdapter', () => {
42
66
  it('sends the assistantID for a real assistant', async () => {
43
67
  const fetchJSON = await runAdapter('real-assistant-id')
@@ -67,4 +91,28 @@ describe('CozyRealtimeChatAdapter', () => {
67
91
  ]
68
92
  expect(body).not.toHaveProperty('assistantID')
69
93
  })
94
+
95
+ it('yields the fallback message when the stream completes with no content', async () => {
96
+ const results = await runAdapterAndCollectResults(makeStreamBridge())
97
+ const finalResult = results[results.length - 1]
98
+ expect(finalResult.content).toEqual([
99
+ { type: 'text', text: 'assistant.default_empty_response' }
100
+ ])
101
+ })
102
+
103
+ it('yields the sanitized content when the stream returns text', async () => {
104
+ const streamBridge = {
105
+ // eslint-disable-next-line @typescript-eslint/require-await
106
+ createStream: jest.fn(async function* () {
107
+ yield 'Hello '
108
+ yield 'world'
109
+ }),
110
+ getSources: jest.fn(() => null),
111
+ cleanup: jest.fn()
112
+ } as unknown as StreamBridge
113
+
114
+ const results = await runAdapterAndCollectResults(streamBridge)
115
+ const finalResult = results[results.length - 1]
116
+ expect(finalResult.content).toEqual([{ type: 'text', text: 'Hello world' }])
117
+ })
70
118
  })
@@ -1,6 +1,10 @@
1
1
  export function makeConversationId(): string;
2
2
  export function isAssistantEnabled(): any;
3
3
  export function sanitizeChatContent(content: string): string;
4
+ export function formatAnswer(message: {
5
+ role: string;
6
+ content: string;
7
+ }, t: (key: string) => string): string;
4
8
  export function formatConversationDate(dateString: any, t: any, lang: any): string;
5
9
  export function getNameOfConversation(conversation: any): any;
6
- export function getDescriptionOfConversation(conversation: any): any;
10
+ export function getDescriptionOfConversation(conversation: any, t: any): string | undefined;
@@ -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.sanitizeChatContent = exports.makeConversationId = exports.isAssistantEnabled = exports.getNameOfConversation = exports.getDescriptionOfConversation = exports.formatConversationDate = void 0;
8
+ exports.sanitizeChatContent = exports.makeConversationId = exports.isAssistantEnabled = exports.getNameOfConversation = exports.getDescriptionOfConversation = exports.formatConversationDate = exports.formatAnswer = void 0;
9
9
 
10
10
  var _cozyFlags = _interopRequireDefault(require("cozy-flags"));
11
11
 
@@ -41,9 +41,26 @@ var sanitizeChatContent = function sanitizeChatContent(content) {
41
41
  .replace(/\s?\[Source\s+\d+(?:\s*,\s*(?:Source\s+)?\d+)*\]/g, '') // remove « [Sources: 1, 3, 6] » citations, with optional empty link parens
42
42
  .replace(/\s?\[Sources?:\s*\d+(?:\s*,\s*\d+)*\s*\](?:\([^)]*\))?/g, '');
43
43
  };
44
+ /**
45
+ * Formats a chat message's answer: sanitizes citation markup, and
46
+ * substitutes a translated fallback when an assistant answer is empty.
47
+ * Used wherever an assistant response may come back empty from the RAG backend.
48
+ *
49
+ * @param {{ role: string, content: string }} message
50
+ * @param {(key: string) => string} t - translation function from twake-i18n
51
+ * @returns {string}
52
+ */
53
+
44
54
 
45
55
  exports.sanitizeChatContent = sanitizeChatContent;
46
56
 
57
+ var formatAnswer = function formatAnswer(message, t) {
58
+ var sanitized = sanitizeChatContent(message.content);
59
+ return message.role === 'assistant' && !sanitized.trim() ? t('assistant.default_empty_response') : sanitized;
60
+ };
61
+
62
+ exports.formatAnswer = formatAnswer;
63
+
47
64
  var formatConversationDate = function formatConversationDate(dateString, t, lang) {
48
65
  if (!dateString) return '';
49
66
  var date = new Date(dateString);
@@ -91,11 +108,12 @@ var getNameOfConversation = function getNameOfConversation(conversation) {
91
108
 
92
109
  exports.getNameOfConversation = getNameOfConversation;
93
110
 
94
- var getDescriptionOfConversation = function getDescriptionOfConversation(conversation) {
95
- var _conversation$message4, _conversation$message5;
111
+ var getDescriptionOfConversation = function getDescriptionOfConversation(conversation, t) {
112
+ var _conversation$message4;
96
113
 
97
- var content = (_conversation$message4 = conversation.messages) === null || _conversation$message4 === void 0 ? void 0 : (_conversation$message5 = _conversation$message4[conversation.messages.length - 1]) === null || _conversation$message5 === void 0 ? void 0 : _conversation$message5.content;
98
- return content && sanitizeChatContent(content);
114
+ var lastMessage = conversation === null || conversation === void 0 ? void 0 : (_conversation$message4 = conversation.messages) === null || _conversation$message4 === void 0 ? void 0 : _conversation$message4[conversation.messages.length - 1];
115
+ if (!lastMessage) return undefined;
116
+ return formatAnswer(lastMessage, t);
99
117
  };
100
118
 
101
119
  exports.getDescriptionOfConversation = getDescriptionOfConversation;
@@ -179,4 +179,25 @@ describe('getDescriptionOfConversation', () => {
179
179
  }
180
180
  expect(getDescriptionOfConversation(convo)).toBe('It is 4')
181
181
  })
182
+
183
+ it('returns the translated fallback when the last assistant message has no content', () => {
184
+ const t = key => key
185
+ const convo = {
186
+ messages: [
187
+ { role: 'user', content: 'sum?' },
188
+ { role: 'assistant', content: '' }
189
+ ]
190
+ }
191
+ expect(getDescriptionOfConversation(convo, t)).toBe(
192
+ 'assistant.default_empty_response'
193
+ )
194
+ })
195
+
196
+ it('does not apply the fallback when the last message is from the user', () => {
197
+ const t = key => key
198
+ const convo = {
199
+ messages: [{ role: 'user', content: '[Source 1]' }]
200
+ }
201
+ expect(getDescriptionOfConversation(convo, t)).toBe('')
202
+ })
182
203
  })
@@ -47,6 +47,7 @@
47
47
  "yesterday": "Yesterday"
48
48
  },
49
49
  "default_error": "An error occurred during response generation. Please try again",
50
+ "default_empty_response": "I could not find an answer to your request. Please try rephrasing it",
50
51
  "hide": "Hide",
51
52
  "show": "Show",
52
53
  "search_conversation": {
@@ -47,6 +47,7 @@
47
47
  "yesterday": "Hier"
48
48
  },
49
49
  "default_error": "Une erreur est survenue lors de la génération de la réponse. Veuillez réessayer",
50
+ "default_empty_response": "Je n'ai pas trouvé de réponse à votre demande. Essayez de reformuler votre question",
50
51
  "hide": "Masquer",
51
52
  "show": "Afficher",
52
53
  "search_conversation": {
@@ -53,6 +53,7 @@ var en = {
53
53
  yesterday: "Yesterday"
54
54
  },
55
55
  default_error: "An error occurred during response generation. Please try again",
56
+ default_empty_response: "I could not find an answer to your request. Please try rephrasing it",
56
57
  hide: "Hide",
57
58
  show: "Show",
58
59
  search_conversation: {
@@ -236,6 +237,7 @@ var fr = {
236
237
  yesterday: "Hier"
237
238
  },
238
239
  default_error: "Une erreur est survenue lors de la g\xE9n\xE9ration de la r\xE9ponse. Veuillez r\xE9essayer",
240
+ default_empty_response: "Je n'ai pas trouv\xE9 de r\xE9ponse \xE0 votre demande. Essayez de reformuler votre question",
239
241
  hide: "Masquer",
240
242
  show: "Afficher",
241
243
  search_conversation: {
@@ -409,6 +411,7 @@ var ru = {
409
411
  yesterday: "\u0412\u0447\u0435\u0440\u0430"
410
412
  },
411
413
  default_error: "\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043E\u0448\u0438\u0431\u043A\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u0441\u043D\u043E\u0432\u0430",
414
+ default_empty_response: "\u042F \u043D\u0435 \u0441\u043C\u043E\u0433 \u043D\u0430\u0439\u0442\u0438 \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 \u0432\u0430\u0448 \u0437\u0430\u043F\u0440\u043E\u0441. \u041F\u043E\u043F\u0440\u043E\u0431\u0443\u0439\u0442\u0435 \u043F\u0435\u0440\u0435\u0444\u043E\u0440\u043C\u0443\u043B\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u043E\u043F\u0440\u043E\u0441",
412
415
  hide: "\u0421\u043A\u0440\u044B\u0442\u044C",
413
416
  show: "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C",
414
417
  search_conversation: {
@@ -616,6 +619,7 @@ var vi = {
616
619
  yesterday: "H\xF4m qua"
617
620
  },
618
621
  default_error: "\u0110\xE3 x\u1EA3y ra l\u1ED7i, vui l\xF2ng th\u1EED l\u1EA1i",
622
+ default_empty_response: "T\xF4i kh\xF4ng t\xECm th\u1EA5y c\xE2u tr\u1EA3 l\u1EDDi cho y\xEAu c\u1EA7u c\u1EE7a b\u1EA1n. Vui l\xF2ng th\u1EED di\u1EC5n \u0111\u1EA1t l\u1EA1i",
619
623
  hide: "\u1EA8n",
620
624
  show: "Hi\u1EC7n",
621
625
  message: {
@@ -37,6 +37,7 @@
37
37
  "yesterday": "Вчера"
38
38
  },
39
39
  "default_error": "Произошла ошибка, попробуйте снова",
40
+ "default_empty_response": "Я не смог найти ответ на ваш запрос. Попробуйте переформулировать вопрос",
40
41
  "hide": "Скрыть",
41
42
  "show": "Показать",
42
43
  "search_conversation": {
@@ -44,6 +44,7 @@
44
44
  "yesterday": "Hôm qua"
45
45
  },
46
46
  "default_error": "Đã xảy ra lỗi, vui lòng thử lại",
47
+ "default_empty_response": "Tôi không tìm thấy câu trả lời cho yêu cầu của bạn. Vui lòng thử diễn đạt lại",
47
48
  "hide": "Ẩn",
48
49
  "show": "Hiện",
49
50
  "message": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cozy-search",
3
3
  "description": "UI components about search bar and IA assistant",
4
- "version": "2.2.18",
4
+ "version": "2.2.19",
5
5
  "author": {
6
6
  "name": "Cozy Cloud",
7
7
  "url": "https://github.com/cozy"
@@ -96,5 +96,5 @@
96
96
  "lint": "cd .. && yarn eslint --ext js,jsx,ts packages/cozy-search"
97
97
  },
98
98
  "types": "dist/index.d.ts",
99
- "gitHead": "c442a0903e06f8877c303548a9d22be5ee60bf01"
99
+ "gitHead": "9945afd53a9a030aa0a80b15380191db260d30c0"
100
100
  }