diff --git a/frontend/package.json b/frontend/package.json index c64943f..5914cb1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "@fontsource/roboto": "^4.5.8", "@mui/material": "^5.11.4", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.10.0" } } diff --git a/frontend/src/components/adf-details.tsx b/frontend/src/components/adf-details.tsx new file mode 100644 index 0000000..8868b1a --- /dev/null +++ b/frontend/src/components/adf-details.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { AlertColor } from '@mui/material'; + +import { GraphProps } from './graph-g6'; + +export type Parsing = 'Naive' | 'Hybrid'; + +export type StrategySnakeCase = 'parse_only' | 'ground' | 'complete' | 'stable' | 'stable_counting_a' | 'stable_counting_b' | 'stable_nogood'; + +export type StrategyCamelCase = 'ParseOnly' | 'Ground' | 'Complete' | 'Stable' | 'StableCountingA' | 'StableCountingB' | 'StableNogood'; + +export interface AcAndGraph { + ac: string[], + graph: GraphProps, +} + +export type AcsWithGraphsOpt = { + type: 'None', +} | { + type: 'Error', + content: string +} | { + type: 'Some', + content: AcAndGraph[] +}; + +export type Task = { + type: 'Parse', +} | { + type: 'Solve', + content: StrategyCamelCase, +}; + +export interface AdfProblemInfo { + name: string, + code: string, + parsing_used: Parsing, + // NOTE: the keys are really only strategies + acs_per_strategy: { [key in StrategySnakeCase]: AcsWithGraphsOpt }, + running_tasks: Task[], +} + +export function acsWithGraphOptToColor(status: AcsWithGraphsOpt, running: boolean): AlertColor { + if (running) { + return 'warning'; + } + + switch (status.type) { + case 'None': return 'info'; + case 'Error': return 'error'; + case 'Some': return 'success'; + default: + throw new Error('Unknown type union variant (cannot occur)'); + } +} + +export function acsWithGraphOptToText(status: AcsWithGraphsOpt, running: boolean): string { + if (running) { + return 'Running'; + } + + switch (status.type) { + case 'None': return 'Not attempted'; + case 'Error': return 'Failed'; + case 'Some': return 'Done'; + default: + throw new Error('Unknown type union variant (cannot occur)'); + } +} + +function AdfDetails() { + return ( +
Details
+ ); +} + +export default AdfDetails; diff --git a/frontend/src/components/adf-new-form.tsx b/frontend/src/components/adf-new-form.tsx new file mode 100644 index 0000000..e254bd1 --- /dev/null +++ b/frontend/src/components/adf-new-form.tsx @@ -0,0 +1,168 @@ +import React, { + useState, useContext, useCallback, useRef, +} from 'react'; + +import { + Button, + Container, + FormControl, + FormControlLabel, + FormLabel, + Link, + Paper, + Radio, + RadioGroup, + Stack, + Typography, + TextField, + ToggleButtonGroup, + ToggleButton, +} from '@mui/material'; + +import LoadingContext from './loading-context'; +import SnackbarContext from './snackbar-context'; + +import { Parsing } from './adf-details'; + +const PLACEHOLDER = `s(a). +s(b). +s(c). +s(d). +ac(a,c(v)). +ac(b,b). +ac(c,and(a,b)). +ac(d,neg(b)).`; + +function AdfNewForm({ fetchProblems }: { fetchProblems: () => void; }) { + const { setLoading } = useContext(LoadingContext); + const { setStatus: setSnackbarInfo } = useContext(SnackbarContext); + const [isFileUpload, setFileUpload] = useState(false); + const [code, setCode] = useState(PLACEHOLDER); + const [filename, setFilename] = useState(''); + const [parsing, setParsing] = useState('Naive'); + const [name, setName] = useState(''); + const fileRef = useRef(null); + + const addAdf = useCallback( + () => { + setLoading(true); + + const formData = new FormData(); + + if (isFileUpload && fileRef.current) { + const file = fileRef.current.files?.[0]; + if (file) { + formData.append('file', file); + } + } else { + formData.append('code', code); + } + + formData.append('parsing', parsing); + formData.append('name', name); + + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/add`, { + method: 'POST', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + body: formData, + }) + .then((res) => { + switch (res.status) { + case 200: + setSnackbarInfo({ message: 'Successfully added ADF problem!', severity: 'success', potentialUserChange: true }); + fetchProblems(); + break; + default: + setSnackbarInfo({ message: 'An error occured while adding the ADF problem.', severity: 'error', potentialUserChange: true }); + break; + } + }) + .finally(() => setLoading(false)); + }, + [isFileUpload, code, filename, parsing, name, fileRef.current], + ); + + return ( + + + + Add a new Problem + + + + { setFileUpload(newValue); setFilename(''); }} + > + + Write by Hand + + + Upload File + + + + + + + {isFileUpload ? ( + + + + ) : ( + + For more info on the syntax, have a + look + {' '} + here + . + + )} + multiline + fullWidth + variant="filled" + value={code} + onChange={(event) => { setCode(event.target.value); }} + /> + )} + + + + + + Parsing Strategy + setParsing(((e.target as HTMLInputElement).value) as Parsing)} + > + } label="Naive" /> + } label="Hybrid" /> + + + { setName(event.target.value); }} + /> + + + + + + ); +} + +export default AdfNewForm; diff --git a/frontend/src/components/adf-overview.tsx b/frontend/src/components/adf-overview.tsx new file mode 100644 index 0000000..8b13dbf --- /dev/null +++ b/frontend/src/components/adf-overview.tsx @@ -0,0 +1,164 @@ +import React, { + useRef, useState, useCallback, useEffect, useContext, +} from 'react'; + +import { + Chip, + Container, + Paper, + TableContainer, + Table, + TableHead, + TableRow, + TableCell, + TableBody, + Typography, +} from '@mui/material'; + +import AdfNewForm from './adf-new-form'; + +import { + AdfProblemInfo, + StrategySnakeCase, + StrategyCamelCase, + Task, + acsWithGraphOptToColor, + acsWithGraphOptToText, +} from './adf-details'; + +import SnackbarContext from './snackbar-context'; + +function AdfOverview() { + const { status: snackbarInfo } = useContext(SnackbarContext); + const [problems, setProblems] = useState([]); + + const isFirstRender = useRef(true); + + const fetchProblems = useCallback( + () => { + fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/adf/`, { + method: 'GET', + credentials: process.env.NODE_ENV === 'development' ? 'include' : 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((res) => { + switch (res.status) { + case 200: + res.json().then((resProblems) => { + setProblems(resProblems); + }); + break; + case 401: + setProblems([]); + break; + default: + break; + } + }); + }, + [setProblems], + ); + + useEffect( + () => { + // TODO: having the info if the user may have changed on the snackbar info + // is a bit lazy and unclean; be better! + if (isFirstRender.current || snackbarInfo?.potentialUserChange) { + isFirstRender.current = false; + + fetchProblems(); + } + }, + [snackbarInfo?.potentialUserChange], + ); + + useEffect( + () => { + // if there is a running task, fetch problems again after 20 seconds + let timeout: ReturnType; + if (problems.some((p) => p.running_tasks.length > 0)) { + timeout = setTimeout(() => fetchProblems(), 20000); + } + + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, + [problems], + ); + + return ( + <> + + ADF-BDD.DEV + + {problems.length > 0 + && ( + + + + Existing Problems + + + + + + ADF Problem Name + Parse Status + Grounded Solution + Complete Solution + Stable Solution + Stable Solution (Counting Method A) + Stable Solution (Counting Method B) + Stable Solution (Nogood-Based) + + + + {problems.map((problem) => ( + + + {problem.name} + + { + (() => { + const status = problem.acs_per_strategy.parse_only; + const running = problem.running_tasks.some((t: Task) => t.type === 'Parse'); + + const color = acsWithGraphOptToColor(status, running); + const text = acsWithGraphOptToText(status, running); + + return ; + })() + } + { + (['Ground', 'Complete', 'Stable', 'StableCountingA', 'StableCountingB', 'StableNogood'] as StrategyCamelCase[]).map((strategy) => { + const status = problem.acs_per_strategy[strategy.replace(/^([A-Z])/, (_, p1) => p1.toLowerCase()).replace(/([A-Z])/g, (_, p1) => `_${p1.toLowerCase()}`) as StrategySnakeCase]; + const running = problem.running_tasks.some((t: Task) => t.type === 'Solve' && t.content === strategy); + + const color = acsWithGraphOptToColor(status, running); + const text = acsWithGraphOptToText(status, running); + + return ; + }) + } + + ))} + +
+
+
+
+ )} + + + ); +} + +export default AdfOverview; diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index b312717..bd6b9f8 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -1,180 +1,83 @@ -import * as React from 'react'; +import React, { useState, useMemo } from 'react'; + +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import { + Alert, + AlertColor, Backdrop, - Button, CircularProgress, CssBaseline, - Container, - FormControl, - FormControlLabel, - FormLabel, - Link, - Pagination, - Paper, - Radio, - RadioGroup, - Typography, - TextField, + Snackbar, + useMediaQuery, } from '@mui/material'; import LoadingContext from './loading-context'; -import GraphG6, { GraphProps } from './graph-g6'; +import SnackbarContext from './snackbar-context'; import Footer from './footer'; +import AdfOverview from './adf-overview'; +import AdfDetails from './adf-details'; -const { useState, useCallback, useMemo } = React; - -const darkTheme = createTheme({ - palette: { - mode: 'dark', +const browserRouter = createBrowserRouter([ + { + path: '/', + element: , }, -}); - -const placeholder = `s(a). -s(b). -s(c). -s(d). -ac(a,c(v)). -ac(b,b). -ac(c,and(a,b)). -ac(d,neg(b)).`; - -enum Parsing { - Naive = 'Naive', - Hybrid = 'Hybrid', -} - -enum Strategy { - ParseOnly = 'ParseOnly', - Ground = 'Ground', - Complete = 'Complete', - Stable = 'Stable', - StableCountingA = 'StableCountingA', - StableCountingB = 'StableCountingB', - StableNogood = 'StableNogood', -} + { + path: '/:adfName', + element: , + }, +]); function App() { - const [loading, setLoading] = useState(false); - const [code, setCode] = useState(placeholder); - const [parsing, setParsing] = useState(Parsing.Naive); - const [graphs, setGraphs] = useState(); - const [graphIndex, setGraphIndex] = useState(0); + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - const submitHandler = useCallback( - (strategy: Strategy) => { - setLoading(true); - - fetch(`${process.env.NODE_ENV === 'development' ? '//localhost:8080' : ''}/solve`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ code, strategy, parsing }), - }) - .then((res) => res.json()) - .then((data) => { - setGraphs(data); - setGraphIndex(0); - }) - .finally(() => setLoading(false)); - // TODO: error handling - }, - [code, parsing], + const theme = useMemo( + () => createTheme({ + palette: { + mode: prefersDarkMode ? 'dark' : 'light', + }, + }), + [prefersDarkMode], ); + const [loading, setLoading] = useState(false); const loadingContext = useMemo(() => ({ loading, setLoading }), [loading, setLoading]); + const [snackbarInfo, setSnackbarInfo] = useState<{ + message: string, + severity: AlertColor, + potentialUserChange: boolean, + } | undefined>(); + const snackbarContext = useMemo( + () => ({ status: snackbarInfo, setStatus: setSnackbarInfo }), + [snackbarInfo, setSnackbarInfo], + ); + return ( - + - -
- - Solve your ADF Problem with OBDDs! - + + +
+ +
+
- - - For more info on the syntax, have a - look - {' '} - here - . - - )} - multiline - fullWidth - variant="filled" - value={code} - onChange={(event) => { setCode(event.target.value); }} - /> - - - - Parsing Strategy - setParsing(((e.target as HTMLInputElement).value) as Parsing)} - > - } label="Naive" /> - } label="Hybrid" /> - - -
-
- - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - - {' '} - -
- - {graphs - && ( - - {graphs.length > 1 - && ( - <> - Models: -
- setGraphIndex(value - 1)} /> - - )} - {graphs.length > 0 - && ( - - - - )} - {graphs.length === 0 - && <>No models!} -
- )} -
-