Skip to content

Commit d4d58a5

Browse files
authored
Merge pull request #8537 from sagemathinc/help-button-hint
frontend/llm: hint and solution "help me fix" buttons
2 parents bfdd868 + 0c7e230 commit d4d58a5

29 files changed

+482
-244
lines changed

src/.claude/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"Bash(../node_modules/.bin/tsc:*)",
55
"Bash(NODE_OPTIONS=--max-old-space-size=8192 ../node_modules/.bin/tsc --noEmit)",
66
"Bash(bash:*)",
7-
"Bash(curl:*)",
7+
"Bash(chmod:*)",
88
"Bash(curl:*)",
99
"Bash(docker run:*)",
1010
"Bash(find:*)",
@@ -24,9 +24,11 @@
2424
"Bash(pnpm list:*)",
2525
"Bash(pnpm ts-build:*)",
2626
"Bash(pnpm tsc:*)",
27+
"Bash(pnpm update:*)",
2728
"Bash(pnpm why:*)",
2829
"Bash(prettier -w:*)",
2930
"Bash(psql:*)",
31+
"Bash(python3:*)",
3032
"WebFetch(domain:cocalc.com)",
3133
"WebFetch(domain:doc.cocalc.com)",
3234
"WebFetch(domain:docs.anthropic.com)",
@@ -36,6 +38,7 @@
3638
"WebFetch(domain:www.anthropic.com)",
3739
"WebSearch",
3840
"mcp__cclsp__find_definition",
41+
"mcp__cclsp__find_references",
3942
"mcp__github__get_issue",
4043
"mcp__github__get_pull_request_comments",
4144
"mcp__github__get_pull_request",

