Skip to content

Commit 4f6be6b

Browse files
authored
Refactor frontend part 7 (#2846)
* Improve useRef type arguments for HTML elements * Use named imports for React hooks * Refactor MobileWorkspace * Replace if statements with optional invocation * Move constant `SideContentTab`s out of FC body * Simplify classnames call * Remove unnecessary braces around string attributes Only done for non-UI (non-message/label) strings as these UI strings will be pulled out for internationalization in the future. * Deduplicate dependencies * Create `useTokens` hook * Update default throw behavior for `useTokens` * Render SICP chatbot only when logged in * Fix overloaded types for `useTokens` Done following default behavior change. * Use `useTokens` where applicable * Refactor SICP chatbox * Use `useTokens` * Move API call to separate file * Update React import to default import * Refactor SICP chatbox further * Update imports * Create and use ChatMessage type alias * Reorganize SICP chatbot files * Refactor chat completion logic * Use dependency injection in prompt builder * Refactor prompt builder logic * Refactor prompt builder to separate file * Create type definitions * Move, type, rename SICP section summaries * Improve typing * Refactor SICP chat box component * Remove unnecessary template literal * Refactor constants out of component * Create type definitions * Create `CONTEXT_SIZE` constant to replace magic numbers * Refactor logic to use `CONTEXT_SIZE` constant * Remove unnecessary state variables * Refactor payload generation * Move fetching logic from event handler to effect callback instead * Rename `cleanMessage` to `resetChat` * Decouple rendering logic from chat completion logic * Only store string content in `ChatMessage` type * Move rendering function outside component to prevent unnecessary recreation * Update render function signature * Restore GPT-generated output warning for bot messages * Refactor render function logic * Fix React render warnings * Add TODOs for full Markdown/stories-like parsing Also uses non-greedy regex to match and split code blocks: * Only match JavaScript code blocks * Fix false matches * Supports multiple code blocks in a single message * Fix whitespace issue * Fix filename capitalization * Remove duplicated badge code * Fix double request * Block chat input when loading response
1 parent c1abec1 commit 4f6be6b

File tree

34 files changed

+250
-270
lines changed

34 files changed

+250
-270
lines changed

src/commons/editingWorkspaceSideContent/EditingWorkspaceSideContentTextAreaContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const TextAreaContent: React.FC<TextAreaContentProps> = props => {
5757
const makeEditingTextarea = () => (
5858
<Textarea
5959
autoFocus={true}
60-
className={'editing-textarea'}
60+
className="editing-textarea"
6161
onChange={handleEditAssessment}
6262
onBlur={saveEditAssessment}
6363
value={fieldValue}

src/commons/mobileWorkspace/MobileWorkspace.tsx

Lines changed: 44 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FocusStyleManager } from '@blueprintjs/core';
22
import { IconNames } from '@blueprintjs/icons';
33
import { Ace } from 'ace-builds';
4-
import React from 'react';
4+
import React, { useCallback, useEffect, useState } from 'react';
55
import { DraggableEvent } from 'react-draggable';
66
import { useMediaQuery } from 'react-responsive';
77

@@ -30,16 +30,16 @@ export type MobileWorkspaceProps = {
3030
const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
3131
const isAndroid = /Android/.test(navigator.userAgent);
3232
const isPortrait = useMediaQuery({ orientation: 'portrait' });
33-
const [draggableReplPosition, setDraggableReplPosition] = React.useState({ x: 0, y: 0 });
33+
const [draggableReplPosition, setDraggableReplPosition] = useState({ x: 0, y: 0 });
3434

3535
// For disabling draggable Repl when in stepper tab
36-
const [isDraggableReplDisabled, setIsDraggableReplDisabled] = React.useState(false);
36+
const [isDraggableReplDisabled, setIsDraggableReplDisabled] = useState(false);
3737

3838
// Get rid of the focus border on blueprint components
3939
FocusStyleManager.onlyShowFocusOnTabs();
4040

4141
// Handles the panel height when the mobile top controlbar is rendered in the Assessment Workspace
42-
React.useEffect(() => {
42+
useEffect(() => {
4343
if (props.mobileSideContentProps.workspaceLocation === 'assessment') {
4444
document.documentElement.style.setProperty(
4545
'--mobile-panel-height',
@@ -59,7 +59,7 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
5959
* soft keyboard on Android devices. This is due to the viewport height changing when the soft
6060
* keyboard is up on Android devices. IOS devices are not affected.
6161
*/
62-
React.useEffect(() => {
62+
useEffect(() => {
6363
if (isPortrait && isAndroid) {
6464
document.documentElement.style.setProperty('overflow', 'auto');
6565
const metaViewport = document.querySelector('meta[name=viewport]');
@@ -83,50 +83,38 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
8383
};
8484
}, [isPortrait, isAndroid]);
8585

86-
const [targetKeyboardInput, setTargetKeyboardInput] = React.useState<Ace.Editor | null>(null);
86+
const [targetKeyboardInput, setTargetKeyboardInput] = useState<Ace.Editor | null>(null);
8787

8888
const clearTargetKeyboardInput = () => setTargetKeyboardInput(null);
8989

9090
const enableMobileKeyboardForEditor = (props: EditorContainerProps): EditorContainerProps => {
91-
const onFocus = (event: any, editor?: Ace.Editor) => {
92-
if (props.onFocus) {
93-
props.onFocus(event, editor);
94-
}
95-
if (!editor) {
96-
return;
97-
}
98-
setTargetKeyboardInput(editor);
99-
};
100-
const onBlur = (event: any, editor?: Ace.Editor) => {
101-
if (props.onBlur) {
102-
props.onBlur(event, editor);
103-
}
104-
clearTargetKeyboardInput();
105-
};
10691
return {
10792
...props,
108-
onFocus,
109-
onBlur
93+
onFocus: (event, editor?) => {
94+
props.onFocus?.(event, editor);
95+
if (!editor) {
96+
return;
97+
}
98+
setTargetKeyboardInput(editor);
99+
},
100+
onBlur: (event, editor?) => {
101+
props.onBlur?.(event, editor);
102+
clearTargetKeyboardInput();
103+
}
110104
};
111105
};
112106

113107
const enableMobileKeyboardForRepl = (props: ReplProps): ReplProps => {
114-
const onFocus = (editor: Ace.Editor) => {
115-
if (props.onFocus) {
116-
props.onFocus(editor);
117-
}
118-
setTargetKeyboardInput(editor);
119-
};
120-
const onBlur = () => {
121-
if (props.onBlur) {
122-
props.onBlur();
123-
}
124-
clearTargetKeyboardInput();
125-
};
126108
return {
127109
...props,
128-
onFocus,
129-
onBlur
110+
onFocus: editor => {
111+
props.onFocus?.(editor);
112+
setTargetKeyboardInput(editor);
113+
},
114+
onBlur: () => {
115+
props.onBlur?.();
116+
clearTargetKeyboardInput();
117+
}
130118
};
131119
};
132120

@@ -184,13 +172,11 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
184172
};
185173

186174
const handleEditorEval = props.editorContainerProps?.handleEditorEval;
187-
const handleTabChangeForRepl = React.useCallback(
175+
const handleTabChangeForRepl = useCallback(
188176
(newTabId: SideContentType, prevTabId: SideContentType) => {
189177
// Evaluate program upon pressing the run tab.
190178
if (newTabId === SideContentType.mobileEditorRun) {
191-
if (handleEditorEval) {
192-
handleEditorEval();
193-
}
179+
handleEditorEval?.();
194180
}
195181

196182
// Show the REPL upon pressing the run tab if the previous tab is not listed below.
@@ -226,7 +212,7 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
226212
);
227213

228214
const onChange = props.mobileSideContentProps.onChange;
229-
const onSideContentTabChange = React.useCallback(
215+
const onSideContentTabChange = useCallback(
230216
(
231217
newTabId: SideContentType,
232218
prevTabId: SideContentType,
@@ -241,27 +227,7 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
241227
// Convert sidebar tabs with a side content tab ID into side content tabs.
242228
const sideBarTabs: SideContentTab[] = props.sideBarProps.tabs.filter(tab => tab.id !== undefined);
243229

244-
const mobileEditorTab: SideContentTab = React.useMemo(
245-
() => ({
246-
label: 'Editor',
247-
iconName: IconNames.EDIT,
248-
body: null,
249-
id: SideContentType.mobileEditor
250-
}),
251-
[]
252-
);
253-
254-
const mobileRunTab: SideContentTab = React.useMemo(
255-
() => ({
256-
label: 'Run',
257-
iconName: IconNames.PLAY,
258-
body: null,
259-
id: SideContentType.mobileEditorRun
260-
}),
261-
[]
262-
);
263-
264-
const updatedMobileSideContentProps = React.useCallback(() => {
230+
const updatedMobileSideContentProps = useCallback(() => {
265231
return {
266232
...props.mobileSideContentProps,
267233
onChange: onSideContentTabChange,
@@ -277,13 +243,7 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
277243
]
278244
}
279245
};
280-
}, [
281-
onSideContentTabChange,
282-
mobileEditorTab,
283-
mobileRunTab,
284-
props.mobileSideContentProps,
285-
sideBarTabs
286-
]);
246+
}, [onSideContentTabChange, props.mobileSideContentProps, sideBarTabs]);
287247

288248
const inAssessmentWorkspace = props.mobileSideContentProps.workspaceLocation === 'assessment';
289249

@@ -318,3 +278,17 @@ const MobileWorkspace: React.FC<MobileWorkspaceProps> = props => {
318278
};
319279

320280
export default MobileWorkspace;
281+
282+
const mobileEditorTab: SideContentTab = {
283+
label: 'Editor',
284+
iconName: IconNames.EDIT,
285+
body: null,
286+
id: SideContentType.mobileEditor
287+
};
288+
289+
const mobileRunTab: SideContentTab = {
290+
label: 'Run',
291+
iconName: IconNames.PLAY,
292+
body: null,
293+
id: SideContentType.mobileEditorRun
294+
};

src/commons/navigationBar/NavigationBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ const NavigationBar: React.FC = () => {
245245
position={Position.BOTTOM_RIGHT}
246246
interactionKind="hover"
247247
content={desktopNavbarLeftPopoverContent}
248-
popoverClassName={'desktop-navbar-popover'}
248+
popoverClassName="desktop-navbar-popover"
249249
disabled={!enableDesktopPopover}
250250
>
251251
<NavLink

src/commons/sagas/RequestsSaga.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,28 +1660,6 @@ export async function deleteDevice(device: Pick<Device, 'id'>, tokens?: Tokens):
16601660
return true;
16611661
}
16621662

1663-
/**
1664-
* POST /chat
1665-
*/
1666-
1667-
export async function chat(
1668-
tokens: Tokens,
1669-
payload: { role: string; content: string }[]
1670-
): Promise<string> {
1671-
const response = await request(`chat`, 'POST', {
1672-
...tokens,
1673-
body: { json: payload }
1674-
});
1675-
if (!response) {
1676-
throw new Error('Unknown error occurred.');
1677-
}
1678-
if (!response.ok) {
1679-
const message = await response.text();
1680-
throw new Error(`Failed to chat to louis: ${message}`);
1681-
}
1682-
return response.text();
1683-
}
1684-
16851663
function fillTokens(tokens?: Tokens): Tokens {
16861664
tokens = tokens || getTokensFromStore();
16871665
if (!tokens) {

src/commons/sideContent/content/SideContentFaceapiDisplay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const SideContentFaceapiDisplay: React.FC = () => {
2121
<div className="sa-video-header">
2222
<div className="sa-video-header-element">
2323
<Button
24-
className={'sa-live-video-button'}
24+
className="sa-live-video-button"
2525
style={{ height: 20 }}
2626
icon={IconNames.CAMERA}
2727
onClick={takePhoto}
@@ -35,7 +35,7 @@ const SideContentFaceapiDisplay: React.FC = () => {
3535
<Divider />
3636
<div className="sa-video-header-element">
3737
<Button
38-
className={'sa-still-image-button'}
38+
className="sa-still-image-button"
3939
style={{ height: 20 }}
4040
icon={IconNames.RESET}
4141
onClick={resetPhoto}

src/commons/sourceRecorder/SourceRecorderTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class SourcecastTable extends React.Component<SourceRecorderTableProps, State> {
169169
<div className="SourcecastTable">
170170
<div className="ag-grid-parent">
171171
<AgGridReact
172-
domLayout={'autoHeight'}
172+
domLayout="autoHeight"
173173
columnDefs={this.state.columnDefs}
174174
defaultColDef={this.defaultColumnDefs}
175175
onGridReady={this.onGridReady}

src/commons/utils/Hooks.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { useMediaQuery } from 'react-responsive';
88

99
import { OverallState } from '../application/ApplicationTypes';
10+
import { Tokens } from '../application/types/SessionTypes';
1011
import Constants from './Constants';
1112
import { readLocalStorage, setLocalStorage } from './LocalStorageHelper';
1213

@@ -135,3 +136,23 @@ export const useSession = () => {
135136
isLoggedIn
136137
};
137138
};
139+
140+
// Overload for useTokens
141+
type UseTokens = {
142+
(): Tokens;
143+
(options: { throwWhenEmpty: true }): Tokens;
144+
(options: { throwWhenEmpty: false }): Partial<Tokens>;
145+
};
146+
147+
/**
148+
* Returns the access token and refresh token from the session.
149+
* @param throwWhenEmpty (optional) If true, throws an error if no tokens are found.
150+
*/
151+
export const useTokens: UseTokens = ({ throwWhenEmpty = true } = {}) => {
152+
const accessToken = useTypedSelector(state => state.session.accessToken);
153+
const refreshToken = useTypedSelector(state => state.session.refreshToken);
154+
if (throwWhenEmpty && (!accessToken || !refreshToken)) {
155+
throw new Error('No access token or refresh token found');
156+
}
157+
return { accessToken, refreshToken } as Tokens;
158+
};

src/commons/workspace/Workspace.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { IconNames } from '@blueprintjs/icons';
33
import { useFullscreen } from '@mantine/hooks';
44
import { Enable, NumberSize, Resizable, ResizableProps, ResizeCallback } from 're-resizable';
55
import { Direction } from 're-resizable/lib/resizer';
6-
import React from 'react';
6+
import React, { useEffect, useRef, useState } from 'react';
77

88
import ControlBar, { ControlBarProps } from '../controlBar/ControlBar';
99
import EditorContainer, { EditorContainerProps } from '../editor/EditorContainer';
@@ -35,15 +35,15 @@ type StateProps = {
3535
};
3636

3737
const Workspace: React.FC<WorkspaceProps> = props => {
38-
const sideBarResizable = React.useRef<Resizable | null>(null);
39-
const contentContainerDiv = React.useRef<HTMLDivElement | null>(null);
40-
const editorDividerDiv = React.useRef<HTMLDivElement | null>(null);
41-
const leftParentResizable = React.useRef<Resizable | null>(null);
42-
const maxDividerHeight = React.useRef<number | null>(null);
43-
const sideDividerDiv = React.useRef<HTMLDivElement | null>(null);
38+
const sideBarResizable = useRef<Resizable | null>(null);
39+
const contentContainerDiv = useRef<HTMLDivElement>(null);
40+
const editorDividerDiv = useRef<HTMLDivElement>(null);
41+
const leftParentResizable = useRef<Resizable | null>(null);
42+
const maxDividerHeight = useRef<number | null>(null);
43+
const sideDividerDiv = useRef<HTMLDivElement>(null);
4444
const [contentContainerWidth] = useDimensions(contentContainerDiv);
45-
const [expandedSideBarWidth, setExpandedSideBarWidth] = React.useState(200);
46-
const [isSideBarExpanded, setIsSideBarExpanded] = React.useState(true);
45+
const [expandedSideBarWidth, setExpandedSideBarWidth] = useState(200);
46+
const [isSideBarExpanded, setIsSideBarExpanded] = useState(true);
4747

4848
const sideBarCollapsedWidth = 40;
4949

@@ -52,7 +52,7 @@ const Workspace: React.FC<WorkspaceProps> = props => {
5252

5353
FocusStyleManager.onlyShowFocusOnTabs();
5454

55-
React.useEffect(() => {
55+
useEffect(() => {
5656
if (props.sideContentIsResizeable && maxDividerHeight.current === null) {
5757
maxDividerHeight.current = sideDividerDiv.current!.clientHeight;
5858
}

src/features/cseMachine/CseMachineLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ export class Layout {
490490
return Layout.prevLayout;
491491
} else {
492492
const layout = (
493-
<div className={'sa-cse-machine'} data-testid="sa-cse-machine">
493+
<div className="sa-cse-machine" data-testid="sa-cse-machine">
494494
<div
495495
id="scroll-container"
496496
ref={Layout.scrollContainerRef}

src/features/cseMachine/components/ControlItemComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class ControlItemComponent extends Visible implements IHoverable {
123123
<Tag
124124
{...ShapeDefaultProps}
125125
stroke="black"
126-
fill={'black'}
126+
fill="black"
127127
opacity={ControlStashConfig.TooltipOpacity}
128128
/>
129129
<Text

src/features/cseMachine/components/StashItemComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class StashItemComponent extends Visible implements IHoverable {
127127
<Tag
128128
{...ShapeDefaultProps}
129129
stroke="black"
130-
fill={'black'}
130+
fill="black"
131131
opacity={ControlStashConfig.TooltipOpacity}
132132
/>
133133
<Text

src/features/cseMachine/components/values/FnValue.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export class FnValue extends Value implements IHoverable {
168168
visible={true}
169169
ref={this.labelRef}
170170
>
171-
<KonvaTag stroke="black" fill={'white'} opacity={Config.FnTooltipOpacity} />
171+
<KonvaTag stroke="black" fill="white" opacity={Config.FnTooltipOpacity} />
172172
<KonvaText
173173
text={this.exportTooltip}
174174
fontFamily={Config.FontFamily}
@@ -185,7 +185,7 @@ export class FnValue extends Value implements IHoverable {
185185
visible={false}
186186
ref={this.labelRef}
187187
>
188-
<KonvaTag stroke="black" fill={'black'} opacity={Config.FnTooltipOpacity} />
188+
<KonvaTag stroke="black" fill="black" opacity={Config.FnTooltipOpacity} />
189189
<KonvaText
190190
text={this.tooltip}
191191
fontFamily={Config.FontFamily}

0 commit comments

Comments
 (0)