Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/account/other-settings.tsx
Views: 687
/*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,21} from "@cocalc/frontend/components";22import AIAvatar from "@cocalc/frontend/components/ai-avatar";23import { IS_MOBILE, IS_TOUCH } from "@cocalc/frontend/feature";24import LLMSelector from "@cocalc/frontend/frame-editors/llm/llm-selector";25import { LOCALIZATIONS, labels } from "@cocalc/frontend/i18n";26import {27VBAR_EXPLANATION,28VBAR_KEY,29VBAR_OPTIONS,30getValidVBAROption,31} from "@cocalc/frontend/project/page/vbar";32import { NewFilenameFamilies } from "@cocalc/frontend/project/utils";33import track from "@cocalc/frontend/user-tracking";34import { webapp_client } from "@cocalc/frontend/webapp-client";35import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";36import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";37import { dark_mode_mins, get_dark_mode_config } from "./dark-mode";38import { I18NSelector, I18N_MESSAGE, I18N_TITLE } from "./i18n-selector";39import Tours from "./tours";40import { useLanguageModelSetting } from "./useLanguageModelSetting";41import { UserDefinedLLMComponent } from "./user-defined-llm";4243// See https://github.com/sagemathinc/cocalc/issues/562044// There are weird bugs with relying only on mathjax, whereas our45// implementation of katex with a fallback to mathjax works very well.46// This makes it so katex can't be disabled.47const ALLOW_DISABLE_KATEX = false;4849export function katexIsEnabled() {50if (!ALLOW_DISABLE_KATEX) {51return true;52}53return redux.getStore("account")?.getIn(["other_settings", "katex"]) ?? true;54}5556interface Props {57other_settings: Map<string, any>;58is_stripe_customer: boolean;59kucalc: string;60}6162export function OtherSettings(props: Readonly<Props>): JSX.Element {63const intl = useIntl();64const { locale } = useLocalizationCtx();65const isCoCalcCom = useTypedRedux("customize", "is_cocalc_com");66const user_defined_llm = useTypedRedux("customize", "user_defined_llm");6768const [model, setModel] = useLanguageModelSetting();6970function on_change(name: string, value: any): void {71redux.getActions("account").set_other_settings(name, value);72}7374function toggle_global_banner(val: boolean): void {75if (val) {76// this must be "null", not "undefined" – otherwise the data isn't stored in the DB.77on_change("show_global_info2", null);78} else {79on_change("show_global_info2", webapp_client.server_time());80}81}8283// private render_first_steps(): Rendered {84// if (props.kucalc !== KUCALC_COCALC_COM) return;85// return (86// <Checkbox87// checked={!!props.other_settings.get("first_steps")}88// onChange={(e) => on_change("first_steps", e.target.checked)}89// >90// Offer the First Steps guide91// </Checkbox>92// );93// }9495function render_global_banner(): Rendered {96return (97<Checkbox98checked={!props.other_settings.get("show_global_info2")}99onChange={(e) => toggle_global_banner(e.target.checked)}100>101<FormattedMessage102id="account.other-settings.global_banner"103defaultMessage={`<strong>Show announcement banner</strong>: only shows up if there is a104message`}105/>106</Checkbox>107);108}109110function render_time_ago_absolute(): Rendered {111return (112<Checkbox113checked={!!props.other_settings.get("time_ago_absolute")}114onChange={(e) => on_change("time_ago_absolute", e.target.checked)}115>116<FormattedMessage117id="account.other-settings.time_ago_absolute"118defaultMessage={`Display <strong>timestamps as absolute points in time</strong>119instead of relative to the current time`}120/>121</Checkbox>122);123}124125function render_confirm(): Rendered {126if (!IS_MOBILE) {127return (128<Checkbox129checked={!!props.other_settings.get("confirm_close")}130onChange={(e) => on_change("confirm_close", e.target.checked)}131>132<FormattedMessage133id="account.other-settings.confirm_close"134defaultMessage={`<strong>Confirm Close:</strong> always ask for confirmation before135closing the browser window`}136/>137</Checkbox>138);139}140}141142function render_katex() {143if (!ALLOW_DISABLE_KATEX) {144return null;145}146return (147<Checkbox148checked={!!props.other_settings.get("katex")}149onChange={(e) => on_change("katex", e.target.checked)}150>151<FormattedMessage152id="account.other-settings.katex"153defaultMessage={`<strong>KaTeX:</strong> attempt to render formulas154using {katex} (much faster, but missing context menu options)`}155values={{ katex: <A href={"https://katex.org/"}>KaTeX</A> }}156/>157</Checkbox>158);159}160161function render_standby_timeout(): Rendered {162if (IS_TOUCH) {163return;164}165return (166<LabeledRow167label={intl.formatMessage({168id: "account.other-settings.standby_timeout",169defaultMessage: "Standby timeout",170})}171>172<NumberInput173on_change={(n) => on_change("standby_timeout_m", n)}174min={1}175max={180}176unit="minutes"177number={props.other_settings.get("standby_timeout_m")}178/>179</LabeledRow>180);181}182183function render_mask_files(): Rendered {184return (185<Checkbox186checked={!!props.other_settings.get("mask_files")}187onChange={(e) => on_change("mask_files", e.target.checked)}188>189<FormattedMessage190id="account.other-settings.mask_files"191defaultMessage={`<strong>Mask files:</strong> grey out files in the files viewer192that you probably do not want to open`}193/>194</Checkbox>195);196}197198function render_hide_project_popovers(): Rendered {199return (200<Checkbox201checked={!!props.other_settings.get("hide_project_popovers")}202onChange={(e) => on_change("hide_project_popovers", e.target.checked)}203>204<FormattedMessage205id="account.other-settings.project_popovers"206defaultMessage={`<strong>Hide Project Tab Popovers:</strong>207do not show the popovers over the project tabs`}208/>209</Checkbox>210);211}212213function render_hide_file_popovers(): Rendered {214return (215<Checkbox216checked={!!props.other_settings.get("hide_file_popovers")}217onChange={(e) => on_change("hide_file_popovers", e.target.checked)}218>219<FormattedMessage220id="account.other-settings.file_popovers"221defaultMessage={`<strong>Hide File Tab Popovers:</strong>222do not show the popovers over file tabs`}223/>224</Checkbox>225);226}227228function render_hide_button_tooltips(): Rendered {229return (230<Checkbox231checked={!!props.other_settings.get("hide_button_tooltips")}232onChange={(e) => on_change("hide_button_tooltips", e.target.checked)}233>234<FormattedMessage235id="account.other-settings.button_tooltips"236defaultMessage={`<strong>Hide Button Tooltips:</strong>237hides some button tooltips (this is only partial)`}238/>239</Checkbox>240);241}242243function render_default_file_sort(): Rendered {244return (245<LabeledRow246label={intl.formatMessage({247id: "account.other-settings.default_file_sort.label",248defaultMessage: "Default file sort",249})}250>251<SelectorInput252selected={props.other_settings.get("default_file_sort")}253options={{254time: intl.formatMessage({255id: "account.other-settings.default_file_sort.by_time",256defaultMessage: "Sort by time",257}),258name: intl.formatMessage({259id: "account.other-settings.default_file_sort.by_name",260defaultMessage: "Sort by name",261}),262}}263on_change={(value) => on_change("default_file_sort", value)}264/>265</LabeledRow>266);267}268269function render_new_filenames(): Rendered {270const selected =271props.other_settings.get(NEW_FILENAMES) ?? DEFAULT_NEW_FILENAMES;272return (273<LabeledRow274label={intl.formatMessage({275id: "account.other-settings.filename_generator.label",276defaultMessage: "Filename generator",277})}278>279<div>280<SelectorInput281selected={selected}282options={NewFilenameFamilies}283on_change={(value) => on_change(NEW_FILENAMES, value)}284/>285<Paragraph286type="secondary"287ellipsis={{ expandable: true, symbol: "more" }}288>289{intl.formatMessage({290id: "account.other-settings.filename_generator.description",291defaultMessage: `Select how automatically generated filenames are generated.292In particular, to make them unique or to include the current time.`,293})}294</Paragraph>295</div>296</LabeledRow>297);298}299300function render_page_size(): Rendered {301return (302<LabeledRow303label={intl.formatMessage({304id: "account.other-settings._page_size.label",305defaultMessage: "Number of files per page",306})}307>308<NumberInput309on_change={(n) => on_change("page_size", n)}310min={1}311max={10000}312number={props.other_settings.get("page_size")}313/>314</LabeledRow>315);316}317318function render_no_free_warnings(): Rendered {319let extra;320if (!props.is_stripe_customer) {321extra = <span>(only available to customers)</span>;322} else {323extra = <span>(thanks for being a customer)</span>;324}325return (326<Checkbox327disabled={!props.is_stripe_customer}328checked={!!props.other_settings.get("no_free_warnings")}329onChange={(e) => on_change("no_free_warnings", e.target.checked)}330>331Hide free warnings: do{" "}332<b>333<i>not</i>334</b>{" "}335show a warning banner when using a free trial project {extra}336</Checkbox>337);338}339340function render_dark_mode(): Rendered {341const checked = !!props.other_settings.get("dark_mode");342const config = get_dark_mode_config(props.other_settings.toJS());343const label_style = { width: "100px", display: "inline-block" } as const;344return (345<div>346<Checkbox347checked={checked}348onChange={(e) => on_change("dark_mode", e.target.checked)}349style={{350color: "rgba(229, 224, 216)",351backgroundColor: "rgb(36, 37, 37)",352marginLeft: "-5px",353padding: "5px",354borderRadius: "3px",355}}356>357<FormattedMessage358id="account.other-settings.theme.dark_mode.compact"359defaultMessage={`Dark mode: reduce eye strain by showing a dark background (via {DR})`}360values={{361DR: (362<A363style={{ color: "#e96c4d", fontWeight: 700 }}364href="https://darkreader.org/"365>366DARK READER367</A>368),369}}370/>371</Checkbox>372{checked ? (373<Card374size="small"375title={intl.formatMessage({376id: "account.other-settings.theme.dark_mode.configuration",377defaultMessage: "Dark Mode Configuration",378})}379>380<span style={label_style}>381<FormattedMessage382id="account.other-settings.theme.dark_mode.brightness"383defaultMessage="Brightness"384/>385</span>386<InputNumber387min={dark_mode_mins.brightness}388max={100}389value={config.brightness}390onChange={(x) => on_change("dark_mode_brightness", x)}391/>392<br />393<span style={label_style}>394<FormattedMessage395id="account.other-settings.theme.dark_mode.contrast"396defaultMessage="Contrast"397/>398</span>399<InputNumber400min={dark_mode_mins.contrast}401max={100}402value={config.contrast}403onChange={(x) => on_change("dark_mode_contrast", x)}404/>405<br />406<span style={label_style}>407<FormattedMessage408id="account.other-settings.theme.dark_mode.sepia"409defaultMessage="Sepia"410/>411</span>412<InputNumber413min={dark_mode_mins.sepia}414max={100}415value={config.sepia}416onChange={(x) => on_change("dark_mode_sepia", x)}417/>418<br />419<span style={label_style}>420<FormattedMessage421id="account.other-settings.theme.dark_mode.grayscale"422defaultMessage="Grayscale"423/>424</span>425<InputNumber426min={dark_mode_mins.grayscale}427max={100}428value={config.grayscale}429onChange={(x) => on_change("dark_mode_grayscale", x)}430/>431</Card>432) : undefined}433</div>434);435}436437function render_antd(): Rendered {438return (439<>440<Checkbox441checked={props.other_settings.get("antd_rounded", true)}442onChange={(e) => on_change("antd_rounded", e.target.checked)}443>444<FormattedMessage445id="account.other-settings.theme.antd.rounded"446defaultMessage={`<b>Rounded Design</b>: use rounded corners for buttons, etc.`}447/>448</Checkbox>449<Checkbox450checked={props.other_settings.get("antd_animate", true)}451onChange={(e) => on_change("antd_animate", e.target.checked)}452>453<FormattedMessage454id="account.other-settings.theme.antd.animations"455defaultMessage={`<b>Animations</b>: briefly animate some aspects, e.g. buttons`}456/>457</Checkbox>458<Checkbox459checked={props.other_settings.get("antd_brandcolors", false)}460onChange={(e) => on_change("antd_brandcolors", e.target.checked)}461>462<FormattedMessage463id="account.other-settings.theme.antd.color_scheme"464defaultMessage={`<b>Color Scheme</b>: use brand colors instead of default colors`}465/>466</Checkbox>467<Checkbox468checked={props.other_settings.get("antd_compact", false)}469onChange={(e) => on_change("antd_compact", e.target.checked)}470>471<FormattedMessage472id="account.other-settings.theme.antd.compact"473defaultMessage={`<b>Compact Design</b>: use a more compact design`}474/>475</Checkbox>476</>477);478}479480function render_i18n_selector(): Rendered {481return (482<LabeledRow label={intl.formatMessage(labels.language)}>483<div>484<I18NSelector />{" "}485<HelpIcon title={intl.formatMessage(I18N_TITLE)}>486{intl.formatMessage(I18N_MESSAGE)}487</HelpIcon>488</div>489</LabeledRow>490);491}492493function render_vertical_fixed_bar_options(): Rendered {494const selected = getValidVBAROption(props.other_settings.get(VBAR_KEY));495const options = Object.fromEntries(496Object.entries(VBAR_OPTIONS).map(([k, v]) => [k, intl.formatMessage(v)]),497);498return (499<LabeledRow500label={intl.formatMessage({501id: "account.other-settings.vbar.title",502defaultMessage: "Vertical Project Bar",503})}504>505<div>506<SelectorInput507style={{ marginBottom: "10px" }}508selected={selected}509options={options}510on_change={(value) => {511on_change(VBAR_KEY, value);512track("flyout", { aspect: "layout", how: "account", value });513}}514/>515<Paragraph516type="secondary"517ellipsis={{ expandable: true, symbol: "more" }}518>519{intl.formatMessage(VBAR_EXPLANATION)}520</Paragraph>521</div>522</LabeledRow>523);524}525526function render_disable_all_llm(): Rendered {527return (528<Checkbox529checked={!!props.other_settings.get("openai_disabled")}530onChange={(e) => {531on_change("openai_disabled", e.target.checked);532redux.getStore("projects").clearOpenAICache();533}}534>535<FormattedMessage536id="account.other-settings.llm.disable_all"537defaultMessage={`<strong>Disable all AI integrations</strong>,538e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}539/>540</Checkbox>541);542}543544function render_language_model(): Rendered {545return (546<LabeledRow547label={intl.formatMessage({548id: "account.other-settings.llm.default_llm",549defaultMessage: "Default AI Language Model",550})}551>552<LLMSelector model={model} setModel={setModel} />553</LabeledRow>554);555}556557function render_llm_reply_language(): Rendered {558return (559<Checkbox560checked={!!props.other_settings.get(OTHER_SETTINGS_REPLY_ENGLISH_KEY)}561onChange={(e) => {562on_change(OTHER_SETTINGS_REPLY_ENGLISH_KEY, e.target.checked);563}}564>565<FormattedMessage566id="account.other-settings.llm.reply_language"567defaultMessage={`<strong>Always reply in English:</strong>568If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}569values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}570/>571</Checkbox>572);573}574575function render_custom_llm(): Rendered {576// on cocalc.com, do not even show that they're disabled577if (isCoCalcCom && !user_defined_llm) return;578return <UserDefinedLLMComponent on_change={on_change} />;579}580581function render_llm_settings() {582// we hide this panel, if all servers and user defined LLms are disabled583const customize = redux.getStore("customize");584const enabledLLMs = customize.getEnabledLLMs();585const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);586if (!anyLLMenabled) return;587return (588<Panel589header={590<>591<AIAvatar size={18} />{" "}592<FormattedMessage593id="account.other-settings.llm.title"594defaultMessage={`AI Settings`}595/>596</>597}598>599{render_disable_all_llm()}600{render_language_model()}601{render_llm_reply_language()}602{render_custom_llm()}603</Panel>604);605}606607if (props.other_settings == null) {608return <Loading />;609}610return (611<>612{render_llm_settings()}613614<Panel615header={616<>617<Icon name="highlighter" />{" "}618<FormattedMessage619id="account.other-settings.theme"620defaultMessage="Theme"621description="Visual UI theme of the application"622/>623</>624}625>626{render_dark_mode()}627{render_antd()}628</Panel>629630<Panel631header={632<>633<Icon name="gear" /> Other634</>635}636>637{render_confirm()}638{render_katex()}639{render_time_ago_absolute()}640{render_global_banner()}641{render_mask_files()}642{render_hide_project_popovers()}643{render_hide_file_popovers()}644{render_hide_button_tooltips()}645{render_no_free_warnings()}646<Checkbox647checked={!!props.other_settings.get("disable_markdown_codebar")}648onChange={(e) => {649on_change("disable_markdown_codebar", e.target.checked);650}}651>652<FormattedMessage653id="account.other-settings.markdown_codebar"654defaultMessage={`<strong>Disable the markdown code bar</strong> in all markdown documents.655Checking this hides the extra run, copy, and explain buttons in fenced code blocks.`}656/>657</Checkbox>658{render_i18n_selector()}659{render_vertical_fixed_bar_options()}660{render_new_filenames()}661{render_default_file_sort()}662{render_page_size()}663{render_standby_timeout()}664<div style={{ height: "10px" }} />665<Tours />666</Panel>667</>668);669}670671672