src/packages/frontend/course/configuration/customize-student-project-functionality.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ const OPTIONS: Option[] = [
227227
description: defineMessage({
228228
id: "course.customize-student-project-functionality.disableSomeChatGPT.description",
229229
defaultMessage:
230-
"Disable AI integration (ChatGPT & co.) except that 'Help me fix' and 'Explain' buttons. Use this if you only want the students to use AI assistance to get unstuck.",
230+
"Disable AI integration (ChatGPT & co.) except for 'Hint', 'Explain' buttons, and chat replies. Students can get hints to help them get unstuck, but cannot get complete solutions from 'Help me fix'.",
231231
}),
232232
},
233233
{
@@ -406,7 +406,7 @@ export const useStudentProjectFunctionality: Hook = (project_id?: string) => {
406406
return state;
407407
};
408408

409-
// Getting the information known right now about studnet project functionality.
409+
// Getting the information known right now about student project functionality.
410410
// Similar to the above hook, but just a point in time snapshot. Use this
411411
// for old components that haven't been converted to react hooks yet.
412412
export function getStudentProjectFunctionality(

src/packages/frontend/cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"syncstring",
5050
"synctable",
5151
"synctables",
52+
"synctex",
5253
"tidymodels",
5354
"tidyverse",
5455
"timetravel",

src/packages/frontend/frame-editors/frame-tree/format-error.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// A dismissable error message that appears when formatting code.
1+
// A dismissible error message that appears when formatting code.
22

33
import { Alert, Button } from "antd";
44
import { useMemo } from "react";

src/packages/frontend/frame-editors/latex-editor/errors-and-warnings.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,8 @@ const Item: React.FC<ItemProps> = React.memo(
143143
style={{ float: "right", marginLeft: "10px" }}
144144
size="small"
145145
task={"ran latex"}
146-
error={
147-
(item.get("message") ?? "") + "\n" + (item.get("content") ?? "")
148-
}
146+
error={item.get("message") ?? ""}
147+
line={item.get("content") ?? ""}
149148
input={() => {
150149
const s = actions._syncstring.to_str();
151150
const v = s

src/packages/frontend/frame-editors/latex-editor/gutters.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ function Component({
8787
size="small"
8888
style={{ marginTop: "5px" }}
8989
task={"ran latex"}
90-
error={content}
90+
error={message}
91+
line={content}
9192
input={() => {
9293
const s = actions._syncstring.to_str();
9394
const v = s
@@ -100,7 +101,7 @@ function Component({
100101
language={"latex"}
101102
extraFileInfo={actions.languageModelExtraFileInfo()}
102103
tag={"latex-error-popover"}
103-
prioritize="end"
104+
prioritize="start-end"
104105
/>
105106
</>
106107
)}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Button, Space } from "antd";
2+
import React from "react";
3+
import { defineMessage, useIntl } from "react-intl";
4+
5+
import { AIAvatar, RawPrompt } from "@cocalc/frontend/components";
6+
import { Icon } from "@cocalc/frontend/components/icon";
7+
import PopconfirmKeyboard from "@cocalc/frontend/components/popconfirm-keyboard";
8+
import { LLMCostEstimation } from "@cocalc/frontend/misc/llm-cost-estimation";
9+
import LLMSelector, { modelToName } from "./llm-selector";
10+
11+
const messages = {
12+
buttonText: defineMessage({
13+
id: "frame-editors.llm.help-me-fix-button.button-text",
14+
defaultMessage:
15+
"{isHint, select, true {Give me a Hint...} other {Fix this Problem...}}",
16+
description:
17+
"Button text for help-me-fix functionality - hint vs complete solution",
18+
}),
19+
okText: defineMessage({
20+
id: "frame-editors.llm.help-me-fix-button.ok-text",
21+
defaultMessage:
22+
"{isHint, select, true {Get Hint [Return]} other {Get Solution [Return]}}",
23+
description:
24+
"Confirmation button text in help-me-fix dialog - hint vs complete solution",
25+
}),
26+
title: defineMessage({
27+
id: "frame-editors.llm.help-me-fix-button.title",
28+
defaultMessage:
29+
"{isHint, select, true {Get Hint from} other {Get Complete Solution from}}",
30+
description: "Title text in help-me-fix dialog - hint vs complete solution",
31+
}),
32+
};
33+
34+
interface HelpMeFixButtonProps {
35+
mode: "hint" | "solution";
36+
model: string;
37+
setModel: (model: string) => void;
38+
project_id: string;
39+
inputText: string;
40+
tokens: number;
41+
size?: any;
42+
style?: React.CSSProperties;
43+
gettingHelp: boolean;
44+
onConfirm: () => void;
45+
}
46+
47+
export default function HelpMeFixButton({
48+
mode,
49+
model,
50+
setModel,
51+
project_id,
52+
inputText,
53+
tokens,
54+
size,
55+
style,
56+
gettingHelp,
57+
onConfirm,
58+
}: HelpMeFixButtonProps) {
59+
const intl = useIntl();
60+
const isHint = mode === "hint";
61+
const title = intl.formatMessage(messages.title, { isHint });
62+
const buttonText = intl.formatMessage(messages.buttonText, { isHint });
63+
const okText = intl.formatMessage(messages.okText, { isHint });
64+
const buttonIcon = isHint ? "lightbulb" : "wrench";
65+
const okIcon = isHint ? "lightbulb" : "paper-plane";
66+
67+
return (
68+
<PopconfirmKeyboard
69+
icon={<AIAvatar size={20} />}
70+
title={
71+
<>
72+
{title}{" "}
73+
<LLMSelector
74+
model={model}
75+
setModel={setModel}
76+
project_id={project_id}
77+
/>
78+
</>
79+
}
80+
description={() => (
81+
<div
82+
style={{
83+
width: "550px",
84+
overflow: "auto",
85+
maxWidth: "90vw",
86+
maxHeight: "400px",
87+
}}
88+
>
89+
The following will be sent to {modelToName(model)}:
90+
<RawPrompt input={inputText} />
91+
<LLMCostEstimation
92+
model={model}
93+
tokens={tokens}
94+
type="secondary"
95+
paragraph
96+
/>
97+
</div>
98+
)}
99+
okText={
100+
<>
101+
<Icon name={okIcon} /> {okText}
102+
</>
103+
}
104+
onConfirm={onConfirm}
105+
>
106+
<Button size={size} style={style} disabled={gettingHelp}>
107+
<Space>
108+
<Icon name={buttonIcon} />
109+
{buttonText}
110+
</Space>
111+
</Button>
112+
</PopconfirmKeyboard>
113+
);
114+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import getChatActions from "@cocalc/frontend/chat/get-actions";
2+
import { backtickSequence } from "@cocalc/frontend/markdown/util";
3+
import { trunc, trunc_left, trunc_middle } from "@cocalc/util/misc";
4+
import { CUTOFF } from "./consts";
5+
import { modelToMention } from "./llm-selector";
6+
import shortenError from "./shorten-error";
7+
8+
export interface GetHelpOptions {
9+
project_id: string;
10+
path: string;
11+
tag?: string;
12+
error: string;
13+
input?: string;
14+
task?: string;
15+
line?: string;
16+
language?: string;
17+
extraFileInfo?: string;
18+
redux: any;
19+
prioritize?: "start" | "start-end" | "end";
20+
model: string;
21+
}
22+
23+
export interface CreateMessageOpts {
24+
tag?: string;
25+
error: string;
26+
line: string;
27+
input?: string;
28+
task?: string;
29+
language?: string;
30+
extraFileInfo?: string;
31+
prioritize?: "start" | "start-end" | "end";
32+
model: string;
33+
open: boolean;
34+
full: boolean;
35+
isHint?: boolean;
36+
}
37+
38+
export async function getHelp(options: GetHelpOptions) {
39+
const {
40+
project_id,
41+
path,
42+
tag,
43+
line = "",
44+
error,
45+
input,
46+
task,
47+
language,
48+
extraFileInfo,
49+
redux,
50+
prioritize,
51+
model,
52+
} = options;
53+
54+
const solutionText = createMessage({
55+
error,
56+
task,
57+
line,
58+
input,
59+
language,
60+
extraFileInfo,
61+
model,
62+
prioritize,
63+
open: false,
64+
full: false,
65+
isHint: false,
66+
});
67+
68+
try {
69+
const actions = await getChatActions(redux, project_id, path);
70+
setTimeout(() => actions.scrollToBottom(), 100);
71+
await actions.sendChat({
72+
input: solutionText,
73+
tag: `help-me-fix-solution${tag ? `:${tag}` : ""}`,
74+
noNotification: true,
75+
});
76+
} catch (err) {
77+
console.error("Error getting help:", err);
78+
throw err;
79+
}
80+
}
81+
82+
export function createMessage({
83+
error,
84+
line,
85+
language,
86+
input,
87+
model,
88+
task,
89+
extraFileInfo,
90+
prioritize,
91+
open,
92+
full,
93+
isHint = false,
94+
}: CreateMessageOpts): string {
95+
const message: string[] = [];
96+
const prefix = full ? modelToMention(model) + " " : "";
97+
if (isHint) {
98+
message.push(
99+
`${prefix}Please give me a hint to help me fix my code. Do not provide the complete solution - just point me in the right direction.`,
100+
);
101+
} else {
102+
message.push(`${prefix}Help me fix my code.`);
103+
}
104+
105+
if (full)
106+
message.push(`<details${open ? " open" : ""}><summary>Context</summary>`);
107+
108+
if (task) {
109+
message.push(`I ${task}.`);
110+
}
111+
112+
error = trimStr(error, language);
113+
line = trimStr(line, language);
114+
115+
message.push(`I received the following error:`);
116+
const delimE = backtickSequence(error);
117+
message.push(`${delimE}${language}\n${error}\n${delimE}`);
118+
119+
if (line) {
120+
message.push(`For the following line:`);
121+
const delimL = backtickSequence(line);
122+
message.push(`${delimL}${language}\n${line}\n${delimL}`);
123+
}
124+
125+
// We put the input last, since it could be huge and get truncated.
126+
// It's much more important to show the error, obviously.
127+
if (input) {
128+
if (input.length < CUTOFF) {
129+
message.push(`My ${extraFileInfo ?? ""} contains:`);
130+
} else {
131+
if (prioritize === "start-end") {
132+
input = trunc_middle(input, CUTOFF, "\n\n[...]\n\n");
133+
} else if (prioritize === "end") {
134+
input = trunc_left(input, CUTOFF);
135+
} else {
136+
input = trunc(input, CUTOFF);
137+
}
138+
const describe =
139+
prioritize === "start"
140+
? "starts"
141+
: prioritize === "end"
142+
? "ends"
143+
: "starts and ends";
144+
message.push(
145+
`My ${
146+
extraFileInfo ?? ""
147+
} code ${describe} as follows, but is too long to fully include here:`,
148+
);
149+
}
150+
const delimI = backtickSequence(input);
151+
message.push(`${delimI}${language}\n${input}\n${delimI}`);
152+
}
153+
154+
if (full) message.push("</details>");
155+
156+
return message.join("\n\n");
157+
}
158+
159+
function trimStr(s: string, language): string {
160+
if (s.length > 3000) {
161+
// 3000 is about 500 tokens
162+
// This uses structure:
163+
s = shortenError(s, language);
164+
if (s.length > 3000) {
165+
// this just puts ... in the middle.
166+
s = trunc_middle(s, 3000);
167+
}
168+
}
169+
return s;
170+
}

0 commit comments

Comments
 (0)