Skip to content

Commit 7085f85

Browse files
committed
lite: add easy way to configure AI api keys
1 parent cdedb9d commit 7085f85

File tree

4 files changed

+146
-9
lines changed

4 files changed

+146
-9
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { useEffect, useState } from "react";
2+
import { Alert, Button, Col, Input, Row, Space } from "antd";
3+
import { getLogger } from "@cocalc/frontend/logger";
4+
import { query } from "@cocalc/frontend/frame-editors/generic/client";
5+
import { Gap, Loading } from "@cocalc/frontend/components";
6+
7+
const log = getLogger("account:lite-ai-settings");
8+
9+
type ProviderKey = {
10+
keyField: string;
11+
enableField: string;
12+
label: string;
13+
placeholder?: string;
14+
};
15+
16+
const PROVIDERS: ProviderKey[] = [
17+
{
18+
keyField: "openai_api_key",
19+
enableField: "openai_enabled",
20+
label: "OpenAI API Key",
21+
placeholder: "sk-...",
22+
},
23+
{
24+
keyField: "google_vertexai_key",
25+
enableField: "google_vertexai_enabled",
26+
label: "Google Gemini API Key",
27+
placeholder: "Google AI Studio key",
28+
},
29+
{
30+
keyField: "mistral_api_key",
31+
enableField: "mistral_enabled",
32+
label: "Mistral API Key",
33+
},
34+
{
35+
keyField: "anthropic_api_key",
36+
enableField: "anthropic_enabled",
37+
label: "Anthropic API Key",
38+
},
39+
];
40+
41+
type State = "load" | "ready" | "save" | "error";
42+
43+
export default function LiteAISettings() {
44+
const [values, setValues] = useState<Record<string, string>>({});
45+
const [state, setState] = useState<State>("load");
46+
const [error, setError] = useState<string>("");
47+
48+
useEffect(() => {
49+
load();
50+
}, []);
51+
52+
async function load(): Promise<void> {
53+
setState("load");
54+
try {
55+
const result = await query({
56+
query: {
57+
site_settings: [{ name: null, value: null }],
58+
},
59+
});
60+
const next: Record<string, string> = {};
61+
for (const row of result.query.site_settings ?? []) {
62+
next[row.name] = row.value;
63+
}
64+
setValues(next);
65+
setError("");
66+
setState("ready");
67+
} catch (err) {
68+
log.info("failed to load llm settings", err);
69+
setError(`${err}`);
70+
setState("error");
71+
}
72+
}
73+
74+
function onChange(key: string, val: string) {
75+
setValues((cur) => ({ ...cur, [key]: val }));
76+
}
77+
78+
async function save(): Promise<void> {
79+
setState("save");
80+
try {
81+
for (const { keyField, enableField } of PROVIDERS) {
82+
const val = values[keyField] ?? "";
83+
await query({
84+
query: { server_settings: { name: keyField, value: val } },
85+
});
86+
await query({
87+
query: {
88+
site_settings: {
89+
name: enableField,
90+
value: val ? "yes" : "no",
91+
},
92+
},
93+
});
94+
}
95+
setState("ready");
96+
} catch (err) {
97+
log.info("failed to save llm settings", err);
98+
setError(`${err}`);
99+
setState("error");
100+
}
101+
}
102+
103+
return (
104+
<div>
105+
<h3>AI Provider Keys</h3>
106+
<p style={{ marginBottom: 12 }}>
107+
Enter API keys for the providers you want to use. When a key is saved,
108+
the corresponding AI UI is enabled automatically.
109+
</p>
110+
{error && (
111+
<Alert type="error" message="Error" description={error} closable />
112+
)}
113+
<Space direction="vertical" style={{ width: "100%" }} size="middle">
114+
{PROVIDERS.map(({ keyField, label, placeholder }) => (
115+
<Row key={keyField} gutter={8} align="middle">
116+
<Col span={8}>{label}</Col>
117+
<Col span={16}>
118+
<Input.Password
119+
allowClear
120+
value={values[keyField] ?? ""}
121+
placeholder={placeholder}
122+
onChange={(e) => onChange(keyField, e.target.value)}
123+
/>
124+
</Col>
125+
</Row>
126+
))}
127+
</Space>
128+
<Gap />
129+
<Button type="primary" onClick={save} disabled={state === "save"}>
130+
{state === "save" ? <Loading text="Saving" /> : "Save"}
131+
</Button>
132+
</div>
133+
);
134+
}

