Skip to content

Commit 2c1a7b7

Browse files
Web Lab 2: Add project files as context to AI tutor (#68567)
* some initial plumbing * more plumbing * wip * getting files to show up * create context in redux * start plumbing through redux * use new context * clear context after sending * clean up and add file path * add context to chat history * small updates * clean up * undo csp change * refactor to align with AiTutorContextHelper class intentions (#68589) * add key * fix close icon and clear context on level switch --------- Co-authored-by: Ed Baafi <edward.baafi@code.org>
1 parent 7e84788 commit 2c1a7b7

22 files changed

Lines changed: 216 additions & 34 deletions

apps/i18n/codebridge/en_us.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"addToAiTutorContext": "Add to AI Tutor Chat",
23
"addSubFolder": "Add sub-folder",
34
"areYouSure": "Are you sure?",
45
"cannotEditFile": "Cannot currently edit files of type {language}",

apps/src/aiTutor/helpers/aiTutorContextHelper.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export abstract class AiTutorContextHelper<T extends object> {
1919
validationResults,
2020
longInstructions,
2121
documentation,
22+
userSelection,
2223
} = await this.getAiTutorContext();
2324

2425
const hiddenContextString = [
@@ -42,6 +43,12 @@ export abstract class AiTutorContextHelper<T extends object> {
4243
documentation,
4344
]
4445
: []),
46+
...(userSelection
47+
? [
48+
'The student would like to focus on this subset of their current code:',
49+
userSelection,
50+
]
51+
: []),
4552
].join('\n\n');
4653

4754
// TODO: This log is a bit chatty, but useful while we're working on this feature.

apps/src/aiTutor/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export interface AiTutorContext {
1616
validationResults?: string;
1717
longInstructions?: string;
1818
documentation?: string;
19+
userSelection?: string;
1920
}

apps/src/aichat/redux/slice.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ChatAsset,
2525
SaveError,
2626
AiChatClientType,
27+
UserAddedSelectionContextItem,
2728
} from '../types';
2829
import {
2930
DEFAULT_VISIBILITIES,
@@ -55,6 +56,7 @@ const initialState: AichatState = {
5556
saveError: undefined,
5657
showResetMessage: false,
5758
hasSetStartingCustomizations: false,
59+
userAddedSelectionContext: {},
5860
};
5961

6062
const aichatSlice = createSlice({
@@ -328,6 +330,23 @@ const aichatSlice = createSlice({
328330
setSaveError(state, action: PayloadAction<SaveError | undefined>) {
329331
state.saveError = action.payload;
330332
},
333+
addItemToUserAddedSelectionContext(
334+
state,
335+
action: PayloadAction<UserAddedSelectionContextItem>
336+
) {
337+
state.userAddedSelectionContext[action.payload.displayName] =
338+
action.payload;
339+
},
340+
removeItemFromUserAddedSelectionContext(
341+
state,
342+
action: PayloadAction<string>
343+
) {
344+
state.userAddedSelectionContext[action.payload] &&
345+
delete state.userAddedSelectionContext[action.payload];
346+
},
347+
clearUserAddedSelectionContext(state) {
348+
state.userAddedSelectionContext = {};
349+
},
331350
},
332351
});
333352

@@ -386,4 +405,7 @@ export const {
386405
clearStagedFilesAlert,
387406
setSaveError,
388407
clearHasSetStartingCustomizations,
408+
addItemToUserAddedSelectionContext,
409+
removeItemFromUserAddedSelectionContext,
410+
clearUserAddedSelectionContext,
389411
} = aichatSlice.actions;

apps/src/aichat/redux/state.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ServerChatEvent,
1111
ViewMode,
1212
AiChatClientType,
13+
UserAddedSelectionContext,
1314
} from '../types';
1415

1516
export interface AichatState {
@@ -57,4 +58,5 @@ export interface AichatState {
5758
saveError: SaveError | undefined;
5859
// If the model customizations were just reset to the default level values.
5960
showResetMessage: boolean;
61+
userAddedSelectionContext: UserAddedSelectionContext;
6062
}

apps/src/aichat/redux/thunks/submitChatContents.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {createAsyncThunk} from '@reduxjs/toolkit';
33
import {
44
clearChatMessagePending,
55
clearStagedFiles,
6+
clearUserAddedSelectionContext,
67
setChatMessagePending,
78
} from '@cdo/apps/aichat/redux/slice';
89
import {Role} from '@cdo/apps/aiComponentLibrary/chatMessage/types';
@@ -26,6 +27,7 @@ import {
2627
ModelParameters,
2728
AiChatClientType,
2829
AnalyticsProperties,
30+
UserAddedSelectionContextItem,
2931
} from '../../types';
3032
import {getNewRemoveId} from '../utils';
3133

@@ -46,6 +48,7 @@ export const submitChatContents = createAsyncThunk(
4648
hiddenContext?: string;
4749
assets?: ChatAsset[];
4850
analyticsProperties?: AnalyticsProperties;
51+
userAddedSelectionContext?: UserAddedSelectionContextItem[];
4952
},
5053
thunkAPI
5154
) => {
@@ -59,10 +62,13 @@ export const submitChatContents = createAsyncThunk(
5962
modelParameters,
6063
clientType,
6164
analyticsProperties,
65+
userAddedSelectionContext,
6266
} = newUserMessageInput;
6367

6468
// Clear any staged files if present (used with multimodal models)
6569
thunkAPI.dispatch(clearStagedFiles());
70+
// Clear any user added context if present.
71+
thunkAPI.dispatch(clearUserAddedSelectionContext());
6672

6773
const aichatContext: AichatContext = {
6874
clientType,
@@ -77,6 +83,7 @@ export const submitChatContents = createAsyncThunk(
7783
chatMessageText: text,
7884
hiddenContext,
7985
assets,
86+
userAddedSelectionContext,
8087
timestamp: Date.now(),
8188
};
8289
dispatch(setChatMessagePending(newUserMessage));

apps/src/aichat/types/chatEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {AiInteractionStatus} from '@cdo/generated-scripts/sharedConstants';
55
import {ChatAsset} from './assets';
66
import {ModelParameters} from './customizations';
77
import {FeedbackValue} from './toxicity';
8+
import {UserAddedSelectionContextItem} from './userAddedSelectionContext';
89

910
export type ChatEventDescriptionKey = 'CLEAR_CHAT' | 'LOAD_LEVEL';
1011

@@ -22,6 +23,7 @@ interface BaseChatMessage extends BaseChatEvent {
2223
assets?: ChatAsset[];
2324
role: Role;
2425
status: ValueOf<typeof AiInteractionStatus>;
26+
userAddedSelectionContext?: UserAddedSelectionContextItem[];
2527
}
2628

2729
/** Chat message that is being sent to the server for chat completion. Status and request ID are yet undetermined. */

apps/src/aichat/types/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
export * from './analytics';
12
export * from './assets';
23
export * from './chatButton';
34
export * from './chatEvents';
45
export * from './customizations';
56
export * from './levelProperties';
67
export * from './systemPrompt';
78
export * from './toxicity';
8-
export * from './analytics';
9+
export * from './userAddedSelectionContext';
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export interface UserAddedSelectionContextItem {
2+
sourceCode: string;
3+
filename: string;
4+
lineReference?: {start: number; end: number};
5+
displayName: string;
6+
}
7+
8+
export type UserAddedSelectionContext = {
9+
[key: string]: UserAddedSelectionContextItem;
10+
};

apps/src/aichat/views/ChatMessageView.tsx

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ const ChatMessageView: React.FunctionComponent<ChatMessageViewProps> = ({
3232
buildAssetUrl,
3333
}) => {
3434
const [showProfaneUserMessage, setShowProfaneUserMessage] = useState(false);
35-
const {status, role, chatMessageText, assets} = chatMessage;
35+
const {status, role, chatMessageText, assets, userAddedSelectionContext} =
36+
chatMessage;
37+
const hasAssets = assets && buildAssetUrl;
38+
const hasUserAddedSelectionContext = !!userAddedSelectionContext?.length;
3639

3740
const displayText = getChatMessageDisplayText(
3841
status,
@@ -88,27 +91,36 @@ const ChatMessageView: React.FunctionComponent<ChatMessageViewProps> = ({
8891
}
8992

9093
let header;
91-
if (!isAssistant && assets && buildAssetUrl) {
94+
if (!isAssistant && (hasAssets || hasUserAddedSelectionContext)) {
9295
header = (
9396
<div className={styles.assetCol}>
94-
{assets.map(asset => {
95-
const filename = asset.filename;
96-
const url = buildAssetUrl(asset);
97-
return (
98-
<button
99-
key={filename}
100-
type="button"
101-
className={styles.assetButton}
102-
onClick={() => window.open(url, '_blank')}
103-
>
104-
{filename.endsWith('.pdf') ? (
105-
<FilePreview type="pdf" filename={filename} url={url} />
106-
) : (
107-
<img alt="" className={styles.imagePreview} src={url} />
108-
)}
109-
</button>
110-
);
111-
})}
97+
{hasAssets &&
98+
assets.map(asset => {
99+
const filename = asset.filename;
100+
const url = buildAssetUrl(asset);
101+
return (
102+
<button
103+
key={filename}
104+
type="button"
105+
className={styles.assetButton}
106+
onClick={() => window.open(url, '_blank')}
107+
>
108+
{filename.endsWith('.pdf') ? (
109+
<FilePreview type="pdf" filename={filename} url={url} />
110+
) : (
111+
<img alt="" className={styles.imagePreview} src={url} />
112+
)}
113+
</button>
114+
);
115+
})}
116+
{hasUserAddedSelectionContext &&
117+
userAddedSelectionContext.map(contextItem => (
118+
<FilePreview
119+
key={contextItem.displayName}
120+
type="text"
121+
filename={contextItem.displayName}
122+
/>
123+
))}
112124
</div>
113125
);
114126
}

0 commit comments

Comments
 (0)