CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/codemirror/extensions/ai-formula.tsx
Views: 687
1
import { Button, Descriptions, Divider, Input, Modal, Space } from "antd";
2
import { debounce } from "lodash";
3
import { useIntl } from "react-intl";
4
5
import { useLanguageModelSetting } from "@cocalc/frontend/account/useLanguageModelSetting";
6
import {
7
redux,
8
useAsyncEffect,
9
useEffect,
10
useState,
11
useTypedRedux,
12
} from "@cocalc/frontend/app-framework";
13
import { Localize, useLocalizationCtx } from "@cocalc/frontend/app/localize";
14
import type { Message } from "@cocalc/frontend/client/types";
15
import {
16
HelpIcon,
17
Icon,
18
Markdown,
19
Paragraph,
20
Text,
21
Title,
22
} from "@cocalc/frontend/components";
23
import AIAvatar from "@cocalc/frontend/components/ai-avatar";
24
import { LLMModelName } from "@cocalc/frontend/components/llm-name";
25
import LLMSelector from "@cocalc/frontend/frame-editors/llm/llm-selector";
26
import { dialogs } from "@cocalc/frontend/i18n";
27
import { show_react_modal } from "@cocalc/frontend/misc";
28
import { LLMCostEstimation } from "@cocalc/frontend/misc/llm-cost-estimation";
29
import track from "@cocalc/frontend/user-tracking";
30
import { webapp_client } from "@cocalc/frontend/webapp-client";
31
import { isFreeModel } from "@cocalc/util/db-schema/llm-utils";
32
import { Locale } from "@cocalc/util/i18n";
33
import { unreachable } from "@cocalc/util/misc";
34
35
type Mode = "tex" | "md";
36
37
const LLM_USAGE_TAG = `generate-formula`;
38
39
interface Opts {
40
mode: Mode;
41
text?: string;
42
project_id: string;
43
locale?: Locale;
44
}
45
46
export async function ai_gen_formula({
47
mode,
48
text = "",
49
project_id,
50
locale,
51
}: Opts): Promise<string> {
52
return await show_react_modal((cb) => (
53
<Localize>
54
<AiGenFormula
55
mode={mode}
56
text={text}
57
project_id={project_id}
58
locale={locale}
59
cb={cb}
60
/>
61
</Localize>
62
));
63
}
64
65
interface Props extends Opts {
66
cb: (err?: string, result?: string) => void;
67
}
68
69
function AiGenFormula({ mode, text = "", project_id, locale, cb }: Props) {
70
const intl = useIntl();
71
const { setLocale } = useLocalizationCtx();
72
const is_cocalc_com = useTypedRedux("customize", "is_cocalc_com");
73
const [model, setModel] = useLanguageModelSetting(project_id);
74
const [input, setInput] = useState<string>(text);
75
const [formula, setFormula] = useState<string>("");
76
const [fullReply, setFullReply] = useState<string>("");
77
const [generating, setGenerating] = useState<boolean>(false);
78
const [error, setError] = useState<string | undefined>(undefined);
79
const [tokens, setTokens] = useState<number>(0);
80
81
useEffect(() => {
82
if (typeof locale === "string") {
83
setLocale(locale);
84
}
85
}, [locale]);
86
87
useAsyncEffect(
88
debounce(
89
async () => {
90
const { input, history, system } = getPrompt() ?? "";
91
// compute the number of tokens (this MUST be a lazy import):
92
const { getMaxTokens, numTokensUpperBound } = await import(
93
"@cocalc/frontend/misc/llm"
94
);
95
96
const all = [
97
input,
98
history.map(({ content }) => content).join(" "),
99
system,
100
].join(" ");
101
setTokens(numTokensUpperBound(all, getMaxTokens(model)));
102
},
103
1000,
104
{ leading: true, trailing: true },
105
),
106
107
[model, input],
108
);
109
110
const enabled = redux
111
.getStore("projects")
112
.hasLanguageModelEnabled(project_id, LLM_USAGE_TAG);
113
114
function getSystemPrompt(): string {
115
const p1 = `Typset the plain-text description of a mathematical formula as a LaTeX formula. The formula will be`;
116
const p2 = `Return only the LaTeX formula, ready to be inserted into the document. Do not add any explanations.`;
117
switch (mode) {
118
case "tex":
119
return `${p1} in a *.tex file. Assume the package "amsmath" is available. ${p2}`;
120
case "md":
121
return `${p1} in a markdown file. Formulas are inside of $ or $$. ${p2}`;
122
default:
123
unreachable(mode);
124
return p1;
125
}
126
}
127
128
function getPrompt(): { input: string; history: Message[]; system: string } {
129
const system = getSystemPrompt();
130
// 3-shot examples
131
const history: Message[] = [
132
{ role: "user", content: "equation e^(i pi) = -1" },
133
{ role: "assistant", content: "$$e^{i \\pi} = -1$$" },
134
{
135
role: "user",
136
content: "integral 0 to 2 pi sin(x)^2",
137
},
138
{
139
role: "assistant",
140
content: "$\\int_{0}^{2\\pi} \\sin(x)^2 \\, \\mathrm{d}x$",
141
},
142
{
143
role: "user",
144
content: "equation system: [ 1 + x^2 = a, 1 - y^2 = ln(a) ]",
145
},
146
{
147
role: "assistant",
148
content:
149
"\\begin{cases}\n1 + x^2 = a \\\n1 - y^2 = \\ln(a)\n\\end{cases}",
150
},
151
];
152
return { input: input || text, system, history };
153
}
154
155
function wrapFormula(tex: string = "") {
156
// wrap single-line formulas in $...$
157
// if it is multiline, wrap in \begin{equation}...\end{equation}
158
// but only wrap if actually necessary
159
tex = tex.trim();
160
if (tex.split("\n").length > 1) {
161
if (tex.includes("\\begin{")) {
162
return tex;
163
} else if (tex.startsWith("$$") && tex.endsWith("$$")) {
164
return tex;
165
} else {
166
return `\\begin{equation}\n${tex}\n\\end{equation}`;
167
}
168
} else {
169
if (tex.startsWith("$") && tex.endsWith("$")) {
170
return tex;
171
} else if (tex.startsWith("\\(") && tex.endsWith("\\)")) {
172
return tex;
173
} else {
174
return `$${tex}$`;
175
}
176
}
177
}
178
179
function processFormula(formula: string): string {
180
let tex = "";
181
// iterate over all lines in formula. save everything between the first ``` and last ``` in tex
182
let inCode = false;
183
for (const line of formula.split("\n")) {
184
if (line.startsWith("```")) {
185
inCode = !inCode;
186
} else if (inCode) {
187
tex += line + "\n";
188
}
189
}
190
// we found nothing -> the entire formula string is the tex code
191
if (!tex) {
192
tex = formula;
193
}
194
// if there are "\[" and "\]" in the formula, replace both by $$
195
if (tex.includes("\\[") && tex.includes("\\]")) {
196
tex = tex.replace(/\\\[|\\\]/g, "$$");
197
}
198
// similar, replace "\(" and "\)" by single $ signs
199
if (tex.includes("\\(") && tex.includes("\\)")) {
200
tex = tex.replace(/\\\(|\\\)/g, "$");
201
}
202
// if there are at least two $$ or $ in the tex, we extract the part between the first and second $ or $$
203
// This is necessary, because despite the prompt, some LLM return stuff like: "Here is the LaTeX formula: $$ ... $$."
204
for (const delimiter of ["$$", "$"]) {
205
const parts = tex.split(delimiter);
206
if (parts.length >= 3) {
207
tex = parts[1];
208
break;
209
}
210
}
211
setFormula(tex);
212
return tex;
213
}
214
215
async function doGenerate() {
216
try {
217
setError(undefined);
218
setGenerating(true);
219
setFormula("");
220
setFullReply("");
221
track("chatgpt", {
222
project_id,
223
tag: LLM_USAGE_TAG,
224
mode,
225
type: "generate",
226
model,
227
});
228
const { system, input, history } = getPrompt();
229
const reply = await webapp_client.openai_client.query({
230
input,
231
history,
232
system,
233
model,
234
project_id,
235
tag: LLM_USAGE_TAG,
236
});
237
const tex = processFormula(reply);
238
// significant differece? Also show the full reply
239
if (reply.length > 2 * tex.length) {
240
setFullReply(reply);
241
} else {
242
setFullReply("");
243
}
244
} catch (err) {
245
setError(err.message || err.toString());
246
} finally {
247
setGenerating(false);
248
}
249
}
250
251
// Start the query immediately, if the user had selected some text … and it's a free model
252
useEffect(() => {
253
if (text && isFreeModel(model, is_cocalc_com)) {
254
doGenerate();
255
}
256
}, [text]);
257
258
function renderTitle() {
259
return (
260
<>
261
<Title level={4}>
262
<AIAvatar size={20} /> Generate LaTeX Formula
263
</Title>
264
{enabled ? (
265
<>
266
{intl.formatMessage(dialogs.select_llm)}:{" "}
267
<LLMSelector
268
project_id={project_id}
269
model={model}
270
setModel={setModel}
271
/>
272
</>
273
) : undefined}
274
</>
275
);
276
}
277
278
function renderContent() {
279
const help = (
280
<HelpIcon title="Usage" extra="Help">
281
<Paragraph>
282
You can enter the description of your desired formula in various ways:
283
<ul>
284
<li>
285
natural language: <Text code>drake equation</Text>,
286
</li>
287
<li>
288
simple algebraic notation:{" "}
289
<Text code>(a+b)^2 = a^2 + 2 a b + b^2</Text>,
290
</li>
291
<li>
292
or a combination of both:{" "}
293
<Text code>integral from 0 to infinity of (1+sin(x))/x^2 dx</Text>
294
.
295
</li>
296
</ul>
297
</Paragraph>
298
<Paragraph>
299
If the formula is not quite right, click "Geneate" once again, try a
300
different language model, or adjust the description. Of course, you
301
can also edit it as usual after you have inserted it.
302
</Paragraph>
303
<Paragraph>
304
Once you're happy, click the "Insert formula" button and the generated
305
LaTeX formula will be inserted at the current cursor position. The
306
"Insert fully reply" button will, well, insert the entire answer.
307
</Paragraph>
308
<Paragraph>
309
Prior to opening this dialog, you can even select a portion of your
310
text. This will be used as your description and the AI language model
311
will be queried immediately. Inserting the formula will then replace
312
the selected text.
313
</Paragraph>
314
</HelpIcon>
315
);
316
return (
317
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
318
<Paragraph style={{ marginBottom: 0 }}>
319
The <LLMModelName model={model} size={18} /> language model will
320
generate a LaTeX formula based on your description. {help}
321
</Paragraph>
322
<div style={{ textAlign: "right" }}>
323
<LLMCostEstimation
324
// limited to 200, since we only get a formula – which is not a lengthy text!
325
maxOutputTokens={200}
326
model={model}
327
tokens={tokens}
328
type="secondary"
329
/>
330
</div>
331
<Space.Compact style={{ width: "100%" }}>
332
<Input
333
allowClear
334
disabled={generating}
335
placeholder={
336
"Describe the formula in natural language and/or algebraic notation."
337
}
338
defaultValue={text}
339
onChange={(e) => setInput(e.target.value)}
340
onPressEnter={doGenerate}
341
addonBefore={<Icon name="fx" />}
342
/>
343
<Button
344
disabled={!input.trim() || generating}
345
loading={generating}
346
onClick={doGenerate}
347
type={formula ? "default" : "primary"}
348
>
349
Generate
350
</Button>
351
</Space.Compact>
352
{formula ? (
353
<Descriptions
354
size={"small"}
355
column={1}
356
bordered
357
items={[
358
{
359
key: "1",
360
label: "LaTeX",
361
children: <Paragraph code>{formula}</Paragraph>,
362
},
363
{
364
key: "2",
365
label: "Preview",
366
children: <Markdown value={wrapFormula(formula)} />,
367
},
368
...(fullReply
369
? [
370
{
371
key: "3",
372
label: "Full reply",
373
children: <Markdown value={fullReply} />,
374
},
375
]
376
: []),
377
]}
378
/>
379
) : undefined}
380
{error ? <Paragraph type="danger">{error}</Paragraph> : undefined}
381
{mode === "tex" ? (
382
<>
383
<Divider />
384
<Paragraph type="secondary">
385
Note: You might have to ensure that{" "}
386
<code>{"\\usepackage{amsmath}"}</code> is loaded in the preamble.
387
</Paragraph>
388
</>
389
) : undefined}
390
</Space>
391
);
392
}
393
394
function renderButtons() {
395
return (
396
<div>
397
<Button onClick={onCancel}>Cancel</Button>
398
<Button
399
type={"default"}
400
disabled={!fullReply}
401
onClick={() => cb(undefined, `\n\n${fullReply}\n\n`)}
402
>
403
Insert full reply
404
</Button>
405
<Button
406
type={formula ? "primary" : "default"}
407
disabled={!formula}
408
onClick={() => cb(undefined, wrapFormula(formula))}
409
>
410
Insert formula
411
</Button>
412
</div>
413
);
414
}
415
416
function renderBody() {
417
if (!enabled) {
418
return <div>AI language models are disabled.</div>;
419
}
420
return renderContent();
421
}
422
423
function onCancel() {
424
cb(undefined, text);
425
}
426
427
return (
428
<Modal
429
title={renderTitle()}
430
open
431
footer={renderButtons()}
432
onCancel={onCancel}
433
centered
434
width={"70vw"}
435
>
436
{renderBody()}
437
</Modal>
438
);
439
}
440
441