@tradejs/app 1.0.6 → 1.0.9
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/bin/tradejs-app.mjs +7 -0
- package/package.json +12 -12
- package/src/app/api/ai/route.ts +63 -20
- package/src/app/api/backtest/files/route.ts +12 -1
- package/src/app/api/backtest/test/[strategy]/[name]/route.ts +18 -1
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +350 -28
- package/src/app/api/signal/[symbol]/[signalId]/route.ts +9 -6
- package/src/app/api/user/settings/route.ts +51 -23
- package/src/app/components/Dashboard/AiDrawer/index.tsx +38 -51
- package/src/app/components/Shared/Filters/Backtest/index.tsx +12 -5
- package/src/app/components/Shared/Filters/Root/index.tsx +12 -1
- package/src/app/components/Shared/Filters/Symbol/index.tsx +14 -19
- package/src/app/components/Shared/Filters/context.ts +2 -0
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +250 -111
- package/src/app/components/UI/ColorMode/index.tsx +62 -15
- package/src/app/components/UI/Select/index.tsx +3 -0
- package/src/app/components/UI/SelectWithSearch/index.tsx +3 -0
- package/src/app/layout.tsx +10 -8
- package/src/app/lib/klineWindow.ts +17 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +10 -2
- package/src/app/store/ai.ts +174 -0
- package/src/app/store/data.ts +34 -8
- package/src/app/store/index.ts +1 -0
- package/src/app/store/tests.ts +96 -9
- package/src/app/store/tickers.ts +113 -17
- package/src/proxy.ts +23 -50
|
@@ -17,6 +17,21 @@ import {
|
|
|
17
17
|
Text,
|
|
18
18
|
} from '@chakra-ui/react';
|
|
19
19
|
import { FiEdit2, FiSettings } from 'react-icons/fi';
|
|
20
|
+
import {
|
|
21
|
+
AI_RESPONSE_LANGUAGE_OPTIONS,
|
|
22
|
+
normalizeAiResponseLanguage,
|
|
23
|
+
} from '@tradejs/infra/aiLanguages';
|
|
24
|
+
import {
|
|
25
|
+
AI_CUSTOM_ENDPOINT_VALUE,
|
|
26
|
+
AI_ENDPOINT_OPTIONS,
|
|
27
|
+
normalizeAiEndpoint,
|
|
28
|
+
} from '@tradejs/infra/aiEndpoints';
|
|
29
|
+
import {
|
|
30
|
+
AI_CUSTOM_MODEL_VALUE,
|
|
31
|
+
getAiModelOptionsForEndpoint,
|
|
32
|
+
hasPresetAiModelsForEndpoint,
|
|
33
|
+
normalizeAiModel,
|
|
34
|
+
} from '@tradejs/infra/aiModels';
|
|
20
35
|
import { toaster } from '@UI';
|
|
21
36
|
|
|
22
37
|
type SettingsResponse = {
|
|
@@ -26,13 +41,14 @@ type SettingsResponse = {
|
|
|
26
41
|
apiKey: string;
|
|
27
42
|
apiSecret: string;
|
|
28
43
|
};
|
|
29
|
-
token: string;
|
|
30
44
|
coinalyze: {
|
|
31
45
|
apiKey: string;
|
|
32
46
|
};
|
|
33
|
-
|
|
47
|
+
ai: {
|
|
34
48
|
apiKey: string;
|
|
35
49
|
apiEndpoint: string;
|
|
50
|
+
model: string;
|
|
51
|
+
responseLanguage: string;
|
|
36
52
|
};
|
|
37
53
|
telegram: {
|
|
38
54
|
botToken: string;
|
|
@@ -49,10 +65,11 @@ type SettingsViewState = {
|
|
|
49
65
|
userName: string;
|
|
50
66
|
bybitApiKey: string;
|
|
51
67
|
bybitApiSecret: string;
|
|
52
|
-
token: string;
|
|
53
68
|
coinalyzeApiKey: string;
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
aiApiKey: string;
|
|
70
|
+
aiApiEndpoint: string;
|
|
71
|
+
aiModel: string;
|
|
72
|
+
aiResponseLanguage: string;
|
|
56
73
|
tgBotToken: string;
|
|
57
74
|
tgChatId: string;
|
|
58
75
|
};
|
|
@@ -64,20 +81,15 @@ type PasswordState = {
|
|
|
64
81
|
confirmPassword: string;
|
|
65
82
|
};
|
|
66
83
|
|
|
67
|
-
type SectionName =
|
|
68
|
-
| 'bybit'
|
|
69
|
-
| 'password'
|
|
70
|
-
| 'token'
|
|
71
|
-
| 'coinalyze'
|
|
72
|
-
| 'openai'
|
|
73
|
-
| 'telegram';
|
|
84
|
+
type SectionName = 'bybit' | 'password' | 'coinalyze' | 'ai' | 'telegram';
|
|
74
85
|
type EditableField =
|
|
75
86
|
| 'bybitApiKey'
|
|
76
87
|
| 'bybitApiSecret'
|
|
77
|
-
| 'token'
|
|
78
88
|
| 'coinalyzeApiKey'
|
|
79
|
-
| '
|
|
80
|
-
| '
|
|
89
|
+
| 'aiApiKey'
|
|
90
|
+
| 'aiApiEndpoint'
|
|
91
|
+
| 'aiModel'
|
|
92
|
+
| 'aiResponseLanguage'
|
|
81
93
|
| 'tgBotToken'
|
|
82
94
|
| 'tgChatId';
|
|
83
95
|
|
|
@@ -85,10 +97,11 @@ const EMPTY_SETTINGS: SettingsViewState = {
|
|
|
85
97
|
userName: '',
|
|
86
98
|
bybitApiKey: '',
|
|
87
99
|
bybitApiSecret: '',
|
|
88
|
-
token: '',
|
|
89
100
|
coinalyzeApiKey: '',
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
aiApiKey: '',
|
|
102
|
+
aiApiEndpoint: '',
|
|
103
|
+
aiModel: '',
|
|
104
|
+
aiResponseLanguage: '',
|
|
92
105
|
tgBotToken: '',
|
|
93
106
|
tgChatId: '',
|
|
94
107
|
};
|
|
@@ -96,10 +109,11 @@ const EMPTY_SETTINGS: SettingsViewState = {
|
|
|
96
109
|
const EMPTY_DRAFTS: SettingsDraftState = {
|
|
97
110
|
bybitApiKey: '',
|
|
98
111
|
bybitApiSecret: '',
|
|
99
|
-
token: '',
|
|
100
112
|
coinalyzeApiKey: '',
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
aiApiKey: '',
|
|
114
|
+
aiApiEndpoint: '',
|
|
115
|
+
aiModel: '',
|
|
116
|
+
aiResponseLanguage: '',
|
|
103
117
|
tgBotToken: '',
|
|
104
118
|
tgChatId: '',
|
|
105
119
|
};
|
|
@@ -112,10 +126,11 @@ const EMPTY_PASSWORDS: PasswordState = {
|
|
|
112
126
|
const EMPTY_EDITING: Record<EditableField, boolean> = {
|
|
113
127
|
bybitApiKey: false,
|
|
114
128
|
bybitApiSecret: false,
|
|
115
|
-
token: false,
|
|
116
129
|
coinalyzeApiKey: false,
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
aiApiKey: false,
|
|
131
|
+
aiApiEndpoint: false,
|
|
132
|
+
aiModel: false,
|
|
133
|
+
aiResponseLanguage: false,
|
|
119
134
|
tgBotToken: false,
|
|
120
135
|
tgChatId: false,
|
|
121
136
|
};
|
|
@@ -125,18 +140,16 @@ const SECTION_FIELDS: Record<
|
|
|
125
140
|
EditableField[]
|
|
126
141
|
> = {
|
|
127
142
|
bybit: ['bybitApiKey', 'bybitApiSecret'],
|
|
128
|
-
token: ['token'],
|
|
129
143
|
coinalyze: ['coinalyzeApiKey'],
|
|
130
|
-
|
|
144
|
+
ai: ['aiApiKey', 'aiApiEndpoint', 'aiModel', 'aiResponseLanguage'],
|
|
131
145
|
telegram: ['tgBotToken', 'tgChatId'],
|
|
132
146
|
};
|
|
133
147
|
|
|
134
148
|
const MASKED_FIELDS = new Set<EditableField>([
|
|
135
149
|
'bybitApiKey',
|
|
136
150
|
'bybitApiSecret',
|
|
137
|
-
'token',
|
|
138
151
|
'coinalyzeApiKey',
|
|
139
|
-
'
|
|
152
|
+
'aiApiKey',
|
|
140
153
|
'tgBotToken',
|
|
141
154
|
]);
|
|
142
155
|
|
|
@@ -144,17 +157,31 @@ const toViewState = (payload: SettingsResponse): SettingsViewState => ({
|
|
|
144
157
|
userName: payload.userName,
|
|
145
158
|
bybitApiKey: payload.settings.bybit.apiKey || '',
|
|
146
159
|
bybitApiSecret: payload.settings.bybit.apiSecret || '',
|
|
147
|
-
token: payload.settings.token || '',
|
|
148
160
|
coinalyzeApiKey: payload.settings.coinalyze.apiKey || '',
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
aiApiKey: payload.settings.ai.apiKey || '',
|
|
162
|
+
aiApiEndpoint:
|
|
163
|
+
normalizeAiEndpoint(payload.settings.ai.apiEndpoint) ||
|
|
164
|
+
AI_ENDPOINT_OPTIONS[0].value,
|
|
165
|
+
aiModel:
|
|
166
|
+
normalizeAiModel(
|
|
167
|
+
payload.settings.ai.model,
|
|
168
|
+
normalizeAiEndpoint(payload.settings.ai.apiEndpoint) ||
|
|
169
|
+
AI_ENDPOINT_OPTIONS[0].value,
|
|
170
|
+
) ||
|
|
171
|
+
payload.settings.ai.model ||
|
|
172
|
+
'',
|
|
173
|
+
aiResponseLanguage: normalizeAiResponseLanguage(
|
|
174
|
+
payload.settings.ai.responseLanguage,
|
|
175
|
+
),
|
|
151
176
|
tgBotToken: payload.settings.telegram.botToken || '',
|
|
152
177
|
tgChatId: payload.settings.telegram.chatId || '',
|
|
153
178
|
});
|
|
154
179
|
|
|
155
180
|
const toDraftState = (view: SettingsViewState): SettingsDraftState => ({
|
|
156
181
|
...EMPTY_DRAFTS,
|
|
157
|
-
|
|
182
|
+
aiApiEndpoint: view.aiApiEndpoint,
|
|
183
|
+
aiModel: view.aiModel,
|
|
184
|
+
aiResponseLanguage: view.aiResponseLanguage,
|
|
158
185
|
tgChatId: view.tgChatId,
|
|
159
186
|
});
|
|
160
187
|
|
|
@@ -171,6 +198,39 @@ const getErrorMessage = (
|
|
|
171
198
|
? payload.error
|
|
172
199
|
: fallback;
|
|
173
200
|
|
|
201
|
+
const getDraftAiEndpoint = (drafts: SettingsDraftState) =>
|
|
202
|
+
normalizeAiEndpoint(drafts.aiApiEndpoint) || drafts.aiApiEndpoint.trim();
|
|
203
|
+
|
|
204
|
+
const getSelectedAiModelOption = (aiModel: string, aiEndpoint: string) => {
|
|
205
|
+
const trimmedModel = aiModel.trim();
|
|
206
|
+
|
|
207
|
+
return getAiModelOptionsForEndpoint(aiEndpoint).some(
|
|
208
|
+
(option) => option.value === trimmedModel,
|
|
209
|
+
)
|
|
210
|
+
? trimmedModel
|
|
211
|
+
: AI_CUSTOM_MODEL_VALUE;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const DRAWER_SELECT_STYLE = {
|
|
215
|
+
width: '100%',
|
|
216
|
+
height: '40px',
|
|
217
|
+
paddingLeft: '12px',
|
|
218
|
+
paddingRight: '48px',
|
|
219
|
+
borderWidth: '1px',
|
|
220
|
+
borderColor: 'rgba(255, 255, 255, 0.16)',
|
|
221
|
+
borderRadius: '0.375rem',
|
|
222
|
+
background: 'rgba(0, 0, 0, 0.32)',
|
|
223
|
+
appearance: 'none',
|
|
224
|
+
WebkitAppearance: 'none',
|
|
225
|
+
MozAppearance: 'none',
|
|
226
|
+
backgroundImage:
|
|
227
|
+
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='9' viewBox='0 0 14 9' fill='none'%3E%3Cpath d='M1 1.5L7 7.5L13 1.5' stroke='%23E5E7EB' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")",
|
|
228
|
+
backgroundRepeat: 'no-repeat',
|
|
229
|
+
backgroundPosition: 'right 16px center',
|
|
230
|
+
backgroundSize: '14px 9px',
|
|
231
|
+
color: 'rgb(229, 231, 235)',
|
|
232
|
+
} as const;
|
|
233
|
+
|
|
174
234
|
export const AccountSettingsDrawer = () => {
|
|
175
235
|
const [open, setOpen] = useState(false);
|
|
176
236
|
const [loading, setLoading] = useState(false);
|
|
@@ -246,10 +306,13 @@ export const AccountSettingsDrawer = () => {
|
|
|
246
306
|
return Boolean(passwords.password || passwords.confirmPassword);
|
|
247
307
|
}
|
|
248
308
|
|
|
249
|
-
if (section === '
|
|
309
|
+
if (section === 'ai') {
|
|
310
|
+
const draftAiEndpoint = getDraftAiEndpoint(drafts);
|
|
250
311
|
return (
|
|
251
|
-
Boolean(drafts.
|
|
252
|
-
|
|
312
|
+
Boolean(drafts.aiApiKey.trim()) ||
|
|
313
|
+
draftAiEndpoint !== settings.aiApiEndpoint ||
|
|
314
|
+
drafts.aiModel.trim() !== settings.aiModel ||
|
|
315
|
+
drafts.aiResponseLanguage !== settings.aiResponseLanguage
|
|
253
316
|
);
|
|
254
317
|
}
|
|
255
318
|
|
|
@@ -360,46 +423,41 @@ export const AccountSettingsDrawer = () => {
|
|
|
360
423
|
apiSecret: getSecretUpdateValue('bybitApiSecret'),
|
|
361
424
|
},
|
|
362
425
|
}
|
|
363
|
-
: section === '
|
|
426
|
+
: section === 'coinalyze'
|
|
364
427
|
? {
|
|
365
428
|
section,
|
|
366
429
|
data: {
|
|
367
|
-
|
|
430
|
+
apiKey: getSecretUpdateValue('coinalyzeApiKey'),
|
|
368
431
|
},
|
|
369
432
|
}
|
|
370
|
-
: section === '
|
|
433
|
+
: section === 'ai'
|
|
371
434
|
? {
|
|
372
435
|
section,
|
|
373
436
|
data: {
|
|
374
|
-
apiKey: getSecretUpdateValue('
|
|
437
|
+
apiKey: getSecretUpdateValue('aiApiKey'),
|
|
438
|
+
apiEndpoint: drafts.aiApiEndpoint,
|
|
439
|
+
model: drafts.aiModel.trim(),
|
|
440
|
+
responseLanguage: drafts.aiResponseLanguage,
|
|
375
441
|
},
|
|
376
442
|
}
|
|
377
|
-
: section === '
|
|
443
|
+
: section === 'telegram'
|
|
378
444
|
? {
|
|
379
445
|
section,
|
|
380
446
|
data: {
|
|
381
|
-
|
|
382
|
-
|
|
447
|
+
botToken: getSecretUpdateValue('tgBotToken'),
|
|
448
|
+
chatId:
|
|
449
|
+
drafts.tgChatId !== settings.tgChatId
|
|
450
|
+
? drafts.tgChatId.trim()
|
|
451
|
+
: undefined,
|
|
383
452
|
},
|
|
384
453
|
}
|
|
385
|
-
:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
? drafts.tgChatId.trim()
|
|
393
|
-
: undefined,
|
|
394
|
-
},
|
|
395
|
-
}
|
|
396
|
-
: {
|
|
397
|
-
section,
|
|
398
|
-
data: {
|
|
399
|
-
password: passwords.password,
|
|
400
|
-
confirmPassword: passwords.confirmPassword,
|
|
401
|
-
},
|
|
402
|
-
};
|
|
454
|
+
: {
|
|
455
|
+
section,
|
|
456
|
+
data: {
|
|
457
|
+
password: passwords.password,
|
|
458
|
+
confirmPassword: passwords.confirmPassword,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
403
461
|
|
|
404
462
|
const response = await fetch('/api/user/settings', {
|
|
405
463
|
method: 'PATCH',
|
|
@@ -441,6 +499,19 @@ export const AccountSettingsDrawer = () => {
|
|
|
441
499
|
}
|
|
442
500
|
};
|
|
443
501
|
|
|
502
|
+
const selectedAiEndpointOption = AI_ENDPOINT_OPTIONS.some(
|
|
503
|
+
(option) => option.value === drafts.aiApiEndpoint,
|
|
504
|
+
)
|
|
505
|
+
? drafts.aiApiEndpoint
|
|
506
|
+
: AI_CUSTOM_ENDPOINT_VALUE;
|
|
507
|
+
const effectiveAiEndpoint = getDraftAiEndpoint(drafts);
|
|
508
|
+
const hasPresetAiModels = hasPresetAiModelsForEndpoint(effectiveAiEndpoint);
|
|
509
|
+
const aiModelOptions = getAiModelOptionsForEndpoint(effectiveAiEndpoint);
|
|
510
|
+
const selectedAiModelOption = getSelectedAiModelOption(
|
|
511
|
+
drafts.aiModel,
|
|
512
|
+
effectiveAiEndpoint,
|
|
513
|
+
);
|
|
514
|
+
|
|
444
515
|
const renderEditableField = ({
|
|
445
516
|
label,
|
|
446
517
|
field,
|
|
@@ -553,14 +624,17 @@ export const AccountSettingsDrawer = () => {
|
|
|
553
624
|
</Flex>
|
|
554
625
|
) : (
|
|
555
626
|
<Stack gap={5}>
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
627
|
+
<Text fontSize="sm" color="gray.400">
|
|
628
|
+
Signed in as{' '}
|
|
629
|
+
<Text
|
|
630
|
+
as="span"
|
|
631
|
+
fontSize="sm"
|
|
632
|
+
fontWeight="600"
|
|
633
|
+
color="gray.100"
|
|
634
|
+
>
|
|
561
635
|
{settings.userName || 'Unknown user'}
|
|
562
636
|
</Text>
|
|
563
|
-
</
|
|
637
|
+
</Text>
|
|
564
638
|
|
|
565
639
|
<Box
|
|
566
640
|
borderWidth="1px"
|
|
@@ -608,27 +682,124 @@ export const AccountSettingsDrawer = () => {
|
|
|
608
682
|
>
|
|
609
683
|
<Stack gap={4}>
|
|
610
684
|
<Box>
|
|
611
|
-
<Text fontWeight="600">
|
|
685
|
+
<Text fontWeight="600">AI / LLM</Text>
|
|
612
686
|
<Text fontSize="sm" color="gray.400">
|
|
613
|
-
Stored in the user profile and used for AI analysis
|
|
687
|
+
Stored in the user profile and used for AI analysis
|
|
688
|
+
and user-facing AI replies.
|
|
614
689
|
</Text>
|
|
615
690
|
</Box>
|
|
691
|
+
<Field.Root>
|
|
692
|
+
<Field.Label>AI_API_ENDPOINT</Field.Label>
|
|
693
|
+
<select
|
|
694
|
+
value={selectedAiEndpointOption}
|
|
695
|
+
onChange={(event) => {
|
|
696
|
+
const nextValue = event.target.value;
|
|
697
|
+
setDrafts((current) => ({
|
|
698
|
+
...current,
|
|
699
|
+
aiApiEndpoint:
|
|
700
|
+
nextValue === AI_CUSTOM_ENDPOINT_VALUE
|
|
701
|
+
? ''
|
|
702
|
+
: nextValue,
|
|
703
|
+
aiModel:
|
|
704
|
+
nextValue === AI_CUSTOM_ENDPOINT_VALUE
|
|
705
|
+
? ''
|
|
706
|
+
: normalizeAiModel('', nextValue),
|
|
707
|
+
}));
|
|
708
|
+
}}
|
|
709
|
+
style={DRAWER_SELECT_STYLE}
|
|
710
|
+
>
|
|
711
|
+
{AI_ENDPOINT_OPTIONS.map((option) => (
|
|
712
|
+
<option key={option.value} value={option.value}>
|
|
713
|
+
{option.label}
|
|
714
|
+
</option>
|
|
715
|
+
))}
|
|
716
|
+
</select>
|
|
717
|
+
<Text mt="2" fontSize="sm" color="gray.400">
|
|
718
|
+
{effectiveAiEndpoint || 'Endpoint is not set yet.'}
|
|
719
|
+
</Text>
|
|
720
|
+
</Field.Root>
|
|
721
|
+
{selectedAiEndpointOption === AI_CUSTOM_ENDPOINT_VALUE ? (
|
|
722
|
+
<Field.Root>
|
|
723
|
+
<Field.Label>Custom AI API endpoint URL</Field.Label>
|
|
724
|
+
<Input
|
|
725
|
+
value={drafts.aiApiEndpoint}
|
|
726
|
+
placeholder="https://your-openai-compatible-endpoint/v1"
|
|
727
|
+
onChange={(event) =>
|
|
728
|
+
updateDraft('aiApiEndpoint', event.target.value)
|
|
729
|
+
}
|
|
730
|
+
/>
|
|
731
|
+
</Field.Root>
|
|
732
|
+
) : null}
|
|
616
733
|
{renderEditableField({
|
|
617
|
-
label: '
|
|
618
|
-
field: '
|
|
619
|
-
placeholder: 'Enter a new
|
|
620
|
-
})}
|
|
621
|
-
{renderEditableField({
|
|
622
|
-
label: 'OPENAI_API_KEY',
|
|
623
|
-
field: 'openAIApiKey',
|
|
624
|
-
placeholder: 'Enter a new OpenAI API key',
|
|
734
|
+
label: 'AI_API_KEY',
|
|
735
|
+
field: 'aiApiKey',
|
|
736
|
+
placeholder: 'Enter a new AI API key',
|
|
625
737
|
})}
|
|
738
|
+
{hasPresetAiModels ? (
|
|
739
|
+
<Field.Root>
|
|
740
|
+
<Field.Label>AI_MODEL</Field.Label>
|
|
741
|
+
<select
|
|
742
|
+
value={selectedAiModelOption}
|
|
743
|
+
onChange={(event) => {
|
|
744
|
+
const nextValue = event.target.value;
|
|
745
|
+
if (nextValue === AI_CUSTOM_MODEL_VALUE) {
|
|
746
|
+
updateDraft('aiModel', '');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
updateDraft('aiModel', nextValue);
|
|
751
|
+
}}
|
|
752
|
+
style={DRAWER_SELECT_STYLE}
|
|
753
|
+
>
|
|
754
|
+
{aiModelOptions.map((option) => (
|
|
755
|
+
<option key={option.value} value={option.value}>
|
|
756
|
+
{option.label}
|
|
757
|
+
</option>
|
|
758
|
+
))}
|
|
759
|
+
<option value={AI_CUSTOM_MODEL_VALUE}>
|
|
760
|
+
Custom model
|
|
761
|
+
</option>
|
|
762
|
+
</select>
|
|
763
|
+
</Field.Root>
|
|
764
|
+
) : null}
|
|
765
|
+
{!hasPresetAiModels ||
|
|
766
|
+
selectedAiModelOption === AI_CUSTOM_MODEL_VALUE ? (
|
|
767
|
+
<Field.Root>
|
|
768
|
+
<Field.Label>Custom AI model name</Field.Label>
|
|
769
|
+
<Input
|
|
770
|
+
value={drafts.aiModel}
|
|
771
|
+
placeholder="Enter an OpenAI-compatible model name"
|
|
772
|
+
onChange={(event) =>
|
|
773
|
+
updateDraft('aiModel', event.target.value)
|
|
774
|
+
}
|
|
775
|
+
/>
|
|
776
|
+
</Field.Root>
|
|
777
|
+
) : null}
|
|
778
|
+
<Field.Root>
|
|
779
|
+
<Field.Label>AI_RESPONSE_LANGUAGE</Field.Label>
|
|
780
|
+
<select
|
|
781
|
+
value={drafts.aiResponseLanguage}
|
|
782
|
+
onChange={(event) =>
|
|
783
|
+
updateDraft(
|
|
784
|
+
'aiResponseLanguage',
|
|
785
|
+
event.target.value,
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
style={DRAWER_SELECT_STYLE}
|
|
789
|
+
>
|
|
790
|
+
{AI_RESPONSE_LANGUAGE_OPTIONS.map((option) => (
|
|
791
|
+
<option key={option.value} value={option.value}>
|
|
792
|
+
{option.label}
|
|
793
|
+
</option>
|
|
794
|
+
))}
|
|
795
|
+
</select>
|
|
796
|
+
</Field.Root>
|
|
626
797
|
<Flex justify="flex-end">
|
|
627
798
|
<Button
|
|
628
799
|
colorPalette="teal"
|
|
629
|
-
loading={savingSection === '
|
|
630
|
-
disabled={!isSectionDirty('
|
|
631
|
-
onClick={() => saveSection('
|
|
800
|
+
loading={savingSection === 'ai'}
|
|
801
|
+
disabled={!isSectionDirty('ai')}
|
|
802
|
+
onClick={() => saveSection('ai')}
|
|
632
803
|
>
|
|
633
804
|
Save
|
|
634
805
|
</Button>
|
|
@@ -766,38 +937,6 @@ export const AccountSettingsDrawer = () => {
|
|
|
766
937
|
</Flex>
|
|
767
938
|
</Stack>
|
|
768
939
|
</Box>
|
|
769
|
-
|
|
770
|
-
<Box
|
|
771
|
-
borderWidth="1px"
|
|
772
|
-
borderColor="gray.700"
|
|
773
|
-
borderRadius="lg"
|
|
774
|
-
p={4}
|
|
775
|
-
bg="gray.900"
|
|
776
|
-
>
|
|
777
|
-
<Stack gap={4}>
|
|
778
|
-
<Box>
|
|
779
|
-
<Text fontWeight="600">Passwordless auth token</Text>
|
|
780
|
-
<Text fontSize="sm" color="gray.400">
|
|
781
|
-
Token accepted in dashboard links for instant auth.
|
|
782
|
-
</Text>
|
|
783
|
-
</Box>
|
|
784
|
-
{renderEditableField({
|
|
785
|
-
label: 'TOKEN',
|
|
786
|
-
field: 'token',
|
|
787
|
-
placeholder: 'Enter a new passwordless token',
|
|
788
|
-
})}
|
|
789
|
-
<Flex justify="flex-end">
|
|
790
|
-
<Button
|
|
791
|
-
colorPalette="teal"
|
|
792
|
-
loading={savingSection === 'token'}
|
|
793
|
-
disabled={!isSectionDirty('token')}
|
|
794
|
-
onClick={() => saveSection('token')}
|
|
795
|
-
>
|
|
796
|
-
Save
|
|
797
|
-
</Button>
|
|
798
|
-
</Flex>
|
|
799
|
-
</Stack>
|
|
800
|
-
</Box>
|
|
801
940
|
</Stack>
|
|
802
941
|
)}
|
|
803
942
|
</Drawer.Body>
|
|
@@ -8,18 +8,9 @@ import {
|
|
|
8
8
|
type IconButtonProps,
|
|
9
9
|
type SpanProps,
|
|
10
10
|
} from '@chakra-ui/react';
|
|
11
|
-
import { ThemeProvider, useTheme, type ThemeProviderProps } from 'next-themes';
|
|
12
11
|
import * as React from 'react';
|
|
13
12
|
import { LuMoon, LuSun } from 'react-icons/lu';
|
|
14
13
|
|
|
15
|
-
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
|
16
|
-
|
|
17
|
-
export function ColorModeProvider(props: ColorModeProviderProps) {
|
|
18
|
-
return (
|
|
19
|
-
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
14
|
export type ColorMode = 'light' | 'dark';
|
|
24
15
|
|
|
25
16
|
export interface UseColorModeReturn {
|
|
@@ -28,16 +19,72 @@ export interface UseColorModeReturn {
|
|
|
28
19
|
toggleColorMode: () => void;
|
|
29
20
|
}
|
|
30
21
|
|
|
31
|
-
export
|
|
32
|
-
|
|
22
|
+
export interface ColorModeProviderProps {
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
forcedTheme?: ColorMode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ColorModeContext = React.createContext<UseColorModeReturn | null>(null);
|
|
28
|
+
|
|
29
|
+
export function ColorModeProvider({
|
|
30
|
+
children,
|
|
31
|
+
forcedTheme,
|
|
32
|
+
}: ColorModeProviderProps) {
|
|
33
|
+
const [colorMode, setColorModeState] = React.useState<ColorMode>(
|
|
34
|
+
forcedTheme ?? 'dark',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
if (!forcedTheme) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setColorModeState(forcedTheme);
|
|
43
|
+
}, [forcedTheme]);
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const root = document.documentElement;
|
|
47
|
+
const nextColorMode = forcedTheme ?? colorMode;
|
|
48
|
+
const previousColorMode = nextColorMode === 'dark' ? 'light' : 'dark';
|
|
49
|
+
|
|
50
|
+
root.classList.remove(previousColorMode);
|
|
51
|
+
root.classList.add(nextColorMode);
|
|
52
|
+
root.style.colorScheme = nextColorMode;
|
|
53
|
+
}, [colorMode, forcedTheme]);
|
|
54
|
+
|
|
55
|
+
const setColorMode = (nextColorMode: ColorMode) => {
|
|
56
|
+
if (forcedTheme) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setColorModeState(nextColorMode);
|
|
61
|
+
};
|
|
62
|
+
|
|
33
63
|
const toggleColorMode = () => {
|
|
34
|
-
|
|
64
|
+
setColorMode(colorMode === 'dark' ? 'light' : 'dark');
|
|
35
65
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
|
|
67
|
+
const value: UseColorModeReturn = {
|
|
68
|
+
colorMode: forcedTheme ?? colorMode,
|
|
69
|
+
setColorMode,
|
|
39
70
|
toggleColorMode,
|
|
40
71
|
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ColorModeContext.Provider value={value}>
|
|
75
|
+
{children}
|
|
76
|
+
</ColorModeContext.Provider>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useColorMode(): UseColorModeReturn {
|
|
81
|
+
return (
|
|
82
|
+
React.useContext(ColorModeContext) ?? {
|
|
83
|
+
colorMode: 'dark',
|
|
84
|
+
setColorMode: () => {},
|
|
85
|
+
toggleColorMode: () => {},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
41
88
|
}
|
|
42
89
|
|
|
43
90
|
export function useColorModeValue<T>(light: T, dark: T) {
|
|
@@ -19,6 +19,7 @@ interface SelectProps {
|
|
|
19
19
|
multiple?: boolean;
|
|
20
20
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
21
21
|
onChange?: (value: string[]) => void;
|
|
22
|
+
onOpenChange?: (open: boolean) => void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export const Select = ({
|
|
@@ -30,6 +31,7 @@ export const Select = ({
|
|
|
30
31
|
width = '320px',
|
|
31
32
|
size = 'sm',
|
|
32
33
|
onChange,
|
|
34
|
+
onOpenChange,
|
|
33
35
|
}: SelectProps) => {
|
|
34
36
|
const collection = useMemo(
|
|
35
37
|
() =>
|
|
@@ -44,6 +46,7 @@ export const Select = ({
|
|
|
44
46
|
collection={collection}
|
|
45
47
|
{...(value ? { value } : { defaultValue })}
|
|
46
48
|
onValueChange={(details) => onChange?.(details.value)}
|
|
49
|
+
onOpenChange={(details) => onOpenChange?.(details.open)}
|
|
47
50
|
size={size}
|
|
48
51
|
multiple={multiple}
|
|
49
52
|
width={width}
|
|
@@ -23,6 +23,7 @@ interface SelectWithSearchProps {
|
|
|
23
23
|
multiple?: boolean;
|
|
24
24
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
25
25
|
onChange?: (value: string[]) => void;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const SelectWithSearch = ({
|
|
@@ -35,6 +36,7 @@ export const SelectWithSearch = ({
|
|
|
35
36
|
width = '320px',
|
|
36
37
|
size = 'sm',
|
|
37
38
|
onChange,
|
|
39
|
+
onOpenChange,
|
|
38
40
|
}: SelectWithSearchProps) => {
|
|
39
41
|
const { contains } = useFilter({ sensitivity: 'base' });
|
|
40
42
|
const [inputValue, setInputValue] = useState(
|
|
@@ -61,6 +63,7 @@ export const SelectWithSearch = ({
|
|
|
61
63
|
setInputValue(e.inputValue);
|
|
62
64
|
}}
|
|
63
65
|
onOpenChange={(details) => {
|
|
66
|
+
onOpenChange?.(details.open);
|
|
64
67
|
if (details.open) {
|
|
65
68
|
filter('');
|
|
66
69
|
setInputValue('');
|