Path: blob/master/src/packages/frontend/account/other-settings.tsx
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Card, InputNumber } from "antd";6import { Map } from "immutable";7import { FormattedMessage, useIntl } from "react-intl";89import { Checkbox, Panel } from "@cocalc/frontend/antd-bootstrap";10import { Rendered, redux, useTypedRedux } from "@cocalc/frontend/app-framework";11import { useLocalizationCtx } from "@cocalc/frontend/app/localize";12import {13A,14HelpIcon,15Icon,16LabeledRow,17Loading,18NumberInput,19Paragraph,20SelectorInput,21Text,22} from "@cocalc/frontend/components";23import AIAvatar from "@cocalc/frontend/components/ai-avatar";24import { IS_MOBILE, IS_TOUCH } from "@cocalc/frontend/feature";25import LLMSelector from "@cocalc/frontend/frame-editors/llm/llm-selector";26import { LOCALIZATIONS, labels } from "@cocalc/frontend/i18n";27import { getValidActivityBarOption } from "@cocalc/frontend/project/page/activity-bar";28import {29ACTIVITY_BAR_EXPLANATION,30ACTIVITY_BAR_KEY,31ACTIVITY_BAR_LABELS,32ACTIVITY_BAR_LABELS_DEFAULT,33ACTIVITY_BAR_OPTIONS,34ACTIVITY_BAR_TITLE,35ACTIVITY_BAR_TOGGLE_LABELS,36ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION,37} from "@cocalc/frontend/project/page/activity-bar-consts";38import { NewFilenameFamilies } from "@cocalc/frontend/project/utils";39import track from "@cocalc/frontend/user-tracking";40import { webapp_client } from "@cocalc/frontend/webapp-client";41import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";42import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";43import { dark_mode_mins, get_dark_mode_config } from "./dark-mode";44import { I18NSelector, I18N_MESSAGE, I18N_TITLE } from "./i18n-selector";45import Messages from "./messages";46import Tours from "./tours";47import { useLanguageModelSetting } from "./useLanguageModelSetting";48import { UserDefinedLLMComponent } from "./user-defined-llm";4950// See https://github.com/sagemathinc/cocalc/issues/562051// There are weird bugs with relying only on mathjax, whereas our52// implementation of katex with a fallback to mathjax works very well.53// This makes it so katex can't be disabled.54const ALLOW_DISABLE_KATEX = false;5556export function katexIsEnabled() {57if (!ALLOW_DISABLE_KATEX) {58return true;59}60return redux.getStore("account")?.getIn(["other_settings", "katex"]) ?? true;61}6263interface Props {64other_settings: Map<string, any>;65is_stripe_customer: boolean;66kucalc: string;67}6869export function OtherSettings(props: Readonly<Props>): React.JSX.Element {70const intl = useIntl();71const { locale } = useLocalizationCtx();72const isCoCalcCom = useTypedRedux("customize", "is_cocalc_com");73const user_defined_llm = useTypedRedux("customize", "user_defined_llm");7475const [model, setModel] = useLanguageModelSetting();7677function on_change(name: string, value: any): void {78redux.getActions("account").set_other_settings(name, value);79}8081function toggle_global_banner(val: boolean): void {82if (val) {83// this must be "null", not "undefined" – otherwise the data isn't stored in the DB.84on_change("show_global_info2", null);85} else {86on_change("show_global_info2", webapp_client.server_time());87}88}8990// private render_first_steps(): Rendered {91// if (props.kucalc !== KUCALC_COCALC_COM) return;92// return (93// <Checkbox94// checked={!!props.other_settings.get("first_steps")}95// onChange={(e) => on_change("first_steps", e.target.checked)}96// >97// Offer the First Steps guide98// </Checkbox>99// );100// }101102function render_global_banner(): Rendered {103return (104<Checkbox105checked={!props.other_settings.get("show_global_info2")}106onChange={(e) => toggle_global_banner(e.target.checked)}107>108<FormattedMessage109id="account.other-settings.global_banner"110defaultMessage={`<strong>Show Announcement Banner</strong>: only shows up if there is a111message`}112/>113</Checkbox>114);115}116117function render_time_ago_absolute(): Rendered {118return (119<Checkbox120checked={!!props.other_settings.get("time_ago_absolute")}121onChange={(e) => on_change("time_ago_absolute", e.target.checked)}122>123<FormattedMessage124id="account.other-settings.time_ago_absolute"125defaultMessage={`<strong>Display Timestamps as absolute points in time</strong>126instead of relative to the current time`}127/>128</Checkbox>129);130}131132function render_confirm(): Rendered {133if (!IS_MOBILE) {134return (135<Checkbox136checked={!!props.other_settings.get("confirm_close")}137onChange={(e) => on_change("confirm_close", e.target.checked)}138>139<FormattedMessage140id="account.other-settings.confirm_close"141defaultMessage={`<strong>Confirm Close:</strong> always ask for confirmation before142closing the browser window`}143/>144</Checkbox>145);146}147}148149function render_katex() {150if (!ALLOW_DISABLE_KATEX) {151return null;152}153return (154<Checkbox155checked={!!props.other_settings.get("katex")}156onChange={(e) => on_change("katex", e.target.checked)}157>158<FormattedMessage159id="account.other-settings.katex"160defaultMessage={`<strong>KaTeX:</strong> attempt to render formulas161using {katex} (much faster, but missing context menu options)`}162values={{ katex: <A href={"https://katex.org/"}>KaTeX</A> }}163/>164</Checkbox>165);166}167168function render_standby_timeout(): Rendered {169if (IS_TOUCH) {170return;171}172return (173<LabeledRow174label={intl.formatMessage({175id: "account.other-settings.standby_timeout",176defaultMessage: "Standby timeout",177})}178>179<NumberInput180on_change={(n) => on_change("standby_timeout_m", n)}181min={1}182max={180}183unit="minutes"184number={props.other_settings.get("standby_timeout_m")}185/>186</LabeledRow>187);188}189190function render_mask_files(): Rendered {191return (192<Checkbox193checked={!!props.other_settings.get("mask_files")}194onChange={(e) => on_change("mask_files", e.target.checked)}195>196<FormattedMessage197id="account.other-settings.mask_files"198defaultMessage={`<strong>Mask Files:</strong> grey out files in the files viewer199that you probably do not want to open`}200/>201</Checkbox>202);203}204205function render_hide_project_popovers(): Rendered {206return (207<Checkbox208checked={!!props.other_settings.get("hide_project_popovers")}209onChange={(e) => on_change("hide_project_popovers", e.target.checked)}210>211<FormattedMessage212id="account.other-settings.project_popovers"213defaultMessage={`<strong>Hide Project Tab Popovers:</strong>214do not show the popovers over the project tabs`}215/>216</Checkbox>217);218}219220function render_hide_file_popovers(): Rendered {221return (222<Checkbox223checked={!!props.other_settings.get("hide_file_popovers")}224onChange={(e) => on_change("hide_file_popovers", e.target.checked)}225>226<FormattedMessage227id="account.other-settings.file_popovers"228defaultMessage={`<strong>Hide File Tab Popovers:</strong>229do not show the popovers over file tabs`}230/>231</Checkbox>232);233}234235function render_hide_button_tooltips(): Rendered {236return (237<Checkbox238checked={!!props.other_settings.get("hide_button_tooltips")}239onChange={(e) => on_change("hide_button_tooltips", e.target.checked)}240>241<FormattedMessage242id="account.other-settings.button_tooltips"243defaultMessage={`<strong>Hide Button Tooltips:</strong>244hides some button tooltips (this is only partial)`}245/>246</Checkbox>247);248}249250function render_show_symbol_bar_labels(): Rendered {251return (252<Checkbox253checked={!!props.other_settings.get("show_symbol_bar_labels")}254onChange={(e) => on_change("show_symbol_bar_labels", e.target.checked)}255>256<FormattedMessage257id="account.other-settings.symbol_bar_labels"258defaultMessage={`<strong>Show Symbol Bar Labels:</strong>259show labels in the frame editor symbol bar`}260/>261</Checkbox>262);263}264265function render_default_file_sort(): Rendered {266return (267<LabeledRow268label={intl.formatMessage({269id: "account.other-settings.default_file_sort.label",270defaultMessage: "Default file sort",271})}272>273<SelectorInput274selected={props.other_settings.get("default_file_sort")}275options={{276time: intl.formatMessage({277id: "account.other-settings.default_file_sort.by_time",278defaultMessage: "Sort by time",279}),280name: intl.formatMessage({281id: "account.other-settings.default_file_sort.by_name",282defaultMessage: "Sort by name",283}),284}}285on_change={(value) => on_change("default_file_sort", value)}286/>287</LabeledRow>288);289}290291function render_new_filenames(): Rendered {292const selected =293props.other_settings.get(NEW_FILENAMES) ?? DEFAULT_NEW_FILENAMES;294return (295<LabeledRow296label={intl.formatMessage({297id: "account.other-settings.filename_generator.label",298defaultMessage: "Filename generator",299})}300>301<div>302<SelectorInput303selected={selected}304options={NewFilenameFamilies}305on_change={(value) => on_change(NEW_FILENAMES, value)}306/>307<Paragraph308type="secondary"309ellipsis={{ expandable: true, symbol: "more" }}310>311{intl.formatMessage({312id: "account.other-settings.filename_generator.description",313defaultMessage: `Select how automatically generated filenames are generated.314In particular, to make them unique or to include the current time.`,315})}316</Paragraph>317</div>318</LabeledRow>319);320}321322function render_page_size(): Rendered {323return (324<LabeledRow325label={intl.formatMessage({326id: "account.other-settings._page_size.label",327defaultMessage: "Number of files per page",328})}329>330<NumberInput331on_change={(n) => on_change("page_size", n)}332min={1}333max={10000}334number={props.other_settings.get("page_size")}335/>336</LabeledRow>337);338}339340function render_no_free_warnings(): Rendered {341let extra;342if (!props.is_stripe_customer) {343extra = <span>(only available to customers)</span>;344} else {345extra = <span>(thanks for being a customer)</span>;346}347return (348<Checkbox349disabled={!props.is_stripe_customer}350checked={!!props.other_settings.get("no_free_warnings")}351onChange={(e) => on_change("no_free_warnings", e.target.checked)}352>353Hide free warnings: do{" "}354<b>355<i>not</i>356</b>{" "}357show a warning banner when using a free trial project {extra}358</Checkbox>359);360}361362function render_dark_mode(): Rendered {363const checked = !!props.other_settings.get("dark_mode");364const config = get_dark_mode_config(props.other_settings.toJS());365const label_style = { width: "100px", display: "inline-block" } as const;366return (367<div>368<Checkbox369checked={checked}370onChange={(e) => on_change("dark_mode", e.target.checked)}371style={{372color: "rgba(229, 224, 216)",373backgroundColor: "rgb(36, 37, 37)",374marginLeft: "-5px",375padding: "5px",376borderRadius: "3px",377}}378>379<FormattedMessage380id="account.other-settings.theme.dark_mode.compact"381defaultMessage={`Dark mode: reduce eye strain by showing a dark background (via {DR})`}382values={{383DR: (384<A385style={{ color: "#e96c4d", fontWeight: 700 }}386href="https://darkreader.org/"387>388DARK READER389</A>390),391}}392/>393</Checkbox>394{checked ? (395<Card396size="small"397title={intl.formatMessage({398id: "account.other-settings.theme.dark_mode.configuration",399defaultMessage: "Dark Mode Configuration",400})}401>402<span style={label_style}>403<FormattedMessage404id="account.other-settings.theme.dark_mode.brightness"405defaultMessage="Brightness"406/>407</span>408<InputNumber409min={dark_mode_mins.brightness}410max={100}411value={config.brightness}412onChange={(x) => on_change("dark_mode_brightness", x)}413/>414<br />415<span style={label_style}>416<FormattedMessage417id="account.other-settings.theme.dark_mode.contrast"418defaultMessage="Contrast"419/>420</span>421<InputNumber422min={dark_mode_mins.contrast}423max={100}424value={config.contrast}425onChange={(x) => on_change("dark_mode_contrast", x)}426/>427<br />428<span style={label_style}>429<FormattedMessage430id="account.other-settings.theme.dark_mode.sepia"431defaultMessage="Sepia"432/>433</span>434<InputNumber435min={dark_mode_mins.sepia}436max={100}437value={config.sepia}438onChange={(x) => on_change("dark_mode_sepia", x)}439/>440<br />441<span style={label_style}>442<FormattedMessage443id="account.other-settings.theme.dark_mode.grayscale"444defaultMessage="Grayscale"445/>446</span>447<InputNumber448min={dark_mode_mins.grayscale}449max={100}450value={config.grayscale}451onChange={(x) => on_change("dark_mode_grayscale", x)}452/>453</Card>454) : undefined}455</div>456);457}458459function render_antd(): Rendered {460return (461<>462<Checkbox463checked={props.other_settings.get("antd_rounded", true)}464onChange={(e) => on_change("antd_rounded", e.target.checked)}465>466<FormattedMessage467id="account.other-settings.theme.antd.rounded"468defaultMessage={`<b>Rounded Design</b>: use rounded corners for buttons, etc.`}469/>470</Checkbox>471<Checkbox472checked={props.other_settings.get("antd_animate", true)}473onChange={(e) => on_change("antd_animate", e.target.checked)}474>475<FormattedMessage476id="account.other-settings.theme.antd.animations"477defaultMessage={`<b>Animations</b>: briefly animate some aspects, e.g. buttons`}478/>479</Checkbox>480<Checkbox481checked={props.other_settings.get("antd_brandcolors", false)}482onChange={(e) => on_change("antd_brandcolors", e.target.checked)}483>484<FormattedMessage485id="account.other-settings.theme.antd.color_scheme"486defaultMessage={`<b>Color Scheme</b>: use brand colors instead of default colors`}487/>488</Checkbox>489<Checkbox490checked={props.other_settings.get("antd_compact", false)}491onChange={(e) => on_change("antd_compact", e.target.checked)}492>493<FormattedMessage494id="account.other-settings.theme.antd.compact"495defaultMessage={`<b>Compact Design</b>: use a more compact design`}496/>497</Checkbox>498</>499);500}501502function render_i18n_selector(): Rendered {503return (504<LabeledRow label={intl.formatMessage(labels.language)}>505<div>506<I18NSelector />{" "}507<HelpIcon title={intl.formatMessage(I18N_TITLE)}>508{intl.formatMessage(I18N_MESSAGE)}509</HelpIcon>510</div>511</LabeledRow>512);513}514515function render_vertical_fixed_bar_options(): Rendered {516const selected = getValidActivityBarOption(517props.other_settings.get(ACTIVITY_BAR_KEY),518);519const options = Object.fromEntries(520Object.entries(ACTIVITY_BAR_OPTIONS).map(([k, v]) => [521k,522intl.formatMessage(v),523]),524);525return (526<LabeledRow label={intl.formatMessage(ACTIVITY_BAR_TITLE)}>527<div>528<SelectorInput529style={{ marginBottom: "10px" }}530selected={selected}531options={options}532on_change={(value) => {533on_change(ACTIVITY_BAR_KEY, value);534track("flyout", { aspect: "layout", how: "account", value });535}}536/>537<Paragraph538type="secondary"539ellipsis={{ expandable: true, symbol: "more" }}540>541{intl.formatMessage(ACTIVITY_BAR_EXPLANATION)}542</Paragraph>543<Checkbox544checked={545props.other_settings.get(ACTIVITY_BAR_LABELS) ??546ACTIVITY_BAR_LABELS_DEFAULT547}548onChange={(e) => {549on_change(ACTIVITY_BAR_LABELS, e.target.checked);550}}551>552<Paragraph553type="secondary"554style={{ marginBottom: 0 }}555ellipsis={{ expandable: true, symbol: "more" }}556>557<Text strong>558{intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS, {559show: false,560})}561</Text>562: {intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION)}563</Paragraph>564</Checkbox>565</div>566</LabeledRow>567);568}569570function render_disable_all_llm(): Rendered {571return (572<Checkbox573checked={!!props.other_settings.get("openai_disabled")}574onChange={(e) => {575on_change("openai_disabled", e.target.checked);576redux.getStore("projects").clearOpenAICache();577}}578>579<FormattedMessage580id="account.other-settings.llm.disable_all"581defaultMessage={`<strong>Disable all AI integrations</strong>,582e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}583/>584</Checkbox>585);586}587588function render_language_model(): Rendered {589return (590<LabeledRow591label={intl.formatMessage({592id: "account.other-settings.llm.default_llm",593defaultMessage: "Default AI Model",594})}595>596<LLMSelector model={model} setModel={setModel} />597</LabeledRow>598);599}600601function render_llm_reply_language(): Rendered {602return (603<Checkbox604checked={!!props.other_settings.get(OTHER_SETTINGS_REPLY_ENGLISH_KEY)}605onChange={(e) => {606on_change(OTHER_SETTINGS_REPLY_ENGLISH_KEY, e.target.checked);607}}608>609<FormattedMessage610id="account.other-settings.llm.reply_language"611defaultMessage={`<strong>Always reply in English:</strong>612If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}613values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}614/>615</Checkbox>616);617}618619function render_custom_llm(): Rendered {620// on cocalc.com, do not even show that they're disabled621if (isCoCalcCom && !user_defined_llm) return;622return <UserDefinedLLMComponent on_change={on_change} />;623}624625function render_llm_settings() {626// we hide this panel, if all servers and user defined LLms are disabled627const customize = redux.getStore("customize");628const enabledLLMs = customize.getEnabledLLMs();629const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);630if (!anyLLMenabled) return;631return (632<Panel633header={634<>635<AIAvatar size={18} />{" "}636<FormattedMessage637id="account.other-settings.llm.title"638defaultMessage={`AI Settings`}639/>640</>641}642>643{render_disable_all_llm()}644{render_language_model()}645{render_llm_reply_language()}646{render_custom_llm()}647</Panel>648);649}650651if (props.other_settings == null) {652return <Loading />;653}654return (655<>656{render_llm_settings()}657658<Panel659header={660<>661<Icon name="highlighter" />{" "}662<FormattedMessage663id="account.other-settings.theme"664defaultMessage="Theme"665description="Visual UI theme of the application"666/>667</>668}669>670{render_dark_mode()}671{render_antd()}672</Panel>673674<Panel675header={676<>677<Icon name="gear" /> Other678</>679}680>681{render_confirm()}682{render_katex()}683{render_time_ago_absolute()}684{render_global_banner()}685{render_mask_files()}686{render_hide_project_popovers()}687{render_hide_file_popovers()}688{render_hide_button_tooltips()}689{render_show_symbol_bar_labels()}690<Checkbox691checked={!!props.other_settings.get("hide_navbar_balance")}692onChange={(e) => on_change("hide_navbar_balance", e.target.checked)}693>694<FormattedMessage695id="account.other-settings.hide_navbar_balance"696defaultMessage={`<strong>Hide Account Balance</strong> in navigation bar`}697/>698</Checkbox>699{render_no_free_warnings()}700<Checkbox701checked={!!props.other_settings.get("disable_markdown_codebar")}702onChange={(e) => {703on_change("disable_markdown_codebar", e.target.checked);704}}705>706<FormattedMessage707id="account.other-settings.markdown_codebar"708defaultMessage={`<strong>Disable the markdown code bar</strong> in all markdown documents.709Checking this hides the extra run, copy, and explain buttons in fenced code blocks.`}710/>711</Checkbox>712{render_i18n_selector()}713{render_vertical_fixed_bar_options()}714{render_new_filenames()}715{render_default_file_sort()}716{render_page_size()}717{render_standby_timeout()}718<div style={{ height: "10px" }} />719<Tours />720<Messages />721<UseBalance style={{ marginTop: "10px" }} />722</Panel>723</>724);725}726727import UseBalanceTowardSubscriptions from "./balance-toward-subs";728729export function UseBalance({ style, minimal }: { style?; minimal? }) {730const use_balance_toward_subscriptions = useTypedRedux(731"account",732"other_settings",733)?.get("use_balance_toward_subscriptions");734735return (736<UseBalanceTowardSubscriptions737minimal={minimal}738style={style}739use_balance_toward_subscriptions={use_balance_toward_subscriptions}740set_use_balance_toward_subscriptions={(value) => {741const actions = redux.getActions("account");742actions.set_other_settings("use_balance_toward_subscriptions", value);743}}744/>745);746}747748749