Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/other-settings.tsx
6041 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// cSpell:ignore brandcolors codebar
7
8
import { FormattedMessage, useIntl } from "react-intl";
9
10
import { Panel, Switch } from "@cocalc/frontend/antd-bootstrap";
11
import { redux, Rendered, useTypedRedux } from "@cocalc/frontend/app-framework";
12
import { useLocalizationCtx } from "@cocalc/frontend/app/localize";
13
import {
14
Icon,
15
IconName,
16
LabeledRow,
17
Loading,
18
NumberInput,
19
Paragraph,
20
SelectorInput,
21
Text,
22
} from "@cocalc/frontend/components";
23
import AIAvatar from "@cocalc/frontend/components/ai-avatar";
24
import { IS_MOBILE, IS_TOUCH } from "@cocalc/frontend/feature";
25
import LLMSelector from "@cocalc/frontend/frame-editors/llm/llm-selector";
26
import { labels, LOCALIZATIONS } from "@cocalc/frontend/i18n";
27
import { getValidActivityBarOption } from "@cocalc/frontend/project/page/activity-bar";
28
import {
29
ACTIVITY_BAR_EXPLANATION,
30
ACTIVITY_BAR_KEY,
31
ACTIVITY_BAR_LABELS,
32
ACTIVITY_BAR_LABELS_DEFAULT,
33
ACTIVITY_BAR_OPTIONS,
34
ACTIVITY_BAR_TITLE,
35
ACTIVITY_BAR_TOGGLE_LABELS,
36
ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION,
37
} from "@cocalc/frontend/project/page/activity-bar-consts";
38
import { NewFilenameFamilies } from "@cocalc/frontend/project/utils";
39
import track from "@cocalc/frontend/user-tracking";
40
import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";
41
import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";
42
43
import Tours from "./tours";
44
import { useLanguageModelSetting } from "./useLanguageModelSetting";
45
import { UserDefinedLLMComponent } from "./user-defined-llm";
46
47
// Icon constants for account preferences sections
48
export const THEME_ICON_NAME: IconName = "highlighter";
49
export const OTHER_ICON_NAME: IconName = "gear";
50
51
// Import the account state type to get the proper other_settings type
52
import type { AccountState } from "./types";
53
54
interface Props {
55
other_settings: AccountState["other_settings"];
56
is_stripe_customer: boolean;
57
kucalc: string;
58
mode: "appearance" | "ai" | "other";
59
}
60
61
export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
62
const intl = useIntl();
63
const { locale } = useLocalizationCtx();
64
const isCoCalcCom = useTypedRedux("customize", "is_cocalc_com");
65
const user_defined_llm = useTypedRedux("customize", "user_defined_llm");
66
67
const [model, setModel] = useLanguageModelSetting();
68
69
function on_change(name: string, value: any): void {
70
redux.getActions("account").set_other_settings(name, value);
71
}
72
73
// private render_first_steps(): Rendered {
74
// if (props.kucalc !== KUCALC_COCALC_COM) return;
75
// return (
76
// <Switch
77
// checked={!!props.other_settings.get("first_steps")}
78
// onChange={(e) => on_change("first_steps", e.target.checked)}
79
// >
80
// Offer the First Steps guide
81
// </Switch>
82
// );
83
// }
84
85
function render_confirm(): Rendered {
86
if (!IS_MOBILE) {
87
return (
88
<Switch
89
checked={!!props.other_settings.get("confirm_close")}
90
onChange={(e) => on_change("confirm_close", e.target.checked)}
91
>
92
<FormattedMessage
93
id="account.other-settings.confirm_close"
94
defaultMessage={`<strong>Confirm Close:</strong> always ask for confirmation before
95
closing the browser window`}
96
/>
97
</Switch>
98
);
99
}
100
}
101
102
function render_standby_timeout(): Rendered {
103
if (IS_TOUCH) {
104
return;
105
}
106
return (
107
<LabeledRow
108
label={intl.formatMessage({
109
id: "account.other-settings.standby_timeout",
110
defaultMessage: "Standby timeout",
111
})}
112
>
113
<NumberInput
114
on_change={(n) => on_change("standby_timeout_m", n)}
115
min={1}
116
max={180}
117
unit="minutes"
118
number={props.other_settings.get("standby_timeout_m") ?? 30}
119
/>
120
</LabeledRow>
121
);
122
}
123
124
function render_mask_files(): Rendered {
125
return (
126
<Switch
127
checked={!!props.other_settings.get("mask_files")}
128
onChange={(e) => on_change("mask_files", e.target.checked)}
129
>
130
<FormattedMessage
131
id="account.other-settings.mask_files"
132
defaultMessage={`<strong>Dim generated files:</strong> gray out files produced by compilers (.aux, .log, .pyc, etc.) so the main files stand out.`}
133
/>
134
</Switch>
135
);
136
}
137
138
function render_default_file_sort(): Rendered {
139
return (
140
<LabeledRow
141
label={intl.formatMessage({
142
id: "account.other-settings.default_file_sort.label",
143
defaultMessage: "Default file sort",
144
})}
145
>
146
<SelectorInput
147
selected={props.other_settings.get("default_file_sort")}
148
options={{
149
time: intl.formatMessage({
150
id: "account.other-settings.default_file_sort.by_time",
151
defaultMessage: "Sort by time",
152
}),
153
name: intl.formatMessage({
154
id: "account.other-settings.default_file_sort.by_name",
155
defaultMessage: "Sort by name",
156
}),
157
}}
158
on_change={(value) => on_change("default_file_sort", value)}
159
/>
160
</LabeledRow>
161
);
162
}
163
164
function render_new_filenames(): Rendered {
165
const selected =
166
props.other_settings.get(NEW_FILENAMES) ?? DEFAULT_NEW_FILENAMES;
167
return (
168
<LabeledRow
169
label={intl.formatMessage({
170
id: "account.other-settings.filename_generator.label",
171
defaultMessage: "Filename generator",
172
})}
173
>
174
<div>
175
<SelectorInput
176
selected={selected}
177
options={NewFilenameFamilies}
178
on_change={(value) => on_change(NEW_FILENAMES, value)}
179
/>
180
<Paragraph
181
type="secondary"
182
ellipsis={{ expandable: true, symbol: "more" }}
183
>
184
{intl.formatMessage({
185
id: "account.other-settings.filename_generator.description",
186
defaultMessage: `Select how automatically generated filenames are generated.
187
In particular, to make them unique or to include the current time.`,
188
})}
189
</Paragraph>
190
</div>
191
</LabeledRow>
192
);
193
}
194
195
function render_page_size(): Rendered {
196
return (
197
<LabeledRow
198
label={intl.formatMessage({
199
id: "account.other-settings._page_size.label",
200
defaultMessage: "Number of files per page",
201
})}
202
>
203
<NumberInput
204
on_change={(n) => on_change("page_size", n)}
205
min={1}
206
max={10000}
207
number={props.other_settings.get("page_size") ?? 50}
208
/>
209
</LabeledRow>
210
);
211
}
212
213
function render_dim_file_extensions(): Rendered {
214
return (
215
<Switch
216
checked={!!props.other_settings.get("dim_file_extensions")}
217
onChange={(e) => on_change("dim_file_extensions", e.target.checked)}
218
>
219
<FormattedMessage
220
id="account.other-settings.dim_file_extensions"
221
defaultMessage={`<strong>Dim file extensions:</strong> gray out file extensions so their names stand out.`}
222
/>
223
</Switch>
224
);
225
}
226
227
function render_antd(): Rendered {
228
return (
229
<>
230
<Switch
231
checked={props.other_settings.get("antd_rounded", true)}
232
onChange={(e) => on_change("antd_rounded", e.target.checked)}
233
>
234
<FormattedMessage
235
id="account.other-settings.theme.antd.rounded"
236
defaultMessage={`<b>Rounded Design</b>: use rounded corners for buttons, etc.`}
237
/>
238
</Switch>
239
<Switch
240
checked={props.other_settings.get("antd_animate", true)}
241
onChange={(e) => on_change("antd_animate", e.target.checked)}
242
>
243
<FormattedMessage
244
id="account.other-settings.theme.antd.animations"
245
defaultMessage={`<b>Animations</b>: briefly animate some aspects, e.g. buttons`}
246
/>
247
</Switch>
248
<Switch
249
checked={props.other_settings.get("antd_brandcolors", false)}
250
onChange={(e) => on_change("antd_brandcolors", e.target.checked)}
251
>
252
<FormattedMessage
253
id="account.other-settings.theme.antd.color_scheme"
254
defaultMessage={`<b>Color Scheme</b>: use brand colors instead of default colors`}
255
/>
256
</Switch>
257
<Switch
258
checked={props.other_settings.get("antd_compact", false)}
259
onChange={(e) => on_change("antd_compact", e.target.checked)}
260
>
261
<FormattedMessage
262
id="account.other-settings.theme.antd.compact"
263
defaultMessage={`<b>Compact Design</b>: use a more compact design`}
264
/>
265
</Switch>
266
</>
267
);
268
}
269
270
function render_vertical_fixed_bar_options(): Rendered {
271
const selected = getValidActivityBarOption(
272
props.other_settings.get(ACTIVITY_BAR_KEY),
273
);
274
const options = Object.fromEntries(
275
Object.entries(ACTIVITY_BAR_OPTIONS).map(([k, v]) => [
276
k,
277
intl.formatMessage(v),
278
]),
279
);
280
return (
281
<LabeledRow label={intl.formatMessage(ACTIVITY_BAR_TITLE)}>
282
<div>
283
<SelectorInput
284
style={{ marginBottom: "10px" }}
285
selected={selected}
286
options={options}
287
on_change={(value) => {
288
on_change(ACTIVITY_BAR_KEY, value);
289
track("flyout", { aspect: "layout", how: "account", value });
290
}}
291
/>
292
<Paragraph
293
type="secondary"
294
ellipsis={{ expandable: true, symbol: "more" }}
295
>
296
{intl.formatMessage(ACTIVITY_BAR_EXPLANATION)}
297
</Paragraph>
298
<Switch
299
checked={
300
props.other_settings.get(ACTIVITY_BAR_LABELS) ??
301
ACTIVITY_BAR_LABELS_DEFAULT
302
}
303
onChange={(e) => {
304
on_change(ACTIVITY_BAR_LABELS, e.target.checked);
305
}}
306
>
307
<Paragraph
308
type="secondary"
309
style={{ marginBottom: 0 }}
310
ellipsis={{ expandable: true, symbol: "more" }}
311
>
312
<Text strong>
313
{intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS, {
314
show: false,
315
})}
316
</Text>
317
: {intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION)}
318
</Paragraph>
319
</Switch>
320
</div>
321
</LabeledRow>
322
);
323
}
324
325
function render_disable_all_llm(): Rendered {
326
return (
327
<Switch
328
checked={!!props.other_settings.get("openai_disabled")}
329
onChange={(e) => {
330
on_change("openai_disabled", e.target.checked);
331
redux.getStore("projects").clearOpenAICache();
332
}}
333
>
334
<FormattedMessage
335
id="account.other-settings.llm.disable_all"
336
defaultMessage={`<strong>Disable all AI integrations</strong>,
337
e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}
338
/>
339
</Switch>
340
);
341
}
342
343
function render_language_model(): Rendered {
344
return (
345
<LabeledRow
346
label={intl.formatMessage({
347
id: "account.other-settings.llm.default_llm",
348
defaultMessage: "Default AI Model",
349
})}
350
>
351
<LLMSelector model={model} setModel={setModel} />
352
</LabeledRow>
353
);
354
}
355
356
function render_llm_reply_language(): Rendered {
357
return (
358
<Switch
359
checked={!!props.other_settings.get(OTHER_SETTINGS_REPLY_ENGLISH_KEY)}
360
onChange={(e) => {
361
on_change(OTHER_SETTINGS_REPLY_ENGLISH_KEY, e.target.checked);
362
}}
363
>
364
<FormattedMessage
365
id="account.other-settings.llm.reply_language"
366
defaultMessage={`<strong>Always reply in English:</strong>
367
If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}
368
values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}
369
/>
370
</Switch>
371
);
372
}
373
374
function render_custom_llm(): Rendered {
375
// on cocalc.com, do not even show that they're disabled
376
if (isCoCalcCom && !user_defined_llm) return;
377
return (
378
<UserDefinedLLMComponent
379
on_change={on_change}
380
style={{ marginTop: "20px" }}
381
/>
382
);
383
}
384
385
function render_llm_settings() {
386
// we hide this panel, if all servers and user defined LLms are disabled
387
const customize = redux.getStore("customize");
388
const enabledLLMs = customize.getEnabledLLMs();
389
const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);
390
if (!anyLLMenabled) return <></>;
391
return (
392
<Panel
393
header={
394
<>
395
<AIAvatar size={18} />{" "}
396
<FormattedMessage
397
id="account.other-settings.llm.title"
398
defaultMessage={`AI Settings`}
399
/>
400
</>
401
}
402
>
403
{render_disable_all_llm()}
404
{render_language_model()}
405
{render_llm_reply_language()}
406
{render_custom_llm()}
407
</Panel>
408
);
409
}
410
411
if (props.other_settings == null) {
412
return <Loading />;
413
}
414
415
const mode = props.mode ?? "full";
416
417
if (mode === "ai") {
418
return render_llm_settings();
419
}
420
421
if (mode === "appearance") {
422
return (
423
<Panel
424
size="small"
425
header={
426
<>
427
<Icon name={THEME_ICON_NAME} /> {intl.formatMessage(labels.theme)}
428
</>
429
}
430
>
431
{render_antd()}
432
</Panel>
433
);
434
}
435
436
if (mode === "other") {
437
return (
438
<>
439
<Panel
440
size="small"
441
header={
442
<>
443
<Icon name="desktop" /> {intl.formatMessage(labels.browser)}
444
</>
445
}
446
>
447
{render_confirm()}
448
{render_standby_timeout()}
449
</Panel>
450
451
<Panel
452
size="small"
453
header={
454
<>
455
<Icon name="folder-open" />{" "}
456
{intl.formatMessage(labels.file_explorer)}
457
</>
458
}
459
>
460
{render_dim_file_extensions()}
461
{render_mask_files()}
462
{render_default_file_sort()}
463
{render_page_size()}
464
{render_new_filenames()}
465
</Panel>
466
467
{/* Projects */}
468
<Panel
469
size="small"
470
header={
471
<>
472
<Icon name="edit" /> {intl.formatMessage(labels.projects)}
473
</>
474
}
475
>
476
{render_vertical_fixed_bar_options()}
477
</Panel>
478
479
{/* Tours at bottom */}
480
<Tours />
481
</>
482
);
483
}
484
485
// mode === "full" no longer exists
486
unreachable(mode);
487
return <></>;
488
}
489
490
import { unreachable } from "@cocalc/util/misc";
491
import UseBalanceTowardSubscriptions from "./balance-toward-subs";
492
493
export function UseBalance({ style, minimal }: { style?; minimal? }) {
494
const use_balance_toward_subscriptions = useTypedRedux(
495
"account",
496
"other_settings",
497
)?.get("use_balance_toward_subscriptions");
498
499
return (
500
<UseBalanceTowardSubscriptions
501
minimal={minimal}
502
style={style}
503
use_balance_toward_subscriptions={use_balance_toward_subscriptions}
504
set_use_balance_toward_subscriptions={(value) => {
505
const actions = redux.getActions("account");
506
actions.set_other_settings("use_balance_toward_subscriptions", value);
507
}}
508
/>
509
);
510
}
511
512