src/packages/frontend/account/other-settings.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { OTHER_SETTINGS_REPLY_ENGLISH_KEY } from "@cocalc/util/i18n/const";
4242
import Tours from "./tours";
4343
import { useLanguageModelSetting } from "./useLanguageModelSetting";
4444
import { UserDefinedLLMComponent } from "./user-defined-llm";
45+
import LiteAISettings from "./lite-ai-settings";
4546
import { lite } from "@cocalc/frontend/lite";
4647

4748
// Icon constants for account preferences sections
@@ -293,8 +294,8 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
293294
>
294295
<FormattedMessage
295296
id="account.other-settings.llm.disable_all"
296-
defaultMessage={`<strong>Disable all AI integrations</strong>,
297-
e.g., code generation or explanation buttons in Jupyter, @chatgpt mentions, etc.`}
297+
defaultMessage={`<strong>Disable all AI integrations</strong>:
298+
code generation, explanation buttons in Jupyter, @chatgpt mentions, etc.`}
298299
/>
299300
</Switch>
300301
);
@@ -324,14 +325,15 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
324325
<FormattedMessage
325326
id="account.other-settings.llm.reply_language"
326327
defaultMessage={`<strong>Always reply in English:</strong>
327-
If set, the replies are always in English. Otherwise, it replies in your language ({lang}).`}
328+
If set, the replies are always in English; otherwise, replies in your language ({lang}).`}
328329
values={{ lang: intl.formatMessage(LOCALIZATIONS[locale].trans) }}
329330
/>
330331
</Switch>
331332
);
332333
}
333334

334335
function render_custom_llm(): Rendered {
336+
if (lite) return;
335337
// on cocalc.com, do not even show that they're disabled
336338
if (isCoCalcCom && !user_defined_llm) return;
337339
return (
@@ -347,7 +349,7 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
347349
const customize = redux.getStore("customize");
348350
const enabledLLMs = customize.getEnabledLLMs();
349351
const anyLLMenabled = Object.values(enabledLLMs).some((v) => v);
350-
if (!anyLLMenabled) return <></>;
352+
if (!anyLLMenabled && !lite) return <></>;
351353
return (
352354
<Panel
353355
header={
@@ -360,10 +362,11 @@ export function OtherSettings(props: Readonly<Props>): React.JSX.Element {
360362
</>
361363
}
362364
>
363-
{render_disable_all_llm()}
364-
{render_language_model()}
365-
{render_llm_reply_language()}
366-
{render_custom_llm()}
365+
{anyLLMenabled && render_disable_all_llm()}
366+
{anyLLMenabled && render_llm_reply_language()}
367+
{anyLLMenabled && render_language_model()}
368+
{!lite && render_custom_llm()}
369+
{lite && <LiteAISettings />}
367370
</Panel>
368371
);
369372
}

src/packages/frontend/editors/markdown-input/mentionable-users.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {
4141
import { cmp, timestamp_cmp, trunc_middle } from "@cocalc/util/misc";
4242
import { CustomLLMPublic } from "@cocalc/util/types/llm";
4343
import { Item as CompleteItem } from "./complete";
44-
import { lite } from "@cocalc/frontend/lite";
4544

4645
// we make the show_llm_main_menu field required, to avoid forgetting to set it ;-)
4746
type Item = CompleteItem & Required<Pick<CompleteItem, "show_llm_main_menu">>;

src/packages/frontend/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { getLogger } from "@cocalc/conat/client";

0 commit comments

Comments
 (0)