Path: blob/master/src/packages/frontend/account/other-settings.tsx
6041 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// cSpell:ignore brandcolors codebar67import { FormattedMessage, useIntl } from "react-intl";89import { Panel, Switch } from "@cocalc/frontend/antd-bootstrap";10import { redux, Rendered, useTypedRedux } from "@cocalc/frontend/app-framework";11import { useLocalizationCtx } from "@cocalc/frontend/app/localize";12import {13Icon,14IconName,15LabeledRow,16Loading,17NumberInput,18Paragraph,19SelectorInput,20Text,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 { labels, LOCALIZATIONS } from "@cocalc/frontend/i18n";26import { getValidActivityBarOption } from "@cocalc/frontend/project/page/activity-bar";27import {28ACTIVITY_BAR_EXPLANATION,29ACTIVITY_BAR_KEY,30ACTIVITY_BAR_LABELS,31ACTIVITY_BAR_LABELS_DEFAULT,32ACTIVITY_BAR_OPTIONS,33ACTIVITY_BAR_TITLE,34ACTIVITY_BAR_TOGGLE_LABELS,35ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION,36} from "@cocalc/frontend/project/page/activity-bar-consts";37import { NewFilenameFamilies } from "@cocalc/frontend/project/utils";38import track from "@cocalc/frontend/user-tracking";39import { DEFAULT_NEW_FILENAMES, NEW_FILENAMES } from "@cocalc/util/db-schema";40import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";4142import Tours from "./tours";43import { useLanguageModelSetting } from "./useLanguageModelSetting";44import { UserDefinedLLMComponent } from "./user-defined-llm";4546// Icon constants for account preferences sections47export const THEME_ICON_NAME: IconName = "highlighter";48export const OTHER_ICON_NAME: IconName = "gear";4950// Import the account state type to get the proper other_settings type51import type { AccountState } from "./types";5253interface Props {54other_settings: AccountState["other_settings"];55is_stripe_customer: boolean;56kucalc: string;57mode: "appearance" | "ai" | "other";58}5960export function OtherSettings(props: Readonly<Props>): React.JSX.Element {61const intl = useIntl();62const { locale } = useLocalizationCtx();63const isCoCalcCom = useTypedRedux("customize", "is_cocalc_com");64const user_defined_llm = useTypedRedux("customize", "user_defined_llm");6566const [model, setModel] = useLanguageModelSetting();6768function on_change(name: string, value: any): void {69redux.getActions("account").set_other_settings(name, value);70}7172// private render_first_steps(): Rendered {73// if (props.kucalc !== KUCALC_COCALC_COM) return;74// return (75// <Switch76// checked={!!props.other_settings.get("first_steps")}77// onChange={(e) => on_change("first_steps", e.target.checked)}78// >79// Offer the First Steps guide80// </Switch>81// );82// }8384function render_confirm(): Rendered {85if (!IS_MOBILE) {86return (87<Switch88checked={!!props.other_settings.get("confirm_close")}89onChange={(e) => on_change("confirm_close", e.target.checked)}90>91<FormattedMessage92id="account.other-settings.confirm_close"93defaultMessage={`<strong>Confirm Close:</strong> always ask for confirmation before94closing the browser window`}95/>96</Switch>97);98}99}100101function render_standby_timeout(): Rendered {102if (IS_TOUCH) {103return;104}105return (106<LabeledRow107label={intl.formatMessage({108id: "account.other-settings.standby_timeout",109defaultMessage: "Standby timeout",110})}111>112<NumberInput113on_change={(n) => on_change("standby_timeout_m", n)}114min={1}115max={180}116unit="minutes"117number={props.other_settings.get("standby_timeout_m") ?? 30}118/>119</LabeledRow>120);121}122123function render_mask_files(): Rendered {124return (125<Switch126checked={!!props.other_settings.get("mask_files")}127onChange={(e) => on_change("mask_files", e.target.checked)}128>129<FormattedMessage130id="account.other-settings.mask_files"131defaultMessage={`<strong>Dim generated files:</strong> gray out files produced by compilers (.aux, .log, .pyc, etc.) so the main files stand out.`}132/>133</Switch>134);135}136137function render_default_file_sort(): Rendered {138return (139<LabeledRow140label={intl.formatMessage({141id: "account.other-settings.default_file_sort.label",142defaultMessage: "Default file sort",143})}144>145<SelectorInput146selected={props.other_settings.get("default_file_sort")}147options={{148time: intl.formatMessage({149id: "account.other-settings.default_file_sort.by_time",150defaultMessage: "Sort by time",151}),152name: intl.formatMessage({153id: "account.other-settings.default_file_sort.by_name",154defaultMessage: "Sort by name",155}),156}}157on_change={(value) => on_change("default_file_sort", value)}158/>159</LabeledRow>160);161}162163function render_new_filenames(): Rendered {164const selected =165props.other_settings.get(NEW_FILENAMES) ?? DEFAULT_NEW_FILENAMES;166return (167<LabeledRow168label={intl.formatMessage({169id: "account.other-settings.filename_generator.label",170defaultMessage: "Filename generator",171})}172>173<div>174<SelectorInput175selected={selected}176options={NewFilenameFamilies}177on_change={(value) => on_change(NEW_FILENAMES, value)}178/>179<Paragraph180type="secondary"181ellipsis={{ expandable: true, symbol: "more" }}182>183{intl.formatMessage({184id: "account.other-settings.filename_generator.description",185defaultMessage: `Select how automatically generated filenames are generated.186In particular, to make them unique or to include the current time.`,187})}188</Paragraph>189</div>190</LabeledRow>191);192}193194function render_page_size(): Rendered {195return (196<LabeledRow197label={intl.formatMessage({198id: "account.other-settings._page_size.label",199defaultMessage: "Number of files per page",200})}201>202<NumberInput203on_change={(n) => on_change("page_size", n)}204min={1}205max={10000}206number={props.other_settings.get("page_size") ?? 50}207/>208</LabeledRow>209);210}211212function render_dim_file_extensions(): Rendered {213return (214<Switch215checked={!!props.other_settings.get("dim_file_extensions")}216onChange={(e) => on_change("dim_file_extensions", e.target.checked)}217>218<FormattedMessage219id="account.other-settings.dim_file_extensions"220defaultMessage={`<strong>Dim file extensions:</strong> gray out file extensions so their names stand out.`}221/>222</Switch>223);224}225226function render_antd(): Rendered {227return (228<>229<Switch230checked={props.other_settings.get("antd_rounded", true)}231onChange={(e) => on_change("antd_rounded", e.target.checked)}232>233<FormattedMessage234id="account.other-settings.theme.antd.rounded"235defaultMessage={`<b>Rounded Design</b>: use rounded corners for buttons, etc.`}236/>237</Switch>238<Switch239checked={props.other_settings.get("antd_animate", true)}240onChange={(e) => on_change("antd_animate", e.target.checked)}241>242<FormattedMessage243id="account.other-settings.theme.antd.animations"244defaultMessage={`<b>Animations</b>: briefly animate some aspects, e.g. buttons`}245/>246</Switch>247<Switch248checked={props.other_settings.get("antd_brandcolors", false)}249onChange={(e) => on_change("antd_brandcolors", e.target.checked)}250>251<FormattedMessage252id="account.other-settings.theme.antd.color_scheme"253defaultMessage={`<b>Color Scheme</b>: use brand colors instead of default colors`}254/>255</Switch>256<Switch257checked={props.other_settings.get("antd_compact", false)}258onChange={(e) => on_change("antd_compact", e.target.checked)}259>260<FormattedMessage261id="account.other-settings.theme.antd.compact"262defaultMessage={`<b>Compact Design</b>: use a more compact design`}263/>264</Switch>265</>266);267}268269function render_vertical_fixed_bar_options(): Rendered {270const selected = getValidActivityBarOption(271props.other_settings.get(ACTIVITY_BAR_KEY),272);273const options = Object.fromEntries(274Object.entries(ACTIVITY_BAR_OPTIONS).map(([k, v]) => [275k,276intl.formatMessage(v),277]),278);279return (280<LabeledRow label={intl.formatMessage(ACTIVITY_BAR_TITLE)}>281<div>282<SelectorInput283style={{ marginBottom: "10px" }}284selected={selected}285options={options}286on_change={(value) => {287on_change(ACTIVITY_BAR_KEY, value);288track("flyout", { aspect: "layout", how: "account", value });289}}290/>291<Paragraph292type="secondary"293ellipsis={{ expandable: true, symbol: "more" }}294>295{intl.formatMessage(ACTIVITY_BAR_EXPLANATION)}296</Paragraph>297<Switch298checked={299props.other_settings.get(ACTIVITY_BAR_LABELS) ??300ACTIVITY_BAR_LABELS_DEFAULT301}302onChange={(e) => {303on_change(ACTIVITY_BAR_LABELS, e.target.checked);304}}305>306<Paragraph307type="secondary"308style={{ marginBottom: 0 }}309ellipsis={{ expandable: true, symbol: "more" }}310>311<Text strong>312{intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS, {313show: false,314})}315</Text>316: {intl.formatMessage(ACTIVITY_BAR_TOGGLE_LABELS_DESCRIPTION)}317</Paragraph>318</Switch>319</div>320</LabeledRow>321);322}323324function render_disable_all_llm(): Rendered {325return (326<Switch327checked={!!props.other_settings.get("openai_disabled")}328onChange={(e) => {329on_change("openai_disabled", e.target.checked);330redux.getStore("projects").clearOpenAICache();331}}332>333<FormattedMessage334id="account.other-settings.llm.disable_all"335defaultMessage={`<strong>Disable all AI integrations</strong>,336e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}337/>338</Switch>339);340}341342function render_language_model(): Rendered {343return (344<LabeledRow345label={intl.formatMessage({346id: "account.other-settings.llm.default_llm",347defaultMessage: "Default AI Model",348})}349>350<LLMSelector model={model} setModel={setModel} />351</LabeledRow>352);353}354355function render_llm_reply_language(): Rendered {356return (357<Switch358checked={!!props.other_settings.get(OTHER_SETTINGS_REPLY_ENGLISH_KEY)}359onChange={(e) => {360on_change(OTHER_SETTINGS_REPLY_ENGLISH_KEY, e.target.checked);361}}362>363<FormattedMessage364id="account.other-settings.llm.reply_language"365defaultMessage={`<strong>Always reply in English:</strong>366If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}367values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}368/>369</Switch>370);371}372373function render_custom_llm(): Rendered {374// on cocalc.com, do not even show that they're disabled375if (isCoCalcCom && !user_defined_llm) return;376return (377<UserDefinedLLMComponent378on_change={on_change}379style={{ marginTop: "20px" }}380/>381);382}383384function render_llm_settings() {385// we hide this panel, if all servers and user defined LLms are disabled386const customize = redux.getStore("customize");387const enabledLLMs = customize.getEnabledLLMs();388const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);389if (!anyLLMenabled) return <></>;390return (391<Panel392header={393<>394<AIAvatar size={18} />{" "}395<FormattedMessage396id="account.other-settings.llm.title"397defaultMessage={`AI Settings`}398/>399</>400}401>402{render_disable_all_llm()}403{render_language_model()}404{render_llm_reply_language()}405{render_custom_llm()}406</Panel>407);408}409410if (props.other_settings == null) {411return <Loading />;412}413414const mode = props.mode ?? "full";415416if (mode === "ai") {417return render_llm_settings();418}419420if (mode === "appearance") {421return (422<Panel423size="small"424header={425<>426<Icon name={THEME_ICON_NAME} /> {intl.formatMessage(labels.theme)}427</>428}429>430{render_antd()}431</Panel>432);433}434435if (mode === "other") {436return (437<>438<Panel439size="small"440header={441<>442<Icon name="desktop" /> {intl.formatMessage(labels.browser)}443</>444}445>446{render_confirm()}447{render_standby_timeout()}448</Panel>449450<Panel451size="small"452header={453<>454<Icon name="folder-open" />{" "}455{intl.formatMessage(labels.file_explorer)}456</>457}458>459{render_dim_file_extensions()}460{render_mask_files()}461{render_default_file_sort()}462{render_page_size()}463{render_new_filenames()}464</Panel>465466{/* Projects */}467<Panel468size="small"469header={470<>471<Icon name="edit" /> {intl.formatMessage(labels.projects)}472</>473}474>475{render_vertical_fixed_bar_options()}476</Panel>477478{/* Tours at bottom */}479<Tours />480</>481);482}483484// mode === "full" no longer exists485unreachable(mode);486return <></>;487}488489import { unreachable } from "@cocalc/util/misc";490import UseBalanceTowardSubscriptions from "./balance-toward-subs";491492export function UseBalance({ style, minimal }: { style?; minimal? }) {493const use_balance_toward_subscriptions = useTypedRedux(494"account",495"other_settings",496)?.get("use_balance_toward_subscriptions");497498return (499<UseBalanceTowardSubscriptions500minimal={minimal}501style={style}502use_balance_toward_subscriptions={use_balance_toward_subscriptions}503set_use_balance_toward_subscriptions={(value) => {504const actions = redux.getActions("account");505actions.set_other_settings("use_balance_toward_subscriptions", value);506}}507/>508);509}510511512