@tradejs/app 3.1.28 → 3.1.29-beta.259
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.29-beta.259",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -56,11 +56,11 @@
|
|
|
56
56
|
"@chakra-ui/charts": "3.36.1",
|
|
57
57
|
"@chakra-ui/react": "3.36.1",
|
|
58
58
|
"@emotion/react": "^11.14.0",
|
|
59
|
-
"@tradejs/core": "^3.1.
|
|
60
|
-
"@tradejs/indicators": "^3.1.
|
|
61
|
-
"@tradejs/infra": "^3.1.
|
|
62
|
-
"@tradejs/node": "^3.1.
|
|
63
|
-
"@tradejs/types": "^3.1.
|
|
59
|
+
"@tradejs/core": "^3.1.29-beta.259",
|
|
60
|
+
"@tradejs/indicators": "^3.1.29-beta.259",
|
|
61
|
+
"@tradejs/infra": "^3.1.29-beta.259",
|
|
62
|
+
"@tradejs/node": "^3.1.29-beta.259",
|
|
63
|
+
"@tradejs/types": "^3.1.29-beta.259",
|
|
64
64
|
"@types/bcryptjs": "2.4.6",
|
|
65
65
|
"@types/lodash": "4.17.24",
|
|
66
66
|
"@types/node": "24.13.3",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JEV_PROVIDERS, normalizeJevEndpoint } from '@tradejs/core/jev';
|
|
1
2
|
import bcrypt from 'bcryptjs';
|
|
2
3
|
import { NextResponse } from 'next/server';
|
|
3
4
|
import {
|
|
@@ -18,6 +19,10 @@ import { getCurrentUserName } from '#app/lib/currentUser';
|
|
|
18
19
|
export const dynamic = 'force-dynamic';
|
|
19
20
|
|
|
20
21
|
type UpdateBody =
|
|
22
|
+
| {
|
|
23
|
+
section: 'jev';
|
|
24
|
+
data?: { apiKey?: string; apiEndpoint?: string; model?: string };
|
|
25
|
+
}
|
|
21
26
|
| {
|
|
22
27
|
section: 'coinalyze';
|
|
23
28
|
data?: {
|
|
@@ -58,6 +63,7 @@ const UPDATE_SECTIONS = new Set([
|
|
|
58
63
|
'coinalyze',
|
|
59
64
|
'coinmarketcap',
|
|
60
65
|
'ai',
|
|
66
|
+
'jev',
|
|
61
67
|
'telegram',
|
|
62
68
|
'password',
|
|
63
69
|
]);
|
|
@@ -106,6 +112,11 @@ const toResponse = (rawSettings: UserSettings) => {
|
|
|
106
112
|
coinmarketcap: {
|
|
107
113
|
apiKey: maskSecret(settings.COINMARKETCAP_API_KEY),
|
|
108
114
|
},
|
|
115
|
+
jev: {
|
|
116
|
+
apiKey: maskSecret(settings.JEV_API_KEY || ''),
|
|
117
|
+
apiEndpoint: settings.JEV_API_ENDPOINT || JEV_PROVIDERS[0].endpoint,
|
|
118
|
+
model: settings.JEV_MODEL || JEV_PROVIDERS[0].model,
|
|
119
|
+
},
|
|
109
120
|
ai: {
|
|
110
121
|
apiKey: maskSecret(settings.AI_API_KEY),
|
|
111
122
|
apiEndpoint: settings.AI_API_ENDPOINT,
|
|
@@ -199,6 +210,31 @@ export const PATCH = async (request: Request) => {
|
|
|
199
210
|
}
|
|
200
211
|
}
|
|
201
212
|
|
|
213
|
+
if (body.section === 'jev') {
|
|
214
|
+
const patch: Partial<UserRecord> = {};
|
|
215
|
+
if (body.data && 'apiEndpoint' in body.data) {
|
|
216
|
+
const endpoint = normalizeJevEndpoint(body.data.apiEndpoint);
|
|
217
|
+
if (!endpoint)
|
|
218
|
+
return NextResponse.json(
|
|
219
|
+
{ error: 'Invalid Jev decision endpoint URL' },
|
|
220
|
+
{ status: 400 },
|
|
221
|
+
);
|
|
222
|
+
patch.JEV_API_ENDPOINT = endpoint;
|
|
223
|
+
}
|
|
224
|
+
if (body.data && 'model' in body.data) {
|
|
225
|
+
const model = cleanText(body.data.model);
|
|
226
|
+
if (!model || model.length > 200)
|
|
227
|
+
return NextResponse.json(
|
|
228
|
+
{ error: 'Jev model is required' },
|
|
229
|
+
{ status: 400 },
|
|
230
|
+
);
|
|
231
|
+
patch.JEV_MODEL = model;
|
|
232
|
+
}
|
|
233
|
+
const apiKey = cleanOptionalText(body.data?.apiKey);
|
|
234
|
+
if (apiKey && !apiKey.startsWith('********')) patch.JEV_API_KEY = apiKey;
|
|
235
|
+
if (hasKeys(patch)) await updateUserRecord(userName, patch);
|
|
236
|
+
}
|
|
237
|
+
|
|
202
238
|
if (body.section === 'ai') {
|
|
203
239
|
const currentSettings = normalizeSettingsForResponse(
|
|
204
240
|
await getUserSettings(userName),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { JEV_PROVIDERS } from '@tradejs/core/jev';
|
|
3
4
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
4
5
|
import {
|
|
5
6
|
Box,
|
|
@@ -44,6 +45,7 @@ type SettingsResponse = {
|
|
|
44
45
|
coinmarketcap: {
|
|
45
46
|
apiKey: string;
|
|
46
47
|
};
|
|
48
|
+
jev?: { apiKey: string; apiEndpoint: string; model: string };
|
|
47
49
|
ai: {
|
|
48
50
|
apiKey: string;
|
|
49
51
|
apiEndpoint: string;
|
|
@@ -65,6 +67,9 @@ type SettingsViewState = {
|
|
|
65
67
|
userName: string;
|
|
66
68
|
coinalyzeApiKey: string;
|
|
67
69
|
coinmarketcapApiKey: string;
|
|
70
|
+
jevApiKey: string;
|
|
71
|
+
jevApiEndpoint: string;
|
|
72
|
+
jevModel: string;
|
|
68
73
|
aiApiKey: string;
|
|
69
74
|
aiApiEndpoint: string;
|
|
70
75
|
aiModel: string;
|
|
@@ -84,11 +89,15 @@ type SectionName =
|
|
|
84
89
|
| 'password'
|
|
85
90
|
| 'coinalyze'
|
|
86
91
|
| 'coinmarketcap'
|
|
92
|
+
| 'jev'
|
|
87
93
|
| 'ai'
|
|
88
94
|
| 'telegram';
|
|
89
95
|
type EditableField =
|
|
90
96
|
| 'coinalyzeApiKey'
|
|
91
97
|
| 'coinmarketcapApiKey'
|
|
98
|
+
| 'jevApiKey'
|
|
99
|
+
| 'jevApiEndpoint'
|
|
100
|
+
| 'jevModel'
|
|
92
101
|
| 'aiApiKey'
|
|
93
102
|
| 'aiApiEndpoint'
|
|
94
103
|
| 'aiModel'
|
|
@@ -100,6 +109,9 @@ const EMPTY_SETTINGS: SettingsViewState = {
|
|
|
100
109
|
userName: '',
|
|
101
110
|
coinalyzeApiKey: '',
|
|
102
111
|
coinmarketcapApiKey: '',
|
|
112
|
+
jevApiKey: '',
|
|
113
|
+
jevApiEndpoint: JEV_PROVIDERS[0].endpoint,
|
|
114
|
+
jevModel: JEV_PROVIDERS[0].model,
|
|
103
115
|
aiApiKey: '',
|
|
104
116
|
aiApiEndpoint: '',
|
|
105
117
|
aiModel: '',
|
|
@@ -111,6 +123,9 @@ const EMPTY_SETTINGS: SettingsViewState = {
|
|
|
111
123
|
const EMPTY_DRAFTS: SettingsDraftState = {
|
|
112
124
|
coinalyzeApiKey: '',
|
|
113
125
|
coinmarketcapApiKey: '',
|
|
126
|
+
jevApiKey: '',
|
|
127
|
+
jevApiEndpoint: JEV_PROVIDERS[0].endpoint,
|
|
128
|
+
jevModel: JEV_PROVIDERS[0].model,
|
|
114
129
|
aiApiKey: '',
|
|
115
130
|
aiApiEndpoint: '',
|
|
116
131
|
aiModel: '',
|
|
@@ -127,6 +142,9 @@ const EMPTY_PASSWORDS: PasswordState = {
|
|
|
127
142
|
const EMPTY_EDITING: Record<EditableField, boolean> = {
|
|
128
143
|
coinalyzeApiKey: false,
|
|
129
144
|
coinmarketcapApiKey: false,
|
|
145
|
+
jevApiKey: false,
|
|
146
|
+
jevApiEndpoint: false,
|
|
147
|
+
jevModel: false,
|
|
130
148
|
aiApiKey: false,
|
|
131
149
|
aiApiEndpoint: false,
|
|
132
150
|
aiModel: false,
|
|
@@ -139,6 +157,7 @@ const MASKED_FIELDS = new Set<EditableField>([
|
|
|
139
157
|
'coinalyzeApiKey',
|
|
140
158
|
'coinmarketcapApiKey',
|
|
141
159
|
'aiApiKey',
|
|
160
|
+
'jevApiKey',
|
|
142
161
|
'tgBotToken',
|
|
143
162
|
]);
|
|
144
163
|
|
|
@@ -146,6 +165,10 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
|
|
|
146
165
|
userName: payload.userName,
|
|
147
166
|
coinalyzeApiKey: payload.settings.coinalyze.apiKey || '',
|
|
148
167
|
coinmarketcapApiKey: payload.settings.coinmarketcap.apiKey || '',
|
|
168
|
+
jevApiKey: payload.settings.jev?.apiKey || '',
|
|
169
|
+
jevApiEndpoint:
|
|
170
|
+
payload.settings.jev?.apiEndpoint || JEV_PROVIDERS[0].endpoint,
|
|
171
|
+
jevModel: payload.settings.jev?.model || JEV_PROVIDERS[0].model,
|
|
149
172
|
aiApiKey: payload.settings.ai.apiKey || '',
|
|
150
173
|
aiApiEndpoint:
|
|
151
174
|
normalizeAiEndpoint(payload.settings.ai.apiEndpoint) ||
|
|
@@ -167,6 +190,8 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
|
|
|
167
190
|
|
|
168
191
|
const toDraftState = (view: SettingsViewState): SettingsDraftState => ({
|
|
169
192
|
...EMPTY_DRAFTS,
|
|
193
|
+
jevApiEndpoint: view.jevApiEndpoint,
|
|
194
|
+
jevModel: view.jevModel,
|
|
170
195
|
aiApiEndpoint: view.aiApiEndpoint,
|
|
171
196
|
aiModel: view.aiModel,
|
|
172
197
|
aiResponseLanguage: view.aiResponseLanguage,
|
|
@@ -304,6 +329,13 @@ export const AccountSettingsDrawer = () => {
|
|
|
304
329
|
);
|
|
305
330
|
}
|
|
306
331
|
|
|
332
|
+
if (section === 'jev')
|
|
333
|
+
return (
|
|
334
|
+
Boolean(drafts.jevApiKey.trim()) ||
|
|
335
|
+
drafts.jevApiEndpoint !== settings.jevApiEndpoint ||
|
|
336
|
+
drafts.jevModel !== settings.jevModel
|
|
337
|
+
);
|
|
338
|
+
|
|
307
339
|
if (section === 'coinalyze') {
|
|
308
340
|
return Boolean(drafts.coinalyzeApiKey.trim());
|
|
309
341
|
}
|
|
@@ -405,48 +437,57 @@ export const AccountSettingsDrawer = () => {
|
|
|
405
437
|
|
|
406
438
|
try {
|
|
407
439
|
const body =
|
|
408
|
-
section === '
|
|
440
|
+
section === 'jev'
|
|
409
441
|
? {
|
|
410
442
|
section,
|
|
411
443
|
data: {
|
|
412
|
-
apiKey: getSecretUpdateValue('
|
|
444
|
+
apiKey: getSecretUpdateValue('jevApiKey'),
|
|
445
|
+
apiEndpoint: drafts.jevApiEndpoint,
|
|
446
|
+
model: drafts.jevModel.trim(),
|
|
413
447
|
},
|
|
414
448
|
}
|
|
415
|
-
: section === '
|
|
449
|
+
: section === 'coinalyze'
|
|
416
450
|
? {
|
|
417
451
|
section,
|
|
418
452
|
data: {
|
|
419
|
-
apiKey: getSecretUpdateValue('
|
|
420
|
-
apiEndpoint: drafts.aiApiEndpoint,
|
|
421
|
-
model: drafts.aiModel.trim(),
|
|
422
|
-
responseLanguage: drafts.aiResponseLanguage,
|
|
453
|
+
apiKey: getSecretUpdateValue('coinalyzeApiKey'),
|
|
423
454
|
},
|
|
424
455
|
}
|
|
425
|
-
: section === '
|
|
456
|
+
: section === 'ai'
|
|
426
457
|
? {
|
|
427
458
|
section,
|
|
428
459
|
data: {
|
|
429
|
-
apiKey: getSecretUpdateValue('
|
|
460
|
+
apiKey: getSecretUpdateValue('aiApiKey'),
|
|
461
|
+
apiEndpoint: drafts.aiApiEndpoint,
|
|
462
|
+
model: drafts.aiModel.trim(),
|
|
463
|
+
responseLanguage: drafts.aiResponseLanguage,
|
|
430
464
|
},
|
|
431
465
|
}
|
|
432
|
-
: section === '
|
|
466
|
+
: section === 'coinmarketcap'
|
|
433
467
|
? {
|
|
434
468
|
section,
|
|
435
469
|
data: {
|
|
436
|
-
|
|
437
|
-
chatId:
|
|
438
|
-
drafts.tgChatId !== settings.tgChatId
|
|
439
|
-
? drafts.tgChatId.trim()
|
|
440
|
-
: undefined,
|
|
470
|
+
apiKey: getSecretUpdateValue('coinmarketcapApiKey'),
|
|
441
471
|
},
|
|
442
472
|
}
|
|
443
|
-
:
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
473
|
+
: section === 'telegram'
|
|
474
|
+
? {
|
|
475
|
+
section,
|
|
476
|
+
data: {
|
|
477
|
+
botToken: getSecretUpdateValue('tgBotToken'),
|
|
478
|
+
chatId:
|
|
479
|
+
drafts.tgChatId !== settings.tgChatId
|
|
480
|
+
? drafts.tgChatId.trim()
|
|
481
|
+
: undefined,
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
: {
|
|
485
|
+
section,
|
|
486
|
+
data: {
|
|
487
|
+
password: passwords.password,
|
|
488
|
+
confirmPassword: passwords.confirmPassword,
|
|
489
|
+
},
|
|
490
|
+
};
|
|
450
491
|
|
|
451
492
|
const response = await fetch('/api/user/settings', {
|
|
452
493
|
method: 'PATCH',
|
|
@@ -660,6 +701,96 @@ export const AccountSettingsDrawer = () => {
|
|
|
660
701
|
</Stack>
|
|
661
702
|
</Box>
|
|
662
703
|
|
|
704
|
+
<Box
|
|
705
|
+
borderWidth="1px"
|
|
706
|
+
borderColor="gray.700"
|
|
707
|
+
borderRadius="lg"
|
|
708
|
+
p={4}
|
|
709
|
+
bg="gray.900"
|
|
710
|
+
>
|
|
711
|
+
<Stack gap={4}>
|
|
712
|
+
<Box>
|
|
713
|
+
<Text fontWeight="600">Jev</Text>
|
|
714
|
+
<Text fontSize="sm" color="gray.400">
|
|
715
|
+
Evaluate signal context and geometry in backtests and
|
|
716
|
+
runtime.
|
|
717
|
+
</Text>
|
|
718
|
+
</Box>
|
|
719
|
+
<Field.Root>
|
|
720
|
+
<Field.Label>Jev provider</Field.Label>
|
|
721
|
+
<select
|
|
722
|
+
aria-label="Jev provider"
|
|
723
|
+
style={DRAWER_SELECT_STYLE}
|
|
724
|
+
value={
|
|
725
|
+
JEV_PROVIDERS.find(
|
|
726
|
+
(item) => item.endpoint === drafts.jevApiEndpoint,
|
|
727
|
+
)?.endpoint ?? 'custom'
|
|
728
|
+
}
|
|
729
|
+
onChange={(event) => {
|
|
730
|
+
const provider = JEV_PROVIDERS.find(
|
|
731
|
+
(item) => item.endpoint === event.target.value,
|
|
732
|
+
);
|
|
733
|
+
setDrafts((current) => ({
|
|
734
|
+
...current,
|
|
735
|
+
jevApiEndpoint: provider?.endpoint ?? '',
|
|
736
|
+
jevModel: provider?.model ?? '',
|
|
737
|
+
}));
|
|
738
|
+
}}
|
|
739
|
+
>
|
|
740
|
+
{JEV_PROVIDERS.map((provider) => (
|
|
741
|
+
<option
|
|
742
|
+
key={provider.endpoint}
|
|
743
|
+
value={provider.endpoint}
|
|
744
|
+
>
|
|
745
|
+
{provider.label}
|
|
746
|
+
</option>
|
|
747
|
+
))}
|
|
748
|
+
<option value="custom">Custom Decisions API</option>
|
|
749
|
+
</select>
|
|
750
|
+
</Field.Root>
|
|
751
|
+
<Field.Root>
|
|
752
|
+
<Field.Label>JEV_API_ENDPOINT</Field.Label>
|
|
753
|
+
<Input
|
|
754
|
+
aria-label="JEV_API_ENDPOINT"
|
|
755
|
+
value={drafts.jevApiEndpoint}
|
|
756
|
+
onChange={(event) =>
|
|
757
|
+
updateDraft('jevApiEndpoint', event.target.value)
|
|
758
|
+
}
|
|
759
|
+
placeholder="Full URL of the decision endpoint"
|
|
760
|
+
/>
|
|
761
|
+
<Text fontSize="sm" color="gray.400">
|
|
762
|
+
Use a TypeSafe-compatible decision endpoint accepting
|
|
763
|
+
state and questions.
|
|
764
|
+
</Text>
|
|
765
|
+
</Field.Root>
|
|
766
|
+
<Field.Root>
|
|
767
|
+
<Field.Label>JEV_MODEL</Field.Label>
|
|
768
|
+
<Input
|
|
769
|
+
aria-label="JEV_MODEL"
|
|
770
|
+
value={drafts.jevModel}
|
|
771
|
+
onChange={(event) =>
|
|
772
|
+
updateDraft('jevModel', event.target.value)
|
|
773
|
+
}
|
|
774
|
+
/>
|
|
775
|
+
</Field.Root>
|
|
776
|
+
{renderEditableField({
|
|
777
|
+
label: 'JEV_API_KEY',
|
|
778
|
+
field: 'jevApiKey',
|
|
779
|
+
placeholder: 'Enter a new Jev provider API key',
|
|
780
|
+
})}
|
|
781
|
+
<Flex justify="flex-end">
|
|
782
|
+
<Button
|
|
783
|
+
colorPalette="teal"
|
|
784
|
+
loading={savingSection === 'jev'}
|
|
785
|
+
disabled={!isSectionDirty('jev')}
|
|
786
|
+
onClick={() => saveSection('jev')}
|
|
787
|
+
>
|
|
788
|
+
Save
|
|
789
|
+
</Button>
|
|
790
|
+
</Flex>
|
|
791
|
+
</Stack>
|
|
792
|
+
</Box>
|
|
793
|
+
|
|
663
794
|
<Box
|
|
664
795
|
borderWidth="1px"
|
|
665
796
|
borderColor="gray.700"
|