Skip to content

Feature/code replay #237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import About from "./views/About/About"
import Workspace from "./views/Workspace/Workspace"
import Dashboard from "./views/Dashboard/Dashboard"
import Student from "./views/Student/Student"
import StudentDetail from './views/StudentDetail/StudentDetail'
import NotFound from "./views/NotFound"
import StudentLogin from "./views/StudentLogin/StudentLogin";
import Sandbox from "./views/Sandbox/Sandbox"
Expand All @@ -16,27 +17,49 @@ import TeacherLogin from "./views/TeacherLogin/TeacherLogin"
import ContentCreator from './views/ContentCreator/ContentCreator'
import UnitCreator from './views/ContentCreator/UnitCreator/UnitCreator'
import UploadBlocks from './views/UploadBlocks/UploadBlocks'
import Replay from './components/Replay/Replay';

const App = () => {
let history = useHistory();

return (
<div>
<Switch>
<Route exact path={"/"} render={() => <Home history={history}/>}/>
<Route exact path={"/about"} render={() => <About history={history}/>}/>
<Route exact path={"/teacherlogin"} render={() => <TeacherLogin history={history}/>}/>
<Route exact path={"/login"} render={() => <StudentLogin history={history} />}/>
<Route exact path={"/"}>
<Home/>
</Route>
<Route exact path={"/about"}>
<About/>
</Route>
<Route exact path={"/teacherlogin"}>
<TeacherLogin/>
</Route>
<Route exact path={"/login"}>
<StudentLogin/>
</Route>
<PrivateRoute exact path={"/dashboard"} render={() => <Dashboard history={history}/>}/>
<PrivateRoute exact path={"/student"} render={() => <Student history={history} /> } />
<Route path={"/workspace"} render={() => <Workspace history={history} />}/>
<Route path={"/sandbox"} render={() => <Sandbox history={history}/>} />
<Route path={"/workspace"}>
<Workspace/>
</Route>
<Route path={"/sandbox"}>
<Sandbox/>
</Route>
<PrivateRoute exact path={"/day"} render={() => <Day history={history} /> } />
<PrivateRoute path={"/classroom/:id"} render={() => <Classroom history={history} /> } />
<Route exact path={"/ccdashboard"} render={() => <ContentCreator history={history} />}/>
<Route exact path={"/unitcreator"} render={() => <UnitCreator history={history} />}/>
<Route exact path={"/addblocks"} render={() => <UploadBlocks history={history} />}/>

<PrivateRoute path={"/students/:classroomId/:studentId"} render={() => <StudentDetail /> } />
<Route exact path={"/ccdashboard"}>
<ContentCreator/>
</Route>
<Route exact path={"/unitcreator"}>
<UnitCreator/>
</Route>
<Route exact path={"/addblocks"}>
<UploadBlocks/>
</Route>
<Route exact path={"/replay"}>
<Replay/>
</Route>
<Route component={NotFound}/>
</Switch>
</div>
Expand Down
16 changes: 13 additions & 3 deletions client/src/Utils/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,14 @@ export const setSelection = async (classroom, learningStandard) => (
})
);

