Skip to content

Commit f308850

Browse files
committed
frontend/llm: hint for help me fix button
1 parent e4edefd commit f308850

24 files changed

+448
-235
lines changed

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"statsmodels",
4040
"syncdb",
4141
"syncdoc",
42-
"syncdoc",
42+
"synctex",
4343
"syncstring",
4444
"synctable",
4545
"synctables",
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: "450px",
84+
overflow: "auto",
85+
maxWidth: "90vw",
86+
maxHeight: "300px",
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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
language?: string;
16+
extraFileInfo?: string;
17+
redux: any;
18+
prioritize?: "start" | "start-end" | "end";
19+
model: string;
20+
}
21+
22+
export interface CreateMessageOpts {
23+
tag?: string;
24+
error: string;
25+
input?: string;
26+
task?: string;
27+
language?: string;
28+
extraFileInfo?: string;
29+
prioritize?: "start" | "start-end" | "end";
30+
model: string;
31+
open: boolean;
32+
full: boolean;
33+
isHint?: boolean;
34+
}
35+
36+
export async function getHelp(options: GetHelpOptions) {
37+
const {
38+
project_id,
39+
path,
40+
tag,
41+
error,
42+
input,
43+
task,
44+
language,
45+
extraFileInfo,
46+
redux,
47+
prioritize,
48+
model,
49+
} = options;
50+
51+
const solutionText = createMessage({
52+
error,
53+
task,
54+
input,
55+
language,
56+
extraFileInfo,
57+
model,
58+
prioritize,
59+
open: true,
60+
full: false,
61+
isHint: false,
62+
});
63+
64+
try {
65+
const actions = await getChatActions(redux, project_id, path);
66+
setTimeout(() => actions.scrollToBottom(), 100);
67+
await actions.sendChat({
68+
input: solutionText,
69+
tag: `help-me-fix-solution${tag ? `:${tag}` : ""}`,
70+
noNotification: true,
71+
});
72+
} catch (err) {
73+
console.error("Error getting help:", err);
74+
throw err;
75+
}
76+
}
77+
78+
export function createMessage({
79+
error,
80+
language,
81+
input,
82+
model,
83+
task,
84+
extraFileInfo,
85+
prioritize,
86+
open,
87+
full,
88+
isHint = false,
89+
}: CreateMessageOpts): string {
90+
const message: string[] = [];
91+
const prefix = full ? modelToMention(model) + " " : "";
92+
if (isHint) {
93+
message.push(
94+
`${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.`,
95+
);
96+
} else {
97+
message.push(`${prefix}Help me fix my code.`);
98+
}
99+
100+
if (full)
101+
message.push(`<details${open ? " open" : ""}><summary>Context</summary>`);
102+
103+
if (task) {
104+
message.push(`I ${task}.`);
105+
}
106+
107+
if (error.length > 3000) {
108+
// 3000 is about 500 tokens
109+
// This uses structure:
110+
error = shortenError(error, language);
111+
if (error.length > 3000) {
112+
// this just puts ... in the middle.
113+
error = trunc_middle(error, 3000);
114+
}
115+
}
116+
117+
message.push(`I received the following error:`);
118+
const delimE = backtickSequence(error);
119+
message.push(`${delimE}${language}\n${error}\n${delimE}`);
120+
121+
// We put the input last, since it could be huge and get truncated.
122+
// It's much more important to show the error, obviously.
123+
if (input) {
124+
if (input.length < CUTOFF) {
125+
message.push(`My ${extraFileInfo ?? ""} contains:`);
126+
} else {
127+
if (prioritize === "start-end") {
128+
input = trunc_middle(input, CUTOFF, "\n\n[...]\n\n");
129+
} else if (prioritize === "end") {
130+
input = trunc_left(input, CUTOFF);
131+
} else {
132+
input = trunc(input, CUTOFF);
133+
}
134+
const describe =
135+
prioritize === "start"
136+
? "starts"
137+
: prioritize === "end"
138+
? "ends"
139+
: "starts and ends";
140+
message.push(
141+
`My ${
142+
extraFileInfo ?? ""
143+
} code ${describe} as follows, but is too long to fully include here:`,
144+
);
145+
}
146+
const delimI = backtickSequence(input);
147+
message.push(`${delimI}${language}\n${input}\n${delimI}`);
148+
}
149+
150+
if (full) message.push("</details>");
151+
152+
return message.join("\n\n");
153+
}

0 commit comments

Comments
 (0)