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