export const saveWorkspace = async (day, workspace) => (
export const saveWorkspace = async (day, workspace, replay) => (
makeRequest({
method: POST,
path: `${server}/saves`,
data: {
day: day,
workspace: workspace
day,
workspace,
replay
},
auth: true,
error: 'Failed to save your workspace.'
Expand All @@ -234,6 +235,15 @@ export const getSaves = async (day) => (
})
);

export const getAllSaves = async () => (
makeRequest({
method: GET,
path: `${server}/saves`,
auth: true,
error: 'Past saves could not be retrieved.'
})
);

export const createSubmission = async (day, workspace, sketch, path, isAuth) => (
makeRequest({
method: POST,
Expand Down
8 changes: 8 additions & 0 deletions client/src/assets/style.less
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,12 @@

.nav-padding {
padding-top: 8vh;
}

.replayButton {
cursor: pointer;
}

.bold {
font-weight: bold;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ export default function BlocklyCanvasPanel(props) {

const workspaceRef = useRef(null);
const dayRef = useRef(null);
const replayRef = useRef([]);
const undoLength = useRef(0);
const { SubMenu } = Menu;

const setWorkspace = () =>
const setWorkspace = () => {
workspaceRef.current = window.Blockly.inject('blockly-canvas',
{ toolbox: document.getElementById('toolbox') }
);
}

const loadSave = selectedSave => {
try {
Expand Down Expand Up @@ -64,18 +67,31 @@ export default function BlocklyCanvasPanel(props) {
// automatically save workspace every min
setInterval(async () => {
if (isStudent && workspaceRef.current && dayRef.current) {
const res = await handleSave(dayRef.current.id, workspaceRef);
const res = await handleSave(dayRef.current.id, workspaceRef, replayRef.current);
if (res.data) {
setLastAutoSave(res.data[0]);
setLastSavedTime(getFormattedDate(res.data[0].updated_at))
}
}
}, 60000);
setInterval(async () => {
if (workspaceRef.current.undoStack_.length !== undoLength.current) {
undoLength.current = workspaceRef.current.undoStack_.length;
let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current);
let xml_text = window.Blockly.Xml.domToText(xml);
const replay = {
xml: xml_text,
timestamp: Date.now()
}
replayRef.current.push(replay);
console.log(replayRef.current);
}
}, 1000);

// clean up - saves workspace and removes blockly div from DOM
return async () => {
if (isStudent && dayRef.current && workspaceRef.current)
await handleSave(dayRef.current.id, workspaceRef);
await handleSave(dayRef.current.id, workspaceRef, replayRef.current);
if (workspaceRef.current) workspaceRef.current.dispose();
dayRef.current = null
}
Expand All @@ -102,6 +118,7 @@ export default function BlocklyCanvasPanel(props) {
if (onLoadSave) {
let xml = window.Blockly.Xml.textToDom(onLoadSave.workspace);
window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current);
replayRef.current = onLoadSave.replay;
setLastSavedTime(getFormattedDate(onLoadSave.updated_at));
} else if (day.template) {
let xml = window.Blockly.Xml.textToDom(day.template);
Expand All @@ -116,7 +133,7 @@ export default function BlocklyCanvasPanel(props) {

const handleManualSave = async () => {
// save workspace then update load save options
const res = await handleSave(day.id, workspaceRef);
const res = await handleSave(day.id, workspaceRef, replayRef.current);
if (res.err) {
message.error(res.err)
} else {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/DayPanels/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ const flashArduino = async (response) => {
}

// save current workspace
export const handleSave = async (dayId, workspaceRef) => {
export const handleSave = async (dayId, workspaceRef, replay) => {
let xml = window.Blockly.Xml.workspaceToDom(workspaceRef.current);
let xml_text = window.Blockly.Xml.domToText(xml);
return await saveWorkspace(dayId, xml_text);
return await saveWorkspace(dayId, xml_text, replay);
};

export const handleCreatorSaveDay = async (dayId, workspaceRef, blocksList) => {
Expand Down
109 changes: 109 additions & 0 deletions client/src/components/Replay/Replay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom'
import NavBar from '../NavBar/NavBar';

const Replay = () => {
const workspaceRef = useRef(null);
const [step, setStep] = useState(0);
let playback;
const setWorkspace = () => {
workspaceRef.current = window.Blockly.inject('blockly-canvas',
{ toolbox: document.getElementById('toolbox') }
);
}
const replay = [
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"12\" y=\"3\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block></xml>",
"timestamp": 1618859201398
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block></xml>",
"timestamp": 1618859202401
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\" x=\"85\" y=\"289\"><field name=\"OP\">AND</field></block></xml>",
"timestamp": 1618859203397
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\" x=\"146\" y=\"267\"><field name=\"OP\">AND</field></block></xml>",
"timestamp": 1618859204397
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"math_single\" id=\"hVMn@TndjNGID0Wv5wyf\" x=\"162\" y=\"213\"><field name=\"OP\">ROOT</field></block><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\" x=\"146\" y=\"267\"><field name=\"OP\">AND</field></block></xml>",
"timestamp": 1618859206397
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"math_single\" id=\"hVMn@TndjNGID0Wv5wyf\" x=\"35\" y=\"267\"><field name=\"OP\">ROOT</field><value name=\"NUM\"><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\"><field name=\"OP\">AND</field></block></value></block></xml>",
"timestamp": 1618859209397
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"controls_whileUntil\" id=\",Etk-r,BQ][E9nQd^?VO\" x=\"368\" y=\"155\"><field name=\"MODE\">WHILE</field></block><block type=\"math_single\" id=\"hVMn@TndjNGID0Wv5wyf\" x=\"35\" y=\"267\"><field name=\"OP\">ROOT</field><value name=\"NUM\"><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\"><field name=\"OP\">AND</field></block></value></block></xml>",
"timestamp": 1618859211400
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"math_single\" id=\"hVMn@TndjNGID0Wv5wyf\" x=\"35\" y=\"267\"><field name=\"OP\">ROOT</field><value name=\"NUM\"><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\"><field name=\"OP\">AND</field></block></value></block><block type=\"controls_whileUntil\" id=\",Etk-r,BQ][E9nQd^?VO\" x=\"476\" y=\"355\"><field name=\"MODE\">WHILE</field></block></xml>",
"timestamp": 1618859214404
},
{
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\"><block type=\"controls_whileUntil\" id=\"j^FJ6%E)J2vb*o35fJ(R\" x=\"93\" y=\"53\"><field name=\"MODE\">WHILE</field></block><block type=\"controls_if\" id=\"+tDwx%uD*;|/%MoHiBuD\" x=\"72\" y=\"141\"><value name=\"IF0\"><block type=\"variables_get\" id=\"iQ,uNAfjOn;[Fhd|~lzS\"><field name=\"VAR\">item</field></block></value></block><block type=\"math_single\" id=\"hVMn@TndjNGID0Wv5wyf\" x=\"35\" y=\"267\"><field name=\"OP\">ROOT</field><value name=\"NUM\"><block type=\"logic_operation\" id=\"kcE(=RH;B2%jPFhNjyDK\"><field name=\"OP\">AND</field></block></value></block></xml>",
"timestamp": 1618859216400
}
];
useEffect(() => {
workspaceRef.current ? workspaceRef.current.clear() : setWorkspace();
const xml = window.Blockly.Xml.textToDom(replay[step].xml);
window.Blockly.Xml.domToWorkspace(xml, workspaceRef.current);
}, [step]);

const goBack = () => {
setStep(step - 1);
}
// const play = () => {
// playback = setInterval(() => {
// console.log('firing');
// setStep(step + 1);
// console.log(step);
// }, 1000);
// }
const goForward = () => {
setStep(step + 1);
}
return (
<main className="container nav-padding">
<NavBar />
<div id="horizontal-container" className="flex flex-column">
<div id="top-container" className="flex flex-column vertical-container">
<div id="description-container" className="flex flex-row space-between card">
<div className="flex flex-row">
<Link id="link" to="/" className="flex flex-column">
<i className="fa fa-home"/>
</Link>
</div>
<div className="flex flex-row">
<button className="replayButton" onClick={goBack} disabled={step === 0}>&#9198;</button>
<button className="replayButton" disabled={playback}>&#9654;&#65039;</button>
<button className="replayButton" onClick={goForward} disabled={step === (replay.length - 1)}>&#9197;</button>
</div>
</div>
</div>
<div className='flex flex-row'>
<div id='bottom-container' className="flex flex-column vertical-container overflow-visible">
<h1 id="section-header">Code Replay</h1>
<div id="blockly-canvas"/>
</div>
</div>
<div className='flex flex-row'>
<section id='bottom-container' className="flex flex-column vertical-container overflow-visible">
<h2 id="section-header">Logs</h2>
<div>
{ replay.map((item, index) => <p className={step === index ? 'bold' : null} key={item.timestamp}>{item.timestamp}</p>)}
</div>
</section>
</div>
</div>
<xml id="toolbox" is="Blockly workspace"></xml>
</main>
)
};

export default Replay;
7 changes: 6 additions & 1 deletion client/src/views/Classroom/Classroom.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, {useEffect, useState} from "react"
import {Link} from 'react-router-dom';
import {message, Tabs} from 'antd';
import "./Classroom.less"

Expand Down Expand Up @@ -27,7 +28,7 @@ export default function Classroom(props) {
}
});
}, [classroomId]);

console.log(classroom);
return (
<div className="container nav-padding">
<NavBar isMentor={true}/>
Expand All @@ -47,6 +48,10 @@ export default function Classroom(props) {
<Roster history={history} handleLogout={handleLogout} classroomId={classroomId}/>
</TabPane>
</Tabs>
<h2>Student List</h2>
<ul>
{classroom.students?.map(student => <li key={student.id}><Link to={`/students/${classroom.id}/${student.id}`}>{student.name}</Link></li>)}
</ul>
</div>
);

Expand Down
11 changes: 6 additions & 5 deletions client/src/views/ContentCreator/ContentCreator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';

import {useHistory} from 'react-router-dom';
import { Tabs, Table, Popconfirm, message } from 'antd'
import Navbar from '../../components/NavBar/NavBar'
import { QuestionCircleOutlined } from '@ant-design/icons';
Expand All @@ -12,11 +12,12 @@ import UnitEditor from './UnitEditor/UnitEditor'

const { TabPane } = Tabs;

export default function ContentCreator(props) {
export default function ContentCreator() {

const [dataSource, setDataSource] = useState([])
const [dataSourceMap, setDataSourceMap] = useState([])
const [gradeMenu, setGradeMenu] = useState([])
const history = useHistory();

useEffect(() => {
//console.log(getLearningStandardcount())
Expand Down Expand Up @@ -44,7 +45,7 @@ export default function ContentCreator(props) {
width: '22.5%',
align: 'left',
render: (_, key) => (
<DayEditor history={props.history} days = {getDays(key)} learningStandardId={key.id} learningStandardName={key.name} linkBtn={true}/>
<DayEditor history={history} days = {getDays(key)} learningStandardId={key.id} learningStandardName={key.name} linkBtn={true}/>
)
},
{
Expand Down Expand Up @@ -83,7 +84,7 @@ export default function ContentCreator(props) {
// width: '10%',
// align: 'right',
// render: (_, key) => (
// <DayEditor history={props.history} days = {getDays(key)} learningStandardId={key.edit} learningStandardName = {getLearningStandardName(key.edit)} linkBtn={true}/>
// <DayEditor history={history} days = {getDays(key)} learningStandardId={key.edit} learningStandardName = {getLearningStandardName(key.edit)} linkBtn={true}/>
// )
// },
// {
Expand All @@ -93,7 +94,7 @@ export default function ContentCreator(props) {
// width: '10%',
// align: 'right',
// render: (_, key) => (
// <DayEditor history={props.history} days = {getDays(key)} learningStandard={key.edit} linkBtn={true}/>
// <DayEditor history={history} days = {getDays(key)} learningStandard={key.edit} linkBtn={true}/>
// )
// },
// {
Expand Down
Loading