@crowdin/app-project-module 0.66.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,26 +2,12 @@
2
2
 
3
3
  Module that will automatically add all necessary endpoints for Crowdin App.
4
4
 
5
- :bookmark: See the [docs](/docs) for more information.
5
+ :bookmark: See the [docs](https://crowdin.github.io/app-project-module/) for more information.
6
6
 
7
7
  ## Contributing
8
8
 
9
- If you want to contribute please read the [Contributing](/CONTRIBUTING.md) guidelines.
9
+ If you would like to contribute please read the [Contributing](/CONTRIBUTING.md) guidelines.
10
10
 
11
11
  ## Seeking Assistance
12
12
 
13
- If you find any problems or would like to suggest a feature, please feel free to file an issue on Github at [Issues Page](https://github.com/crowdin/app-project-module/issues).
14
-
15
- If you've found an error in these samples, please [Contact Customer Success Service](https://crowdin.com/contacts).
16
-
17
- ## License
18
-
19
- <pre>
20
- The Crowdin App Project module is licensed under the MIT License.
21
- See the LICENSE.md file distributed with this work for additional
22
- information regarding copyright ownership.
23
-
24
- Except as contained in the LICENSE file, the name(s) of the above copyright
25
- holders shall not be used in advertising or otherwise to promote the sale,
26
- use or other dealings in this Software without prior written authorization.
27
- </pre>
13
+ If you find any problems or would like to suggest a feature, please feel free to file an issue on GitHub at [Issues Page](https://github.com/crowdin/app-project-module/issues) or [Contact Customer Success Service](https://crowdin.com/contacts).
@@ -13,13 +13,43 @@ const util_1 = require("../../../util");
13
13
  const logger_1 = require("../../../util/logger");
14
14
  const files_1 = require("../../integration/util/files");
15
15
  const util_2 = require("../util");
16
+ // more about roles in AI messages https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages
17
+ // Crowdin documentation https://support.crowdin.com/developer/crowdin-apps-module-custom-ai/#expected-response-from-the-app
18
+ const ROLES = {
19
+ assistant: 'assistant',
20
+ };
21
+ const DEFAULT_ROLE = ROLES.assistant;
22
+ const getErrorStatus = (e) => {
23
+ if (typeof e === 'object' && 'status' in e && typeof e.status === 'number') {
24
+ return e.status;
25
+ }
26
+ return 500;
27
+ };
28
+ const getErrorCode = (e) => {
29
+ if (typeof e === 'object' && 'code' in e) {
30
+ return e.code;
31
+ }
32
+ return undefined;
33
+ };
16
34
  function handle(aiProvider) {
17
35
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
36
+ let isStream = false;
18
37
  try {
19
38
  let choices;
20
39
  let message;
21
40
  let data;
22
- const { crowdinApiClient: client, crowdinContext: context, body: { messages, model, action, responseFormat: { type: responseFormatType } = { type: undefined } }, } = req;
41
+ const chunks = [];
42
+ const { crowdinApiClient: client, crowdinContext: context, body: { messages, model, action, stream, responseFormat: { type: responseFormatType } = { type: undefined }, }, } = req;
43
+ isStream = !!stream;
44
+ const startStream = () => {
45
+ if (!res.headersSent) {
46
+ res.writeHead(200, {
47
+ Connection: 'keep-alive',
48
+ 'Cache-Control': 'no-cache',
49
+ 'Content-Type': 'text/event-stream',
50
+ });
51
+ }
52
+ };
23
53
  const result = yield aiProvider.chatCompletions({
24
54
  messages,
25
55
  model,
@@ -28,7 +58,32 @@ function handle(aiProvider) {
28
58
  client,
29
59
  context,
30
60
  req,
61
+ isStream,
62
+ sendEvent: ({ content, role }) => __awaiter(this, void 0, void 0, function* () {
63
+ if (!isStream) {
64
+ chunks.push({ content, role });
65
+ return;
66
+ }
67
+ startStream();
68
+ const message = {
69
+ choices: [
70
+ {
71
+ delta: {
72
+ role: role || DEFAULT_ROLE,
73
+ content,
74
+ },
75
+ },
76
+ ],
77
+ };
78
+ res.write(`data: ${JSON.stringify(message)}\n\n`);
79
+ }),
31
80
  });
81
+ if (isStream) {
82
+ startStream();
83
+ res.write('[DONE]');
84
+ res.end();
85
+ return;
86
+ }
32
87
  if ((0, files_1.isExtendedResultType)(result)) {
33
88
  data = result.data;
34
89
  message = result.message;
@@ -36,10 +91,18 @@ function handle(aiProvider) {
36
91
  else {
37
92
  data = result;
38
93
  }
94
+ if (chunks.length) {
95
+ data = [
96
+ {
97
+ role: chunks[0].role || DEFAULT_ROLE,
98
+ content: chunks.map((chunk) => chunk.content).join(''),
99
+ },
100
+ ];
101
+ }
39
102
  if (data && data.length > 0) {
40
103
  choices = data.map((message) => ({
41
104
  message: {
42
- role: message.role || 'assistant',
105
+ role: message.role || DEFAULT_ROLE,
43
106
  content: message.content,
44
107
  },
45
108
  }));
@@ -50,10 +113,17 @@ function handle(aiProvider) {
50
113
  if (req.logError) {
51
114
  req.logError(e);
52
115
  }
53
- if ((0, util_2.isRateLimitError)(e)) {
54
- res.status(429);
116
+ if (!res.headersSent) {
117
+ res.status((0, util_2.isRateLimitError)(e) ? 429 : getErrorStatus(e));
118
+ }
119
+ if (isStream && res.headersSent) {
120
+ res.write(`data: ${JSON.stringify({ error: { message: (0, logger_1.getErrorMessage)(e), code: getErrorCode(e) } })}\n\n`);
121
+ res.write('[DONE]');
122
+ res.end();
123
+ }
124
+ else {
125
+ res.send({ error: { message: (0, logger_1.getErrorMessage)(e) } });
55
126
  }
56
- res.send({ error: { message: (0, logger_1.getErrorMessage)(e) } });
57
127
  }
58
128
  }));
59
129
  }
@@ -22,12 +22,13 @@ function handle(aiProvider) {
22
22
  context: req.crowdinContext,
23
23
  });
24
24
  const data = modelList.map((model) => {
25
- var _a, _b, _c;
25
+ var _a, _b, _c, _d;
26
26
  return ({
27
27
  id: model.id,
28
28
  supportsJsonMode: (_a = model.supportsJsonMode) !== null && _a !== void 0 ? _a : false,
29
29
  supportsFunctionCalling: (_b = model.supportsFunctionCalling) !== null && _b !== void 0 ? _b : false,
30
- contextWindowLimit: (_c = model.contextWindowLimit) !== null && _c !== void 0 ? _c : exports.CONTEXT_WINDOW_LIMIT,
30
+ supportsStreaming: (_c = model.supportsStreaming) !== null && _c !== void 0 ? _c : false,
31
+ contextWindowLimit: (_d = model.contextWindowLimit) !== null && _d !== void 0 ? _d : exports.CONTEXT_WINDOW_LIMIT,
31
32
  });
32
33
  });
33
34
  res.send({ data });
@@ -25,6 +25,8 @@ export interface AiProviderModule extends Environments {
25
25
  client: Crowdin;
26
26
  context: CrowdinContextInfo;
27
27
  req: CrowdinClientRequest;
28
+ isStream: boolean;
29
+ sendEvent: (chunk: Message) => Promise<void>;
28
30
  }) => Promise<Message[] | ExtendedResult<Message[]>>;
29
31
  /**
30
32
  * provides a list of available model
@@ -38,6 +40,7 @@ export interface SupportedModels {
38
40
  id: string;
39
41
  supportsJsonMode?: boolean;
40
42
  supportsFunctionCalling?: boolean;
43
+ supportsStreaming?: boolean;
41
44
  contextWindowLimit?: number;
42
45
  }
43
46
  export interface Message {
@@ -14,16 +14,19 @@ const types_1 = require("../types");
14
14
  const storage_1 = require("../storage");
15
15
  const util_1 = require("../util");
16
16
  const logger_1 = require("../util/logger");
17
+ const connection_1 = require("../util/connection");
17
18
  function handle(config) {
18
19
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
20
  const event = req.body;
20
21
  (0, logger_1.log)(`Received install request ${JSON.stringify(event, null, 2)}`);
21
22
  const token = yield fetchToken(config, event);
23
+ const userId = event.userId;
24
+ const organization = (event.domain || event.organizationId).toString();
22
25
  const credentials = {
23
- id: (event.domain || event.organizationId).toString(),
26
+ id: organization,
24
27
  appSecret: event.appSecret,
25
28
  domain: event.domain,
26
- userId: event.userId,
29
+ userId,
27
30
  agentId: event.agentId,
28
31
  organizationId: event.organizationId,
29
32
  baseUrl: event.baseUrl,
@@ -41,6 +44,11 @@ function handle(config) {
41
44
  yield (0, storage_1.getStorage)().saveCrowdinCredentials(credentials);
42
45
  (0, logger_1.log)('A new App has been installed');
43
46
  }
47
+ if (config.onInstall) {
48
+ const { client } = yield (0, connection_1.prepareCrowdinClient)({ config, credentials, autoRenew: true });
49
+ (0, logger_1.log)('Invoking onInstall hook');
50
+ yield config.onInstall({ organization, userId, client });
51
+ }
44
52
  res.status(204).end();
45
53
  }));
46
54
  }
@@ -106,6 +106,7 @@ function register({ config, app }) {
106
106
  cron.schedule('0 */3 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '3' }).catch(console.error));
107
107
  cron.schedule('0 */6 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '6' }).catch(console.error));
108
108
  cron.schedule('0 */12 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '12' }).catch(console.error));
109
+ cron.schedule('0 0 * * *', () => (0, cron_1.filesCron)({ config, integration: integrationLogic, period: '24' }).catch(console.error));
109
110
  }
110
111
  // remove user errors
111
112
  cron.schedule('0 0 * * *', () => __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,115 @@
1
+ /* text color */
2
+ *[class*="crdn-text"] {
3
+ color: var(--crdn-color, var(--crowdin-body-color));
4
+ }
5
+
6
+ .crdn-text\:primary {
7
+ --crdn-color: var(--crowdin-primary);
8
+ }
9
+
10
+ .crdn-text\:title {
11
+ --crdn-color: var(--crowdin-title-color);
12
+ }
13
+
14
+ .crdn-text\:body {
15
+ --crdn-color: var(--crowdin-body-color);
16
+ }
17
+
18
+ .crdn-text\:muted {
19
+ --crdn-color: var(--crowdin-text-muted);
20
+ }
21
+
22
+ .crdn-text\:disabled {
23
+ --crdn-color: var(--crowdin-text-disabled);
24
+ }
25
+
26
+ .crdn-text\:info {
27
+ --crdn-color: var(--crowdin-info)
28
+ }
29
+
30
+ .crdn-text\:success {
31
+ --crdn-color: var(--crowdin-success)
32
+ }
33
+
34
+ .crdn-text\:warning {
35
+ --crdn-color: var(--crowdin-warning)
36
+ }
37
+
38
+ .crdn-text\:danger {
39
+ --crdn-color: var(--crowdin-danger)
40
+ }
41
+
42
+ /* backgrounds */
43
+
44
+ *[class*="crdn-bg"] {
45
+ background-color: var(--crdn-bg, var(--crowdin-body-bg));
46
+ }
47
+
48
+ .crdn-bg\:lvl-0 {
49
+ --crdn-bg: var(--crowdin-level-0-bg);
50
+ }
51
+
52
+ .crdn-bg\:lvl-1 {
53
+ --crdn-bg: var(--crowdin-level-1-bg);
54
+ }
55
+
56
+ .crdn-bg\:lvl-2 {
57
+ --crdn-bg: var(--crowdin-level-2-bg);
58
+ }
59
+
60
+ .crdn-bg\:lvl-2\:05 {
61
+ --crdn-bg: var(--crowdin-level-2-bg-05);
62
+ }
63
+
64
+ .crdn-bg\:lvl-3 {
65
+ --crdn-bg: var(--crowdin-level-3-bg);
66
+ }
67
+
68
+ .crdn-bg\:primary {
69
+ --crdn-bg: var(--crowdin-primary);
70
+ }
71
+
72
+ .crdn-bg\:success {
73
+ --crdn-bg: var(--crowdin-success-bg)
74
+ }
75
+
76
+ .crdn-bg\:success {
77
+ --crdn-bg: var(--crowdin-success-bg)
78
+ }
79
+
80
+
81
+ .crdn-bg\:warning {
82
+ --crdn-bg: var(--crowdin-warning-bg)
83
+ }
84
+
85
+ .crdn-bg\:danger {
86
+ --crdn-bg: var(--crowdin-danger-bg)
87
+ }
88
+
89
+ /* borders */
90
+
91
+ *[class*="crdn-border"] {
92
+ border-width: 1px;
93
+ border-style: solid;
94
+ border-color: var(--crdn-border, var(--crowdin-border-color));
95
+ }
96
+
97
+ .crdn-border\:primary {
98
+ --crdn-border: var(--crowdin-primary);
99
+ }
100
+
101
+ .crdn-border\:danger {
102
+ --crdn-border: var(--crowdin-danger);
103
+ }
104
+
105
+ .crdn-border\:info {
106
+ --crdn-border: var(--crowdin-danger);
107
+ }
108
+
109
+ .crdn-border\:success {
110
+ --crdn-border: var(--crowdin-success);
111
+ }
112
+
113
+ .crdn-border\:warning {
114
+ --crdn-border: var(--crowdin-warning);
115
+ }
package/out/types.d.ts CHANGED
@@ -140,6 +140,14 @@ export interface ClientConfig extends ImagePath {
140
140
  * modal module
141
141
  */
142
142
  modal?: ModuleContent & UiModule & Environments;
143
+ /**
144
+ * Install hook
145
+ */
146
+ onInstall?: ({ organization, userId, client, }: {
147
+ organization: string;
148
+ userId: number;
149
+ client: Crowdin;
150
+ }) => Promise<void>;
143
151
  /**
144
152
  * Uninstall hook for cleanup logic
145
153
  */
@@ -25,10 +25,10 @@ exports.engine = (0, express_handlebars_1.default)({
25
25
  if (a !== undefined && a !== null && b !== undefined && b !== null) {
26
26
  let bArray;
27
27
  if (Array.isArray(b)) {
28
- bArray = b;
28
+ bArray = b.map((item) => item.toString());
29
29
  }
30
30
  else {
31
- bArray = b.split(' ');
31
+ bArray = b.toString().split(' ');
32
32
  }
33
33
  if (bArray.includes(a === null || a === void 0 ? void 0 : a.toString())) {
34
34
  return options.fn(this);
@@ -44,18 +44,6 @@
44
44
  {{/if}}
45
45
  <crowdin-button icon-before="account_circle" onclick="integrationLogout()">Log out</crowdin-button>
46
46
  </div>
47
- {{#if uploadTranslations}}
48
- <crowdin-alert id="translation-info" no-icon="true" style="display: none;">
49
- <div class="box-center">
50
- <p class="info-text">
51
- We recommend importing existing translations into your Crowdin project for newly uploaded source content.
52
- Please note that translations imported for non-key-value formats may require additional review.
53
- Read more about <crowdin-a href="https://support.crowdin.com/uploading-translations/" target="_blank">Uploading Translations</crowdin-a>.
54
- </p>
55
- </div>
56
- <crowdin-button onclick="closeAlert(this)" class="dismiss-alert" icon>close</crowdin-button>
57
- </crowdin-alert>
58
- {{/if}}
59
47
  </div>
60
48
  <crowdin-simple-integration
61
49
  async-progress
@@ -83,7 +71,7 @@
83
71
  integration-name="{{name}}"
84
72
  integration-logo="logo.png"
85
73
  {{#if uploadTranslations}}
86
- integration-button-menu-items='[{"label":"Upload Translations", "action":"uploadTranslations"}]'
74
+ integration-button-menu-items='[{"label":"Sync translations", "title":"Sync translations to Crowdin", "action":"uploadTranslations"}]'
87
75
  {{/if}}
88
76
  {{#if filtering.crowdinLanguages}}
89
77
  crowdin-filter
@@ -126,7 +114,7 @@
126
114
  <crowdin-modal
127
115
  id="settings-modal"
128
116
  body-overflow-unset="{{#checkLength configurationFields 3}}false{{else}}true{{/checkLength}}"
129
- modal-width="50"
117
+ modal-width="65"
130
118
  modal-title="Settings"
131
119
  close-button-title="Close"
132
120
  >
@@ -179,6 +167,7 @@
179
167
  {{#if dependencySettings}}
180
168
  data-dependency="{{dependencySettings}}"
181
169
  {{/if}}
170
+ is-position-fixed
182
171
  >
183
172
  {{#each options}}
184
173
  <option
@@ -467,18 +456,19 @@
467
456
  appComponent.setIntegrationFilesData(tree);
468
457
  } else if (tree.length) {
469
458
  appComponent.pushIntegrationFilesData(tree).then(() => {
470
- appIntegrationFiles.getSelected().then(selection => {
471
- selection = selection?.filter((node) => node);
472
- if (!selection?.length) {
473
- return;
474
- }
475
-
476
- const selectedIds = selection.map(({id}) => id.toString());
477
- tree.forEach((node) => {
478
- selectedIds.includes(node.parent_id.toString()) && selectedIds.push(node.id.toString());
459
+ if (parentId) {
460
+ appIntegrationFiles.getSelected().then(selection => {
461
+ const selectedIds = selection?.filter((node) => node).map(({id}) => id.toString());
462
+ if (!selectedIds?.length) {
463
+ return;
464
+ }
465
+ const filteredNodes = tree.filter((node) => selectedIds.includes(node.parent_id.toString()));
466
+ filteredNodes.forEach((node) => {
467
+ selectedIds.push(node.id);
468
+ });
469
+ appIntegrationFiles.setSelected(selectedIds);
479
470
  });
480
- appIntegrationFiles.setSelected(selectedIds);
481
- });
471
+ }
482
472
  });
483
473
  }
484
474
  {{#if integrationPagination}}
@@ -1118,11 +1108,6 @@
1118
1108
  localStorage.setItem(`revised_${name}`, 1);
1119
1109
  }
1120
1110
 
1121
- {{#if uploadTranslations}}
1122
- const translationInfo = document.getElementById('translation-info');
1123
- checkAlert(translationInfo);
1124
- {{/if}}
1125
-
1126
1111
  {{#if notice}}
1127
1112
  const notice = document.getElementById('notice');
1128
1113
  checkAlert(notice, 'notice');
@@ -4,6 +4,7 @@
4
4
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
5
5
  <title></title>
6
6
  <link rel="stylesheet" href="/assets/css/styles.css">
7
+ <link rel="stylesheet" href="/assets/css/crowdin-ui-colors.css">
7
8
  <script type="module"
8
9
  src="https://crowdin-web-components.s3.amazonaws.com/crowdin-web-components/crowdin-web-components.esm.js"></script>
9
10
  <script nomodule=""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.66.0",
3
+ "version": "0.67.0",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",
@@ -21,11 +21,11 @@
21
21
  "@aws-sdk/client-s3": "^3.606.0",
22
22
  "@aws-sdk/s3-request-presigner": "^3.598.0",
23
23
  "@crowdin/crowdin-apps-functions": "0.8.1",
24
- "@crowdin/logs-formatter": "^2.1.5",
24
+ "@crowdin/logs-formatter": "^2.1.6",
25
25
  "@godaddy/terminus": "^4.12.1",
26
26
  "amqplib": "^0.10.4",
27
27
  "crypto-js": "^4.2.0",
28
- "express": "4.19.2",
28
+ "express": "^4.21.0",
29
29
  "express-handlebars": "^5.3.5",
30
30
  "lodash.get": "^4.4.2",
31
31
  "lodash.uniqby": "^4.7.0",
@@ -58,8 +58,8 @@
58
58
  "@types/express": "4.17.21",
59
59
  "@types/express-handlebars": "^5.3.1",
60
60
  "@types/jest": "^29.5.12",
61
- "@types/lodash.uniqby": "^4.7.9",
62
61
  "@types/lodash.get": "^4.4.9",
62
+ "@types/lodash.uniqby": "^4.7.9",
63
63
  "@types/node": "^16.18.101",
64
64
  "@types/node-cron": "^3.0.11",
65
65
  "@types/pg": "^8.11.6",
@@ -67,6 +67,7 @@
67
67
  "@types/uuid": "^9.0.7",
68
68
  "@typescript-eslint/eslint-plugin": "^2.3.1",
69
69
  "@typescript-eslint/parser": "^2.3.1",
70
+ "auto-changelog": "^2.5.0",
70
71
  "eslint": "^6.4.0",
71
72
  "eslint-config-prettier": "^6.3.0",
72
73
  "eslint-plugin-prettier": "^3.1.1",