CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/account/other-settings.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Card, InputNumber } from "antd";
7
import { Map } from "immutable";
8
import { FormattedMessage, useIntl } from "react-intl";
9
10
import { Checkbox, Panel } from "@cocalc/frontend/antd-bootstrap";
11
import { Rendered, redux, useTypedRedux } from "@cocalc/frontend/app-framework";
12
import { useLocalizationCtx } from "@cocalc/frontend/app/localize";
13
import {
14
A,
15
HelpIcon,
16
Icon,
17
LabeledRow,
18
Loading,
19
NumberInput,
20
Paragraph,
21
SelectorInput,
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 { LOCALIZATIONS, labels } from "@cocalc/frontend/i18n";
27
import {
28
VBAR_EXPLANATION,
29
VBAR_KEY,
30
VBAR_OPTIONS,
31
getValidVBAROption,
32
} from "@cocalc/frontend/project/page/vbar";
33
import { NewFilenameFamilies } from "@cocalc/frontend/project/utils";
34
import track from "@cocalc/frontend/user-tracking";
35
import { webapp_client } from "@cocalc/frontend/webapp-client";
36
import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";
37
import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";
38
import { dark_mode_mins, get_dark_mode_config } from "./dark-mode";
39
import { I18NSelector, I18N_MESSAGE, I18N_TITLE } from "./i18n-selector";
40
import Tours from "./tours";
41
import { useLanguageModelSetting } from "./useLanguageModelSetting";
42
import { UserDefinedLLMComponent } from "./user-defined-llm";
43
44
// See https://github.com/sagemathinc/cocalc/issues/5620
45
// There are weird bugs with relying only on mathjax, whereas our
46
// implementation of katex with a fallback to mathjax works very well.
47
// This makes it so katex can't be disabled.
48
const ALLOW_DISABLE_KATEX = false;
49
50
export function katexIsEnabled() {
51
if (!ALLOW_DISABLE_KATEX) {
52
return true;
53
}
54
return redux.getStore("account")?.getIn(["other_settings", "katex"]) ?? true;
55
}
56
57
interface Props {
58
other_settings: Map<string, any>;
59
is_stripe_customer: boolean;
60
kucalc: string;
61
}
62
63
export function OtherSettings(props: Readonly<Props>): JSX.Element {
64
const intl = useIntl();
65
const { locale } = useLocalizationCtx();
66
const isCoCalcCom = useTypedRedux("customize", "is_cocalc_com");
67
const user_defined_llm = useTypedRedux("customize", "user_defined_llm");
68
69
const [model, setModel] = useLanguageModelSetting();
70
71
function on_change(name: string, value: any): void {
72
redux.getActions("account").set_other_settings(name, value);
73
}
74
75
function toggle_global_banner(val: boolean): void {
76
if (val) {
77
// this must be "null", not "undefined" – otherwise the data isn't stored in the DB.
78
on_change("show_global_info2", null);
79
} else {
80
on_change("show_global_info2", webapp_client.server_time());
81
}
82
}
83
84
// private render_first_steps(): Rendered {
85
// if (props.kucalc !== KUCALC_COCALC_COM) return;
86
// return (
87
// <Checkbox
88
// checked={!!props.other_settings.get("first_steps")}
89
// onChange={(e) => on_change("first_steps", e.target.checked)}
90
// >
91
// Offer the First Steps guide
92
// </Checkbox>
93
// );
94
// }
95
96
function render_global_banner(): Rendered {
97
return (
98
<Checkbox
99
checked={!props.other_settings.get("show_global_info2")}
100
onChange={(e) => toggle_global_banner(e.target.checked)}
101
>
102
<FormattedMessage
103
id="account.other-settings.global_banner"
104
defaultMessage={`<strong>Show announcement banner</strong>: only shows up if there is a
105
message`}
106
/>
107
</Checkbox>
108
);
109
}
110
111
function render_time_ago_absolute(): Rendered {
112
return (
113
<Checkbox
114
checked={!!props.other_settings.get("time_ago_absolute")}
115
onChange={(e) => on_change("time_ago_absolute", e.target.checked)}
116
>
117
<FormattedMessage
118
id="account.other-settings.time_ago_absolute"
119
defaultMessage={`Display <strong>timestamps as absolute points in time</strong>
120
instead of relative to the current time`}
121
/>
122
</Checkbox>
123
);
124
}
125
126
function render_confirm(): Rendered {
127
if (!IS_MOBILE) {
128
return (
129
<Checkbox
130
checked={!!props.other_settings.get("confirm_close")}
131
onChange={(e) => on_change("confirm_close", e.target.checked)}
132
>
133
<FormattedMessage
134
id="account.other-settings.confirm_close"
135
defaultMessage={`<strong>Confirm Close:</strong> always ask for confirmation before
136
closing the browser window`}
137
/>
138
</Checkbox>
139
);
140
}
141
}
142
143
function render_katex() {
144
if (!ALLOW_DISABLE_KATEX) {
145
return null;
146
}
147
return (
148
<Checkbox
149
checked={!!props.other_settings.get("katex")}
150
onChange={(e) => on_change("katex", e.target.checked)}
151
>
152
<FormattedMessage
153
id="account.other-settings.katex"
154
defaultMessage={`<strong>KaTeX:</strong> attempt to render formulas
155
using {katex} (much faster, but missing context menu options)`}
156
values={{ katex: <A href={"https://katex.org/"}>KaTeX</A> }}
157
/>
158
</Checkbox>
159
);
160
}
161
162
function render_standby_timeout(): Rendered {
163
if (IS_TOUCH) {
164
return;
165
}
166
return (
167
<LabeledRow
168
label={intl.formatMessage({
169
id: "account.other-settings.standby_timeout",
170
defaultMessage: "Standby timeout",
171
})}
172
>
173
<NumberInput
174
on_change={(n) => on_change("standby_timeout_m", n)}
175
min={1}
176
max={180}
177
unit="minutes"
178
number={props.other_settings.get("standby_timeout_m")}
179
/>
180
</LabeledRow>
181
);
182
}
183
184
function render_mask_files(): Rendered {
185
return (
186
<Checkbox
187
checked={!!props.other_settings.get("mask_files")}
188
onChange={(e) => on_change("mask_files", e.target.checked)}
189
>
190
<FormattedMessage
191
id="account.other-settings.mask_files"
192
defaultMessage={`<strong>Mask files:</strong> grey out files in the files viewer
193
that you probably do not want to open`}
194
/>
195
</Checkbox>
196
);
197
}
198
199
function render_hide_project_popovers(): Rendered {
200
return (
201
<Checkbox
202
checked={!!props.other_settings.get("hide_project_popovers")}
203
onChange={(e) => on_change("hide_project_popovers", e.target.checked)}
204
>
205
<FormattedMessage
206
id="account.other-settings.project_popovers"
207
defaultMessage={`<strong>Hide Project Tab Popovers:</strong>
208
do not show the popovers over the project tabs`}
209
/>
210
</Checkbox>
211
);
212
}
213
214
function render_hide_file_popovers(): Rendered {
215
return (
216
<Checkbox
217
checked={!!props.other_settings.get("hide_file_popovers")}
218
onChange={(e) => on_change("hide_file_popovers", e.target.checked)}
219
>
220
<FormattedMessage
221
id="account.other-settings.file_popovers"
222
defaultMessage={`<strong>Hide File Tab Popovers:</strong>
223
do not show the popovers over file tabs`}
224
/>
225
</Checkbox>
226
);
227
}
228
229
function render_hide_button_tooltips(): Rendered {
230
return (
231
<Checkbox
232
checked={!!props.other_settings.get("hide_button_tooltips")}
233
onChange={(e) => on_change("hide_button_tooltips", e.target.checked)}
234
>
235
<FormattedMessage
236
id="account.other-settings.button_tooltips"
237
defaultMessage={`<strong>Hide Button Tooltips:</strong>
238
hides some button tooltips (this is only partial)`}
239
/>
240
</Checkbox>
241
);
242
}
243
244
function render_default_file_sort(): Rendered {
245
return (
246
<LabeledRow
247
label={intl.formatMessage({
248
id: "account.other-settings.default_file_sort.label",
249
defaultMessage: "Default file sort",
250
})}
251
>
252
<SelectorInput
253
selected={props.other_settings.get("default_file_sort")}
254
options={{
255
time: intl.formatMessage({
256
id: "account.other-settings.default_file_sort.by_time",
257
defaultMessage: "Sort by time",
258
}),
259
name: intl.formatMessage({
260
id: "account.other-settings.default_file_sort.by_name",
261
defaultMessage: "Sort by name",
262
}),
263
}}
264
on_change={(value) => on_change("default_file_sort", value)}
265
/>
266
</LabeledRow>
267
);
268
}
269
270
function render_new_filenames(): Rendered {
271
const selected =
272
props.other_settings.get(NEW_FILENAMES) ?? DEFAULT_NEW_FILENAMES;
273
return (
274
<LabeledRow
275
label={intl.formatMessage({
276
id: "account.other-settings.filename_generator.label",
277
defaultMessage: "Filename generator",
278
})}
279
>
280
<div>
281
<SelectorInput
282
selected={selected}
283
options={NewFilenameFamilies}
284
on_change={(value) => on_change(NEW_FILENAMES, value)}
285
/>
286
<Paragraph
287
type="secondary"
288
ellipsis={{ expandable: true, symbol: "more" }}
289
>
290
{intl.formatMessage({
291
id: "account.other-settings.filename_generator.description",
292
defaultMessage: `Select how automatically generated filenames are generated.
293
In particular, to make them unique or to include the current time.`,
294
})}
295
</Paragraph>
296
</div>
297
</LabeledRow>
298
);
299
}
300
301
function render_page_size(): Rendered {
302
return (
303
<LabeledRow
304
label={intl.formatMessage({
305
id: "account.other-settings._page_size.label",
306
defaultMessage: "Number of files per page",
307
})}
308
>
309
<NumberInput
310
on_change={(n) => on_change("page_size", n)}
311
min={1}
312
max={10000}
313
number={props.other_settings.get("page_size")}
314
/>
315
</LabeledRow>
316
);
317
}
318
319
function render_no_free_warnings(): Rendered {
320
let extra;
321
if (!props.is_stripe_customer) {
322
extra = <span>(only available to customers)</span>;
323
} else {
324
extra = <span>(thanks for being a customer)</span>;
325
}
326
return (
327
<Checkbox
328
disabled={!props.is_stripe_customer}
329
checked={!!props.other_settings.get("no_free_warnings")}
330
onChange={(e) => on_change("no_free_warnings", e.target.checked)}
331
>
332
Hide free warnings: do{" "}
333
<b>
334
<i>not</i>
335
</b>{" "}
336
show a warning banner when using a free trial project {extra}
337
</Checkbox>
338
);
339
}
340
341
function render_dark_mode(): Rendered {
342
const checked = !!props.other_settings.get("dark_mode");
343
const config = get_dark_mode_config(props.other_settings.toJS());
344
const label_style = { width: "100px", display: "inline-block" } as const;
345
return (
346
<div>
347
<Checkbox
348
checked={checked}
349
onChange={(e) => on_change("dark_mode", e.target.checked)}
350
style={{
351
color: "rgba(229, 224, 216)",
352
backgroundColor: "rgb(36, 37, 37)",
353
marginLeft: "-5px",
354
padding: "5px",
355
borderRadius: "3px",
356
}}
357
>
358
<FormattedMessage
359
id="account.other-settings.theme.dark_mode.compact"
360
defaultMessage={`Dark mode: reduce eye strain by showing a dark background (via {DR})`}
361
values={{
362
DR: (
363
<A
364
style={{ color: "#e96c4d", fontWeight: 700 }}
365
href="https://darkreader.org/"
366
>
367
DARK READER
368
</A>
369
),
370
}}
371
/>
372
</Checkbox>
373
{checked ? (
374
<Card
375
size="small"
376
title={intl.formatMessage({
377
id: "account.other-settings.theme.dark_mode.configuration",
378
defaultMessage: "Dark Mode Configuration",
379
})}
380
>
381
<span style={label_style}>
382
<FormattedMessage
383
id="account.other-settings.theme.dark_mode.brightness"
384
defaultMessage="Brightness"
385
/>
386
</span>
387
<InputNumber
388
min={dark_mode_mins.brightness}
389
max={100}
390
value={config.brightness}
391
onChange={(x) => on_change("dark_mode_brightness", x)}
392
/>
393
<br />
394
<span style={label_style}>
395
<FormattedMessage
396
id="account.other-settings.theme.dark_mode.contrast"
397
defaultMessage="Contrast"
398
/>
399
</span>
400
<InputNumber
401
min={dark_mode_mins.contrast}
402
max={100}
403
value={config.contrast}
404
onChange={(x) => on_change("dark_mode_contrast", x)}
405
/>
406
<br />
407
<span style={label_style}>
408
<FormattedMessage
409
id="account.other-settings.theme.dark_mode.sepia"
410
defaultMessage="Sepia"
411
/>
412
</span>
413
<InputNumber
414
min={dark_mode_mins.sepia}
415
max={100}
416
value={config.sepia}
417
onChange={(x) => on_change("dark_mode_sepia", x)}
418
/>
419
<br />
420
<span style={label_style}>
421
<FormattedMessage
422
id="account.other-settings.theme.dark_mode.grayscale"
423
defaultMessage="Grayscale"
424
/>
425
</span>
426
<InputNumber
427
min={dark_mode_mins.grayscale}
428
max={100}
429
value={config.grayscale}
430
onChange={(x) => on_change("dark_mode_grayscale", x)}
431
/>
432
</Card>
433
) : undefined}
434
</div>
435
);
436
}
437
438
function render_antd(): Rendered {
439
return (
440
<>
441
<Checkbox
442
checked={props.other_settings.get("antd_rounded", true)}
443
onChange={(e) => on_change("antd_rounded", e.target.checked)}
444
>
445
<FormattedMessage
446
id="account.other-settings.theme.antd.rounded"
447
defaultMessage={`<b>Rounded Design</b>: use rounded corners for buttons, etc.`}
448
/>
449
</Checkbox>
450
<Checkbox
451
checked={props.other_settings.get("antd_animate", true)}
452
onChange={(e) => on_change("antd_animate", e.target.checked)}
453
>
454
<FormattedMessage
455
id="account.other-settings.theme.antd.animations"
456
defaultMessage={`<b>Animations</b>: briefly animate some aspects, e.g. buttons`}
457
/>
458
</Checkbox>
459
<Checkbox
460
checked={props.other_settings.get("antd_brandcolors", false)}
461
onChange={(e) => on_change("antd_brandcolors", e.target.checked)}
462
>
463
<FormattedMessage
464
id="account.other-settings.theme.antd.color_scheme"
465
defaultMessage={`<b>Color Scheme</b>: use brand colors instead of default colors`}
466
/>
467
</Checkbox>
468
<Checkbox
469
checked={props.other_settings.get("antd_compact", false)}
470
onChange={(e) => on_change("antd_compact", e.target.checked)}
471
>
472
<FormattedMessage
473
id="account.other-settings.theme.antd.compact"
474
defaultMessage={`<b>Compact Design</b>: use a more compact design`}
475
/>
476
</Checkbox>
477
</>
478
);
479
}
480
481
function render_i18n_selector(): Rendered {
482
return (
483
<LabeledRow label={intl.formatMessage(labels.language)}>
484
<div>
485
<I18NSelector />{" "}
486
<HelpIcon title={intl.formatMessage(I18N_TITLE)}>
487
{intl.formatMessage(I18N_MESSAGE)}
488
</HelpIcon>
489
</div>
490
</LabeledRow>
491
);
492
}
493
494
function render_vertical_fixed_bar_options(): Rendered {
495
const selected = getValidVBAROption(props.other_settings.get(VBAR_KEY));
496
const options = Object.fromEntries(
497
Object.entries(VBAR_OPTIONS).map(([k, v]) => [k, intl.formatMessage(v)]),
498
);
499
return (
500
<LabeledRow
501
label={intl.formatMessage({
502
id: "account.other-settings.vbar.title",
503
defaultMessage: "Vertical Project Bar",
504
})}
505
>
506
<div>
507
<SelectorInput
508
style={{ marginBottom: "10px" }}
509
selected={selected}
510
options={options}
511
on_change={(value) => {
512
on_change(VBAR_KEY, value);
513
track("flyout", { aspect: "layout", how: "account", value });
514
}}
515
/>
516
<Paragraph
517
type="secondary"
518
ellipsis={{ expandable: true, symbol: "more" }}
519
>
520
{intl.formatMessage(VBAR_EXPLANATION)}
521
</Paragraph>
522
</div>
523
</LabeledRow>
524
);
525
}
526
527
function render_disable_all_llm(): Rendered {
528
return (
529
<Checkbox
530
checked={!!props.other_settings.get("openai_disabled")}
531
onChange={(e) => {
532
on_change("openai_disabled", e.target.checked);
533
redux.getStore("projects").clearOpenAICache();
534
}}
535
>
536
<FormattedMessage
537
id="account.other-settings.llm.disable_all"
538
defaultMessage={`<strong>Disable all AI integrations</strong>,
539
e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}
540
/>
541
</Checkbox>
542
);
543
}
544
545
function render_language_model(): Rendered {
546
return (
547
<LabeledRow
548
label={intl.formatMessage({
549
id: "account.other-settings.llm.default_llm",
550
defaultMessage: "Default AI Language Model",
551
})}
552
>
553
<LLMSelector model={model} setModel={setModel} />
554
</LabeledRow>
555
);
556
}
557
558
function render_llm_reply_language(): Rendered {
559
return (
560
<Checkbox
561
checked={!!props.other_settings.get(OTHER_SETTINGS_REPLY_ENGLISH_KEY)}
562
onChange={(e) => {
563
on_change(OTHER_SETTINGS_REPLY_ENGLISH_KEY, e.target.checked);
564
}}
565
>
566
<FormattedMessage
567
id="account.other-settings.llm.reply_language"
568
defaultMessage={`<strong>Always reply in English:</strong>
569
If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}
570
values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}
571
/>
572
</Checkbox>
573
);
574
}
575
576
function render_custom_llm(): Rendered {
577
// on cocalc.com, do not even show that they're disabled
578
if (isCoCalcCom && !user_defined_llm) return;
579
return <UserDefinedLLMComponent on_change={on_change} />;
580
}
581
582
function render_llm_settings() {
583
// we hide this panel, if all servers and user defined LLms are disabled
584
const customize = redux.getStore("customize");
585
const enabledLLMs = customize.getEnabledLLMs();
586
const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);
587
if (!anyLLMenabled) return;
588
return (
589
<Panel
590
header={
591
<>
592
<AIAvatar size={18} />{" "}
593
<FormattedMessage
594
id="account.other-settings.llm.title"
595
defaultMessage={`AI Settings`}
596
/>
597
</>
598
}
599
>
600
{render_disable_all_llm()}
601
{render_language_model()}
602
{render_llm_reply_language()}
603
{render_custom_llm()}
604
</Panel>
605
);
606
}
607
608
if (props.other_settings == null) {
609
return <Loading />;
610
}
611
return (
612
<>
613
{render_llm_settings()}
614
615
<Panel
616
header={
617
<>
618
<Icon name="highlighter" />{" "}
619
<FormattedMessage
620
id="account.other-settings.theme"
621
defaultMessage="Theme"
622
description="Visual UI theme of the application"
623
/>
624
</>
625
}
626
>
627
{render_dark_mode()}
628
{render_antd()}
629
</Panel>
630
631
<Panel
632
header={
633
<>
634
<Icon name="gear" /> Other
635
</>
636
}
637
>
638
{render_confirm()}
639
{render_katex()}
640
{render_time_ago_absolute()}
641
{render_global_banner()}
642
{render_mask_files()}
643
{render_hide_project_popovers()}
644
{render_hide_file_popovers()}
645
{render_hide_button_tooltips()}
646
{render_no_free_warnings()}
647
<Checkbox
648
checked={!!props.other_settings.get("disable_markdown_codebar")}
649
onChange={(e) => {
650
on_change("disable_markdown_codebar", e.target.checked);
651
}}
652
>
653
<FormattedMessage
654
id="account.other-settings.markdown_codebar"
655
defaultMessage={`<strong>Disable the markdown code bar</strong> in all markdown documents.
656
Checking this hides the extra run, copy, and explain buttons in fenced code blocks.`}
657
/>
658
</Checkbox>
659
{render_i18n_selector()}
660
{render_vertical_fixed_bar_options()}
661
{render_new_filenames()}
662
{render_default_file_sort()}
663
{render_page_size()}
664
{render_standby_timeout()}
665
<div style={{ height: "10px" }} />
666
<Tours />
667
</Panel>
668
</>
669
);
670
}
671
672